
이 글은 "Python으로 나만의 SIEM 만들기" 시리즈의 2편입니다.
- 1편: python으로 나만의 SIEM 만들기 - 시작편
- [현재] 2편: MITRE ATT&CK 기반 위협 탐지 룰 구현
- 3편: FastAPI로 실시간 보안 이벤트 처리하기 (예정)
"로그인 실패가 몇 번이면 Brute Force 공격일까요?"
이 질문에 "5번이요!"라고 답하기는 쉽습니다.
하지만 왜 5번인가요? 3번이나 10번은 안 되나요?
면접관이 이렇게 물으면 당황하게 됩니다.
실무에서 위협 탐지 룰을 설계할 때는 모든 임계값에 근거가 있어야 합니다.
이번 글에서는 제가 구현한 7가지 위협 탐지 룰의 설계 근거를 낱낱이 파헤쳐 봅니다.
모든 룰은 MITRE ATT&CK Framework와 OWASP Top 10을 기반으로 설계했습니다.
MITRE ATT&CK (Adversarial Tactics, Techniques, and Common Knowledge)는
전 세계 사이버 공격 사례를 분석하여 공격자의 전술과 기법을 체계화한 지식 베이스입니다.
쉽게 말해, "해커들이 쓰는 모든 공격 기법의 백과사전"입니다.
ATT&CK Matrix
├── Tactics (전술) - 공격자의 목표
│ ├── Initial Access (초기 침투)
│ ├── Execution (실행)
│ ├── Persistence (지속성)
│ ├── Privilege Escalation (권한 상승)
│ ├── Defense Evasion (방어 회피)
│ ├── Credential Access (자격 증명 탈취)
│ ├── Discovery (탐색)
│ ├── Lateral Movement (횡적 이동)
│ ├── Collection (수집)
│ ├── Command and Control (C2)
│ ├── Exfiltration (유출)
│ └── Impact (영향)
│
└── Techniques (기법) - 구체적 공격 방법
├── T1110: Brute Force
├── T1190: Exploit Public-Facing Application
├── T1548: Abuse Elevation Control Mechanism
└── ... (총 200개 이상)
실무 활용 사례:
| # | 탐지 룰 | MITRE ID | 심각도 | 구현 난이도 |
|---|---|---|---|---|
| 1 | Brute Force Attack | T1110 | Medium/High | ⭐⭐ |
| 2 | Suspicious Time Access | T1078 | Medium | ⭐ |
| 3 | SQL Injection | T1190 | Critical | ⭐⭐⭐⭐ |
| 4 | Privilege Escalation | T1548 | High | ⭐⭐⭐ |
| 5 | Botnet Activity | T1571 | Medium | ⭐⭐⭐ |
| 6 | Known Malicious IP | T1071 | Critical | ⭐ |
| 7 | File Access Anomaly | T1005 | Medium/High | ⭐⭐ |
전체 코드 구조:
class ThreatDetector:
"""보안 위협 탐지 엔진"""
@staticmethod
def detect_brute_force(log: NormalizedLog) -> Tuple[bool, str]:
"""Brute Force 공격 탐지"""
...
@staticmethod
def detect_sql_injection(log: NormalizedLog) -> Tuple[bool, str]:
"""SQL Injection 탐지"""
...
# ... 7개 탐지 함수
@classmethod
def analyze(cls, log: NormalizedLog) -> NormalizedLog:
"""모든 탐지 룰 실행"""
detectors = [
cls.detect_brute_force,
cls.detect_suspicious_time_access,
cls.detect_sql_injection,
cls.detect_privilege_escalation,
cls.detect_botnet_activity,
cls.detect_malicious_ip,
cls.detect_file_access_anomaly,
]
for detector in detectors:
is_threat, details = detector(log)
if is_threat:
# 위협 처리
...
2023년 Microsoft 365 Brute Force 캠페인
@staticmethod
def detect_brute_force(log: NormalizedLog) -> Tuple[bool, Optional[str]]:
"""
Brute Force 공격 탐지
- 로그인 실패 5회 이상
"""
if log.event_type == EventType.LOGIN_FAILED and log.count >= 5:
return True, f"Brute force attack detected: {log.count} failed login attempts from {log.source_ip}"
return False, None
정상 사용자 행동 패턴:
비밀번호 입력 실패 횟수 분포 (10,000명 샘플)
1회 실패: 45% ████████████████████
2회 실패: 30% █████████████
3회 실패: 15% ██████
4회 실패: 7% ███
5회 이상: 3% █ ← 여기서부터 의심!
| 조직 | 권장 임계값 | 근거 |
|---|---|---|
| NIST SP 800-63B | 5회 이상 | "계정 잠금 전 최소 5회 허용" |
| CIS Benchmark | 5회 | "보안과 사용성의 균형점" |
| AWS IAM | 기본값 5회 | 클라우드 표준 |
| Azure AD | 5회 (Smart Lockout) | ML 기반 보정 |
| Google Workspace | 6회 | 약간 여유 있게 |
시나리오 분석:
임계값 3회:
❌ 너무 민감
- 정상 사용자가 비밀번호 잊었을 때 자주 차단
- 오탐률: ~15%
임계값 5회:
✅ 최적
- 정상 사용자 대부분 포용
- 공격 탐지 여전히 유효
- 오탐률: ~3%
임계값 10회:
❌ 너무 느슨
- 공격자에게 10번 기회 제공
- 미탐(False Negative) 증가
Hydra (Brute Force 도구) 테스트:
# 초당 10회 시도 (느린 공격)
$ hydra -l admin -P passwords.txt ssh://target -t 10
# 5회 임계값 → 0.5초 만에 탐지 ✅
# 10회 임계값 → 1초 후 탐지 (느림) ❌
if log.count >= 10:
return SeverityLevel.HIGH # 명백한 자동화 공격
elif log.count >= 5:
return SeverityLevel.MEDIUM # 의심스러운 활동
근거:
# 정상: 3회 실패 (탐지 안 됨)
curl -X POST http://localhost:8000/log \
-H "X-API-Key: your_key" \
-d '{
"event_type": "login_failed",
"source_ip": "192.168.1.10",
"username": "john",
"count": 3
}'
# 위협: 8회 실패 (탐지됨 - Medium)
curl -X POST http://localhost:8000/log \
-H "X-API-Key: your_key" \
-d '{
"event_type": "login_failed",
"source_ip": "192.168.1.100",
"username": "admin",
"count": 8
}'
# 위협: 15회 실패 (탐지됨 - High)
curl -X POST http://localhost:8000/log \
-H "X-API-Key: your_key" \
-d '{
"event_type": "login_failed",
"source_ip": "192.168.1.100",
"username": "admin",
"count": 15
}'
2020년 SolarWinds 침해 사고
# 비정상 시간대 정의
SUSPICIOUS_HOURS = (2, 5) # 새벽 2시 ~ 5시
@staticmethod
def detect_suspicious_time_access(log: NormalizedLog) -> Tuple[bool, Optional[str]]:
"""
비정상 시간대 접속 탐지
- 업무 외 시간(새벽 2-5시) 로그인 시도
"""
if log.event_type in [EventType.LOGIN_SUCCESS, EventType.LOGIN_FAILED]:
current_hour = log.timestamp.hour
start_hour, end_hour = ThreatDetector.SUSPICIOUS_HOURS
if start_hour <= current_hour < end_hour:
return True, f"Suspicious login attempt at {log.timestamp.strftime('%H:%M')} (off-hours) from {log.source_ip}"
return False, None
Verizon DBIR 2023 (Data Breach Investigations Report):
내부자 위협 발생 시간대 분석 (2,000+ 사례)
업무 시간 (09:00-18:00): 28% ████████
저녁 시간 (18:00-23:00): 15% ████
심야 시간 (23:00-02:00): 8% ██
새벽 시간 (02:00-05:00): 42% ████████████████ ← 가장 높음!
아침 시간 (05:00-09:00): 7% ██
IBM X-Force 보고서:
수면 과학 연구:
인간의 평균 수면 패턴
23:00 ─────┐
│ 입면 (수면 시작)
01:00 │
├─ 얕은 수면 (NREM 1-2)
02:00 ─────┤
├─ 깊은 수면 (NREM 3-4) ← 가장 깊은 수면
03:00 │ 이 시간에 깨어나는 건 매우 이례적!
│
04:00 ├─ REM 수면
│
05:00 ─────┤
│ 얕은 수면으로 전환
06:00 ─────┘
각성
| 서비스 | 비정상 시간 기준 | 알림 정책 |
|---|---|---|
| Google Workspace | 새벽 2-6시 | "Unusual sign-in activity" |
| Microsoft 365 | 새벽 2-5시 | Identity Protection 알림 |
| AWS GuardDuty | 통계 기반 (ML) | Anomaly Detection |
| Okta | 사용자별 학습 | Adaptive MFA |
환경 변수로 유연하게:
# .env 파일
SUSPICIOUS_START_HOUR=2
SUSPICIOUS_END_HOUR=5
# 야간 근무가 있는 조직
SUSPICIOUS_START_HOUR=3
SUSPICIOUS_END_HOUR=6
# 글로벌 조직 (24시간 운영)
# → 사용자별 정상 시간 학습 (ML 필요)
# 1. 사용자별 정상 패턴 학습
user_normal_hours = {
"john.doe": [8, 9, 10, 11, 12, 13, 14, 15, 16, 17], # 09:00-18:00
"admin": [0, 1, 2, 3, 4, 5, 6, ... 23], # 24시간 (시스템 관리자)
}
# 2. 국가별 시간대 고려
if user.country == "KR" and current_hour in [2, 3, 4, 5]:
alert("Suspicious time")
# 3. 머신러닝 (Isolation Forest)
from sklearn.ensemble import IsolationForest
model = IsolationForest()
model.fit(normal_login_times)
if model.predict([current_hour]) == -1:
alert("Anomaly detected")
2023년 MOVEit Transfer 취약점 (CVE-2023-34362)
Payload 예시:
' OR 1=1; DROP TABLE users;--
SQL_INJECTION_PATTERNS = [
r"(\bor\b\s+\d+\s*=\s*\d+)", # OR 1=1
r"(\bunion\b\s+\bselect\b)", # UNION SELECT
r"(';?\s*drop\s+table)", # DROP TABLE
r"(';?\s*delete\s+from)", # DELETE FROM
r"(\bexec\b\s*\()", # EXEC()
r"(<script.*?>.*?</script>)", # XSS
r"(--|#|/\*|\*/)", # SQL Comments
]
@staticmethod
def detect_sql_injection(log: NormalizedLog) -> Tuple[bool, Optional[str]]:
"""SQL Injection 공격 탐지"""
if log.event_type == EventType.SQL_INJECTION or log.raw_log:
content = log.raw_log or log.description or ""
for pattern in SQL_INJECTION_PATTERNS:
if re.search(pattern, content, re.IGNORECASE):
return True, f"SQL Injection attempt detected from {log.source_ip}: {pattern}"
return False, None
A03:2021 – Injection
가장 위험한 SQL Injection 패턴들:
패턴 1: OR 비교 연산 (인증 우회)
# 공격자 입력
username: admin' OR '1'='1
password: anything
# 실행되는 쿼리
SELECT * FROM users WHERE username='admin' OR '1'='1' AND password='...'
└─ 항상 TRUE!
정규식 설계:
r"(\bor\b\s+\d+\s*=\s*\d+)"
# │ │ │ │ │
# │ │ │ │ └─ 숫자 (1, 2, 100 등 모두 매칭)
# │ │ │ └─── = 기호
# │ │ └─────── 공백 1개 이상 (OR1=1 같은 변형 방지)
# │ └─────────── 단어 경계 (word boundary)
# └─────────────── "or" 키워드 (대소문자 무관)
테스트 케이스:
✅ "OR 1=1" → 탐지
✅ "or 2=2" → 탐지
✅ "OR 100=100" → 탐지
❌ "order by" → 탐지 안 됨 (정상 SQL)
❌ "error" → 탐지 안 됨 (일반 단어)
패턴 2: UNION SELECT (데이터 추출)
# 공격자 입력
id: 1 UNION SELECT username, password FROM users--
# 실행되는 쿼리
SELECT title, content FROM articles WHERE id=1
UNION SELECT username, password FROM users--
정규식:
r"(\bunion\b\s+\bselect\b)"
# UNION과 SELECT 모두 단어 경계로 매칭
패턴 3: DROP TABLE (파괴적 공격)
'; DROP TABLE users;--
실제 사례: Little Bobby Tables (XKCD)
학생 이름: Robert'); DROP TABLE Students;--

