security
CHAPTER 76 / 84
읽기 약 2분
FUNCTION
이상 패턴 탐지: 웹 공격
핵심 개념
SQL Injection·XSS·경로 탐색 시도를 웹 서버 로그에서 자동 분류한다.
본문
웹 공격 패턴 정규식
# ⚠️ 이 코드는 허가된 환경에서만 사용하세요.
import re
ATTACK_PATTERNS = {
'sqli': re.compile(
r'(?i)('
r'\bUNION\s+(?:ALL\s+)?SELECT\b|'
r'\bOR\s+1\s*=\s*1\b|'
r'\bAND\s+1\s*=\s*1\b|'
r"'\s*OR\s*'1'\s*=\s*'1|"
r'\bSLEEP\s*\(\d+\)|'
r'\bWAITFOR\s+DELAY\b|'
r'--\s*$|'
r"\\x27\\s*OR\\s*\\x271\\x27=\\x271"
r')'
),
'xss': re.compile(
r'(?i)('
r'<script[^>]*>|'
r'javascript:[^\s]+|'
r'on(?:click|load|error|mouseover|focus)\s*=|'
r'<img[^>]*onerror|'
r'<iframe[^>]*src\s*=|'
r'\balert\s*\(|'
r'document\.cookie|'
r'eval\s*\('
r')'
),
'path_traversal': re.compile(
r'(?i)('
r'\.\.[\\/]|'
r'%2e%2e[\\/]|'
r'%252e%252e|'
r'/etc/passwd|'
r'\\windows\\system32|'
r'C:\\Windows'
r')'
),
'command_injection': re.compile(
r'(?i)('
r';\s*(?:cat|ls|whoami|id|nc|wget|curl)\s|'
r'\|\s*(?:cat|ls|whoami|id)|'
r'\$\(.*\)|'
r'`.*`|'
r'&&\s*\w+'
r')'
),
'lfi_rfi': re.compile(
r'(?i)('
r'php://(?:filter|input)|'
r'data://text/plain|'
r'expect://|'
r'file://|'
r'(?:include|require)\s*=|'
r'\?file=https?://'
r')'
),
'malicious_useragent': re.compile(
r'(?i)('
r'sqlmap|'
r'nikto|'
r'nmap|'
r'masscan|'
r'gobuster|'
r'dirbuster|'
r'burpsuite|'
r'havij|'
r'curl/[\d.]+\s+(?:scan|test)'
r')'
),
}
def classify_request(line: str, parsed: dict | None = None) -> list[str]:
"""단일 로그 라인의 공격 종류 판별 — 다중 매칭 가능."""
detected = []
target = line # 전체 라인에서 패턴 검색
for attack, pattern in ATTACK_PATTERNS.items():
if pattern.search(target):
detected.append(attack)
return detectedURL 디코딩 — 인코딩 우회 방어
from urllib.parse import unquote_plus
def normalize_for_detection(url: str) -> str:
"""다중 인코딩까지 디코딩해서 정규식 매칭률 향상."""
decoded = url
for _ in range(3): # %2527 같은 다중 인코딩
new = unquote_plus(decoded)
if new == decoded:
break
decoded = new
return decoded.lower()
# 예: %2E%2E%2F → ../
print(normalize_for_detection('GET /api?file=%2E%2E%2Fetc%2Fpasswd HTTP/1.1'))
# 'get /api?file=../etc/passwd http/1.1'로그 전체 분류
from collections import Counter
APACHE_COMBINED = re.compile(
r'^(?P<ip>\S+)\s+\S+\s+\S+\s+'
r'\[(?P<time>[^\]]+)\]\s+'
r'"(?P<method>\S+)\s+(?P<path>\S+)\s+\S+"\s+'
r'(?P<status>\d{3})\s+\S+\s+'
r'"(?P<referer>[^"]*)"\s+'
r'"(?P<user_agent>[^"]*)"'
)
def scan_web_attacks(log_path: str) -> dict:
"""웹 로그에서 공격 시도 자동 분류."""
findings = []
attack_counter = Counter()
ip_attacks = Counter()
with open(log_path, encoding='utf-8', errors='replace') as f:
for line in f:
m = APACHE_COMBINED.match(line)
if not m:
continue
full_request = normalize_for_detection(
f'{m["method"]} {m["path"]} {m["user_agent"]}'
)
attacks = classify_request(full_request)
if attacks:
findings.append({
'ip': m['ip'],
'time': m['time'],
'request': f'{m["method"]} {m["path"]}',
'user_agent': m['user_agent'][:100],
'attacks': attacks,
'status': int(m['status']),
})
for atk in attacks:
attack_counter[atk] += 1
ip_attacks[m['ip']] += 1
return {
'total_findings': len(findings),
'attack_distribution': dict(attack_counter),
'top_attacker_ips': ip_attacks.most_common(10),
'sample_findings': findings[:50],
}의심도 점수화
def severity_score(finding: dict) -> int:
"""공격의 심각도 점수 (0~100)."""
score = 0
weights = {
'sqli': 30,
'command_injection': 35,
'lfi_rfi': 25,
'xss': 15,
'path_traversal': 20,
'malicious_useragent': 10,
}
for atk in finding['attacks']:
score += weights.get(atk, 5)
# 200 응답 = 공격 성공 가능
if finding['status'] == 200:
score += 20
# 404/403도 시도 자체로 의심
if finding['status'] in (404, 403):
score += 5
return min(score, 100)
def filter_critical(findings: list[dict], min_score: int = 50) -> list[dict]:
"""심각도 50점 이상만 — 사람이 검토할 우선순위."""
scored = [{'score': severity_score(f), **f} for f in findings]
return [f for f in scored if f['score'] >= min_score]⚠️ False Positive 주의
# 정상 트래픽 중 오탐 사례:
LEGITIMATE_PATTERNS_THAT_LOOK_BAD = {
"?search=cookies": '쇼핑몰의 정상 검색 — "cookies"가 XSS 키워드 같음',
"/api/users/select": '경로에 SELECT가 있어도 SQLi 아님',
"User-Agent: curl/8.0 (test from CI)": 'CI 모니터링 봇',
}
# 해결책 — 응답 코드 + 컨텍스트 결합
def is_real_attack(finding: dict) -> bool:
"""단순 키워드 매칭이 아닌, 패턴+컨텍스트로 판단."""
# 200 응답 + sqli 키워드 → 진짜 의심
if 'sqli' in finding['attacks'] and finding['status'] == 200:
return True
# 404 + path_traversal → 시도는 했지만 실패
if 'path_traversal' in finding['attacks'] and finding['status'] != 200:
return True # 로그는 남기되 차단은 신중
# 정상 검색 → 무시
if finding['request'].startswith('/search'):
return False
return finding.get('score', 0) >= 70자동 차단 vs 알림
# 권장 흐름
ATTACK_RESPONSE = {
'block_immediately': [
'command_injection (수술적 정확도)',
'lfi_rfi (파일 노출 직접 위협)',
],
'alert_only': [
'sqli (오탐 가능성)',
'xss (입력 단계만)',
'path_traversal (정찰 단계)',
],
'log_only': [
'malicious_useragent (단순 봇)',
],
}AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 웹 공격 탐지 정규식의 false positive와 인코딩 우회 케이스를 분석하고 정확도를 높이는 패턴으로 개선해줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
OWASP Top 10 공격 유형별로 실전 페이로드 + 탐지 정규식 + 방어 코드를 복사 가능하게 정리해줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 웹 서버 로그를 분석해서 공격 종류별 분포·시간대·심각도 점수 관리자 대시보드용 리포트를 만들어줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 WAF 트렌드 — 자체 정규식 vs ModSecurity vs Cloudflare WAF 1인 SaaS에 적합한 선택을 솔직히 알려줘.
⭐ 이것만 기억하세요
이상 패턴 탐지: 웹 공격은 이 3가지만 확실히 잡으세요
1.6대 웹 공격 패턴(SQLi/XSS/Path Traversal/Command Injection/LFI/악성 UA)을 정규식으로 자동 분류할 수 있다
2.URL 다중 디코딩 + 응답 코드 결합으로 인코딩 우회와 false positive를 동시에 줄일 수 있다
3.다음 챕터에서 이 분류 결과를 시각화·리포트로 만든다
공유하기
진행도 76 / 84