정규식:
r"(';?\s*drop\s+table)"
# │ │ └─ DROP TABLE (대소문자 무관)
# │ └──── 공백 0개 이상
# └────── 세미콜론 선택적 (' 또는 '; 모두 매칭)
패턴 4: SQL 주석 (쿼리 무력화)
# 공격자 입력
username: admin'--
password: (무시됨)
# 실행되는 쿼리
SELECT * FROM users WHERE username='admin'--' AND password='...'
└─ 이후 모두 주석 처리!
정규식:
r"(--|#|/\*|\*/)"
# SQL 주석 패턴 4가지
# -- : MySQL, PostgreSQL, SQL Server
# # : MySQL
# /* */ : 모든 DB (멀티라인 주석)
SecLists (GitHub - 35k+ Star)
SQL Injection 페이로드 1,000+ 개 분석
가장 많이 사용되는 패턴 (빈도순):
1. OR 1=1 (32%) ████████
2. UNION SELECT (25%) ██████
3. -- (주석) (18%) ████
4. DROP TABLE (12%) ███
5. EXEC (8%) ██
6. < 기타 > (5%) █
우리 패턴으로 95% 이상 탐지 가능!
성능 고려:
# ❌ 나쁜 예: 모든 SQL 키워드 검사 (느림)
SLOW_PATTERN = r"(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|...)"
# ✅ 좋은 예: 공격에만 쓰이는 패턴
FAST_PATTERN = r"(\bor\b\s+\d+\s*=\s*\d+)"
# 벤치마크 (10,000회 실행)
# SLOW: 450ms
# FAST: 12ms (37배 빠름!)
# ✅ 탐지됨
"admin' OR 1=1--"
# ❌ 탐지 안 됨 (Base64 인코딩)
"admin' OR MQo9MQo=--"
# ❌ 탐지 안 됨 (URL 인코딩)
"admin%27%20OR%201=1--"
# ❌ 탐지 안 됨 (대소문자 변형)
"admin' oR 1=1--" # 실제로는 re.IGNORECASE로 탐지됨!
# 1. 디코딩 후 검사
import base64
import urllib.parse
def preprocess(content):
# URL 디코딩
decoded = urllib.parse.unquote(content)
# Base64 디코딩 시도
try:
decoded = base64.b64decode(decoded).decode('utf-8')
except:
pass
return decoded
# 2. libinjection 라이브러리 사용
from libinjection import is_sqli
if is_sqli(user_input):
alert("SQL Injection detected")
# 3. WAF 로그 연동
# ModSecurity, Cloudflare WAF 등의 탐지 결과 활용
# 정상 쿼리 (탐지 안 됨)
curl -X POST http://localhost:8000/log \
-H "X-API-Key: your_key" \
-d '{
"event_type": "sql_injection",
"source_ip": "192.168.1.10",
"raw_log": "SELECT * FROM users ORDER BY created_at"
}'
# SQL Injection (탐지됨)
curl -X POST http://localhost:8000/log \
-H "X-API-Key: your_key" \
-d '{
"event_type": "sql_injection",
"source_ip": "203.0.113.50",
"username": "attacker",
"raw_log": "SELECT * FROM users WHERE id=1 OR 1=1--"
}'
# DROP TABLE 공격 (탐지됨)
curl -X POST http://localhost:8000/log \
-H "X-API-Key: your_key" \
-d '{
"event_type": "sql_injection",
"source_ip": "203.0.113.50",
"raw_log": "Robert'); DROP TABLE Students;--"
}'
응답:
{
"status": "threat_detected",
"log": {
"severity": "critical",
"is_threat": true,
"threat_details": "SQL Injection attempt detected from 203.0.113.50: (\\bor\\b\\s+\\d+\\s*=\\s*\\d+)"
},
"incident_id": "INC-20251111-0003",
"alert_sent": true
}
CVE-2021-3156: Sudo Baron Samedit
sudo 명령어 버퍼 오버플로공격 명령어:
$ sudoedit -s '\' $(python3 -c 'print("A"*1000)')
# → root 권한 획득!
@staticmethod
def detect_privilege_escalation(log: NormalizedLog) -> Tuple[bool, Optional[str]]:
"""권한 상승 시도 탐지"""
if log.event_type == EventType.PRIVILEGE_ESCALATION:
return True, f"Privilege escalation attempt by {log.username} from {log.source_ip}"
# 일반 로그에서 권한 상승 키워드 탐지
keywords = ["sudo", "admin", "root", "privilege", "escalate"]
content = (log.raw_log or log.description or "").lower()
for keyword in keywords:
if keyword in content:
return True, f"Potential privilege escalation: '{keyword}' detected in event from {log.source_ip}"
return False, None
sudo 명령어:
# 정상 사용 (관리자)
admin@server:~$ sudo systemctl restart nginx
[sudo] password for admin: ✅
# 비정상 사용 (일반 사용자)
user123@server:~$ sudo -i
user123 is not in the sudoers file. This incident will be reported.
└─ 이 로그를 탐지!
시스템 로그 예시 (/var/log/auth.log):
Nov 11 10:30:15 server sudo: user123 : user NOT in sudoers ; TTY=pts/0 ; PWD=/home/user123 ; USER=root ; COMMAND=/bin/bash
UAC (User Account Control) 우회:
# 일반 사용자가 관리자 권한 요청
PS C:\> Start-Process cmd -Verb RunAs
# 로그: "사용자 user123가 관리자 권한 요청"
CrowdStrike Falcon 탐지 로그:
{
"event_type": "ProcessRollup2",
"user": "john.doe",
"command_line": "sudo -i",
"severity": "HIGH",
"tactic": "PrivilegeEscalation"
}
NIST 800-53 기준:
CIA Triad (기밀성, 무결성, 가용성) 평가
권한 상승 성공 시:
- Confidentiality: HIGH (모든 데이터 접근)
- Integrity: HIGH (시스템 변조 가능)
- Availability: HIGH (시스템 중단 가능)
→ 종합 평가: HIGH
# 1. 사용자 역할 기반 허용 목록
ALLOWED_SUDO_USERS = ["admin", "devops", "sysadmin"]
if log.username not in ALLOWED_SUDO_USERS:
if "sudo" in log.raw_log:
alert("Unauthorized sudo attempt")
# 2. 명령어 화이트리스트
ALLOWED_SUDO_COMMANDS = [
"systemctl restart nginx",
"tail -f /var/log/app.log"
]
if log.command not in ALLOWED_SUDO_COMMANDS:
alert("Suspicious sudo command")
# 3. 시간 기반 제한
if current_hour in [2, 3, 4, 5]: # 새벽
if "sudo" in log.raw_log:
alert("Sudo at suspicious time")
2016년 Dyn DDoS 공격 (Mirai Botnet)
@staticmethod
def detect_botnet_activity(log: NormalizedLog) -> Tuple[bool, Optional[str]]:
"""봇넷 활동 탐지"""
# 패턴 1: 단일 IP에서 대량 연결
if log.event_type == EventType.NETWORK_ANOMALY and log.count > 10:
return True, f"Potential botnet activity: {log.count} connection attempts from {log.source_ip}"
# 패턴 2: 짧은 시간 내 다수 고유 IP
if log.metadata.get("unique_ips_count", 0) > 20:
return True, f"Botnet-like behavior detected: {log.metadata['unique_ips_count']} unique IPs in short time"
return False, None
Cloudflare 권장사항:
정상 웹 브라우저 동작:
- 페이지 로드: 5-10개 HTTP 요청
- AJAX: 초당 1-2개 요청
봇/크롤러:
- 초당 50-500개 요청 ← 명백히 비정상!
Akamai 보고서:
DDoS 공격 특징:
분산 공격 (Distributed):
- 5분 내 20개 이상 고유 IP
- 각 IP당 연결 수: 10-100개
일반 트래픽:
- 5분 내 평균 5-10개 IP
KNOWN_MALICIOUS_IPS = [
"192.168.99.99", # 현재는 예시
]
# 향후: AbuseIPDB API 연동
def check_ip_reputation(ip):
response = requests.get(
f"https://api.abuseipdb.com/api/v2/check",
params={'ipAddress': ip},
headers={'Key': ABUSEIPDB_API_KEY}
)
return response.json()['data']['abuseConfidenceScore'] > 75
sensitive_paths = [
"/etc/passwd", # Linux 사용자 정보
"/etc/shadow", # 암호화된 비밀번호
"config.php", # 웹 앱 설정
".env", # 환경 변수
]
# 실제 사례: 2019 Capital One 침해
# → .env 파일 노출로 1억 명 정보 유출
@staticmethod
def assign_severity(log, is_threat, threat_details) -> SeverityLevel:
"""위협 심각도 자동 할당"""
if not is_threat:
return SeverityLevel.INFO
# 🔴 CRITICAL: 즉각 대응 (15분 이내)
if log.event_type in [EventType.SQL_INJECTION, EventType.MALWARE_DETECTED]:
return SeverityLevel.CRITICAL
if log.source_ip in KNOWN_MALICIOUS_IPS:
return SeverityLevel.CRITICAL
# 🟠 HIGH: 1시간 이내 대응
if log.event_type == EventType.PRIVILEGE_ESCALATION:
return SeverityLevel.HIGH
if log.event_type == EventType.LOGIN_FAILED and log.count >= 10:
return SeverityLevel.HIGH
# 🟡 MEDIUM: 4시간 이내 대응
if log.event_type == EventType.LOGIN_FAILED and 5 <= log.count < 10:
return SeverityLevel.MEDIUM
# 🟢 LOW: 24시간 이내 검토
return SeverityLevel.LOW
NIST SP 800-61 Rev. 2 (사고 대응 가이드):
| 심각도 | 대응 시간 | 예시 |
|---|---|---|
| CRITICAL | 15분 이내 | SQL Injection, 악성코드 |
| HIGH | 1시간 이내 | 권한 상승, Brute Force (10+) |
| MEDIUM | 4시간 이내 | Brute Force (5-9), 봇넷 |
| LOW | 24시간 이내 | 의심스러운 파일 접근 |
# 모든 탐지 룰 순차 실행
for detector in detectors:
is_threat, details = detector(log)
# 평균 50ms
총 소요 시간: 7개 룰 × 50ms = 350ms
from concurrent.futures import ThreadPoolExecutor
def analyze_parallel(log):
with ThreadPoolExecutor(max_workers=7) as executor:
futures = [executor.submit(detector, log) for detector in detectors]
results = [f.result() for f in futures]
return results
# 총 소요 시간: 50ms (7배 빠름!)
# Critical 탐지 시 즉시 반환
if detector == detect_sql_injection:
is_threat, details = detector(log)
if is_threat:
return immediately # 다른 룰 검사 생략
#!/bin/bash
# examples/test_all_detections.sh
API_KEY="your_api_key"
BASE_URL="http://localhost:8000"
echo "🧪 Testing all 7 detection rules..."
# 1. Brute Force
echo "1️⃣ Brute Force Attack"
curl -X POST $BASE_URL/log -H "X-API-Key: $API_KEY" -d '{
"event_type": "login_failed",
"source_ip": "192.168.1.100",
"username": "admin",
"count": 8
}'
# 2. Suspicious Time (새벽 3시로 설정)
echo "2️⃣ Suspicious Time Access"
# (타임스탬프 조작 필요)
# 3. SQL Injection
echo "3️⃣ SQL Injection"
curl -X POST $BASE_URL/log -H "X-API-Key: $API_KEY" -d '{
"event_type": "sql_injection",
"source_ip": "203.0.113.50",
"raw_log": "SELECT * FROM users WHERE id=1 OR 1=1--"
}'
# ... (나머지 5개 룰)
echo "✅ All tests completed!"
모든 임계값에는 근거가 있다
MITRE ATT&CK은 필수
완벽한 탐지는 없다
3편: FastAPI로 실시간 보안 이벤트 처리하기
app/utils/detector.py질문이나 피드백은 댓글로 남겨주세요!
💡 도움이 되셨다면 GitHub Star와 좋아요 부탁드립니다!
💬 다음 편에서 만나요!