diff --git a/.gitignore b/.gitignore index 8ae0b93..9b6a0fb 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ conf/domains.map run/ data/ *.state +*.lock # Python __pycache__/ diff --git a/docs/YARA_GUIDE.md b/docs/YARA_GUIDE.md new file mode 100644 index 0000000..2c91386 --- /dev/null +++ b/docs/YARA_GUIDE.md @@ -0,0 +1,983 @@ +# YARA 규칙 가이드 + +악성코드 및 웹쉘 탐지를 위한 YARA 사용법 정리 + +## 목차 + +1. [YARA란?](#yara란) +2. [설치](#설치) +3. [규칙 문법](#규칙-문법) +4. [웹쉘 탐지 규칙 예시](#웹쉘-탐지-규칙-예시) +5. [CLI 사용법](#cli-사용법) +6. [Python 연동](#python-연동) +7. [실시간 모니터링](#실시간-모니터링) +8. [공개 규칙 모음](#공개-규칙-모음) +9. [다른 방식과 비교](#다른-방식과-비교) + +--- + +## YARA란? + +YARA는 악성코드 연구자가 악성 샘플을 식별하고 분류하기 위해 만든 **패턴 매칭 도구**입니다. + +| 항목 | 설명 | +|------|------| +| 개발 | VirusTotal (Google 소속) | +| 용도 | 악성코드, 웹쉘, 악성 문서 탐지 | +| 방식 | 문자열 + 바이트 패턴 + 조건 매칭 | +| AI 여부 | ❌ 규칙 기반 (AI 없음) | +| 속도 | ⚡ 매우 빠름 (1-10ms/파일) | +| 라이선스 | BSD-3-Clause (무료) | + +### 핵심 개념 + +``` +YARA = 악성코드 전용 grep/정규식 + +파일 내용에서 특정 패턴을 찾고, +여러 조건을 조합하여 악성 여부를 판단 +``` + +--- + +## 설치 + +### Linux (Ubuntu/Debian) + +```bash +# 패키지 설치 +sudo apt update +sudo apt install yara + +# 버전 확인 +yara --version +``` + +### macOS + +```bash +brew install yara +``` + +### Python 바인딩 + +```bash +pip install yara-python +``` + +### 소스 컴파일 (최신 버전) + +```bash +# 의존성 설치 +sudo apt install automake libtool make gcc pkg-config libssl-dev + +# 소스 다운로드 및 컴파일 +git clone https://github.com/VirusTotal/yara.git +cd yara +./bootstrap.sh +./configure +make +sudo make install +sudo ldconfig +``` + +--- + +## 규칙 문법 + +### 기본 구조 + +```yara +rule 규칙이름 { + meta: + // 메타데이터 (선택사항) + description = "규칙 설명" + author = "작성자" + date = "2025-01-01" + severity = "high" + + strings: + // 탐지할 문자열/패턴 정의 + $string1 = "탐지할 문자열" + $string2 = { 48 65 6C 6C 6F } // 헥스 바이트 + $regex1 = /정규식 패턴/ + + condition: + // 탐지 조건 + any of them +} +``` + +### 문자열 정의 방식 + +```yara +rule StringTypes { + strings: + // 1. 일반 문자열 + $text = "eval(" + + // 2. 대소문자 무시 + $nocase = "system(" nocase + + // 3. 와이드 문자 (UTF-16) + $wide = "malware" wide + + // 4. 둘 다 + $both = "shell" wide ascii nocase + + // 5. 헥스 바이트 (바이너리) + $hex = { 4D 5A 90 00 } // MZ 헤더 + + // 6. 헥스 와일드카드 + $hex_wild = { 4D 5A ?? ?? } // ??는 아무 바이트 + + // 7. 헥스 점프 + $hex_jump = { 4D 5A [2-4] 00 } // 2-4바이트 사이 + + // 8. 정규식 + $regex = /eval\s*\(\s*\$_(GET|POST)/ + + // 9. XOR 인코딩 탐지 + $xor = "password" xor + + // 10. Base64 인코딩 탐지 + $b64 = "eval" base64 + + condition: + any of them +} +``` + +### 조건문 (condition) + +```yara +rule ConditionExamples { + strings: + $a = "eval(" + $b = "system(" + $c = "$_POST" + $d = "$_GET" + + condition: + // 기본 조건 + $a // $a가 존재 + $a and $b // 둘 다 존재 + $a or $b // 하나라도 존재 + not $a // $a가 없음 + + // 개수 조건 + any of them // 하나라도 존재 + all of them // 모두 존재 + 2 of them // 2개 이상 존재 + 2 of ($a, $b, $c) // 지정된 것 중 2개 이상 + + // 위치 조건 + $a at 0 // 파일 시작 위치 + $a in (0..100) // 0-100 바이트 범위 + + // 개수 세기 + #a > 5 // $a가 5번 이상 출현 + + // 파일 크기 + filesize < 1MB + filesize > 100 and filesize < 10KB + + // 매직 바이트 + uint16(0) == 0x5A4D // MZ 헤더 (PE 파일) + uint32(0) == 0x464C457F // ELF 헤더 + + // 복합 조건 + ($a or $b) and ($c or $d) + + // 반복문 + for any i in (1..#a) : (@a[i] < 100) +} +``` + +--- + +## 웹쉘 탐지 규칙 예시 + +### webshell_rules.yar + +```yara +/* + * PHP 웹쉘 탐지 규칙 + * Author: Security Team + * Last Updated: 2025-02-01 + */ + +// ============================================ +// 기본 웹쉘 탐지 +// ============================================ + +rule PHP_Webshell_Generic { + meta: + description = "일반적인 PHP 웹쉘 패턴" + severity = "high" + + strings: + // 코드 실행 함수 + $exec1 = "eval(" ascii nocase + $exec2 = "assert(" ascii nocase + $exec3 = "create_function(" ascii nocase + $exec4 = "call_user_func(" ascii nocase + $exec5 = /preg_replace\s*\([^)]*\/e/ ascii + + // 시스템 명령 실행 + $cmd1 = "system(" ascii nocase + $cmd2 = "exec(" ascii nocase + $cmd3 = "shell_exec(" ascii nocase + $cmd4 = "passthru(" ascii nocase + $cmd5 = "popen(" ascii nocase + $cmd6 = "proc_open(" ascii nocase + $cmd7 = /`[^`]+`/ ascii // 백틱 + + // 사용자 입력 + $input1 = "$_GET" ascii + $input2 = "$_POST" ascii + $input3 = "$_REQUEST" ascii + $input4 = "$_COOKIE" ascii + $input5 = "$_FILES" ascii + + condition: + // PHP 파일 시그니처 + (uint16(0) == 0x3F3C or uint16(0) == 0x683C) and // 5 or #chr2 > 10) or + + // 동적 함수 호출 + 인코딩 + (any of ($dyn*) and any of ($enc*)) or + + // Base64 인코딩된 위험 함수 + any of ($b64*) +} + +// ============================================ +// 유명 웹쉘 시그니처 +// ============================================ + +rule Webshell_C99 { + meta: + description = "C99 웹쉘" + severity = "critical" + reference = "https://github.com/tennc/webshell" + + strings: + $s1 = "c99shell" ascii nocase + $s2 = "c99_buff_prepare" ascii + $s3 = "c99_sess_put" ascii + $s4 = "c99ftpbrutecheck" ascii + $s5 = "c99fsearch" ascii + + condition: + 2 of them +} + +rule Webshell_R57 { + meta: + description = "R57 웹쉘" + severity = "critical" + + strings: + $s1 = "r57shell" ascii nocase + $s2 = "r57_pwd_hash" ascii + $s3 = "r57" ascii nocase + $s4 = "Safe_Mode Bypass" ascii + $s5 = "Dumper" ascii + + condition: + 3 of them +} + +rule Webshell_WSO { + meta: + description = "WSO (Web Shell by oRb) 웹쉘" + severity = "critical" + + strings: + $s1 = "WSO" ascii + $s2 = "Web Shell by oRb" ascii nocase + $s3 = "wso_version" ascii + $s4 = "FilesMan" ascii + + condition: + 2 of them +} + +rule Webshell_B374k { + meta: + description = "B374k 웹쉘" + severity = "critical" + + strings: + $s1 = "b374k" ascii nocase + $s2 = "b374k_config" ascii + $s3 = "b374k 2.8" ascii + + condition: + any of them +} + +rule Webshell_Weevely { + meta: + description = "Weevely 웹쉘 (백도어 생성기)" + severity = "critical" + + strings: + $s1 = /\$\w=\$\w\(\'\',\$\w\(\$\w\(\$\w/ ascii + $s2 = "str_replace" ascii + $s3 = "base64" ascii + + condition: + $s1 and $s2 and $s3 and filesize < 5KB +} + +// ============================================ +// 파일 업로드 취약점 악용 +// ============================================ + +rule PHP_File_Upload_Shell { + meta: + description = "파일 업로드를 통한 웹쉘" + severity = "high" + + strings: + $upload1 = "move_uploaded_file" ascii + $upload2 = "$_FILES" ascii + $upload3 = "copy(" ascii + + $write1 = "file_put_contents" ascii + $write2 = "fwrite(" ascii + $write3 = "fputs(" ascii + + $exec = /(eval|assert|system|exec|shell_exec|passthru)\s*\(/ ascii nocase + + condition: + (any of ($upload*) or any of ($write*)) and $exec +} + +// ============================================ +// 숨겨진 백도어 +// ============================================ + +rule PHP_Hidden_Backdoor { + meta: + description = "숨겨진 PHP 백도어" + severity = "critical" + + strings: + // HTTP 헤더를 통한 명령 수신 + $hdr1 = "$_SERVER['HTTP_" ascii + $hdr2 = "getallheaders()" ascii + $hdr3 = "apache_request_headers()" ascii + + // 특정 파라미터 체크 + $chk1 = /if\s*\(\s*isset\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)\s*\[\s*['"][^'"]{32,}['"]\s*\]/ ascii + $chk2 = /md5\s*\(\s*\$_(GET|POST|REQUEST)/ ascii + + // 실행 + $exec = /(eval|assert|system|exec|passthru|shell_exec)\s*\(/ ascii nocase + + condition: + (any of ($hdr*) or any of ($chk*)) and $exec +} + +// ============================================ +// ASP/ASPX 웹쉘 +// ============================================ + +rule ASPX_Webshell_Generic { + meta: + description = "ASP.NET 웹쉘" + severity = "high" + + strings: + $asp1 = "Request.Form" ascii nocase + $asp2 = "Request.QueryString" ascii nocase + $asp3 = "Request[" ascii nocase + + $exec1 = "Process.Start" ascii + $exec2 = "cmd.exe" ascii nocase + $exec3 = "powershell" ascii nocase + $exec4 = "Eval(" ascii nocase + $exec5 = "Execute(" ascii nocase + + condition: + any of ($asp*) and any of ($exec*) +} + +// ============================================ +// JSP 웹쉘 +// ============================================ + +rule JSP_Webshell_Generic { + meta: + description = "JSP 웹쉘" + severity = "high" + + strings: + $jsp1 = "request.getParameter" ascii + $jsp2 = "Runtime.getRuntime().exec" ascii + $jsp3 = "ProcessBuilder" ascii + + $cmd1 = "cmd.exe" ascii nocase + $cmd2 = "/bin/sh" ascii + $cmd3 = "/bin/bash" ascii + + condition: + $jsp1 and ($jsp2 or $jsp3) or + ($jsp2 or $jsp3) and any of ($cmd*) +} +``` + +--- + +## CLI 사용법 + +### 기본 스캔 + +```bash +# 단일 파일 스캔 +yara rules.yar target_file.php + +# 디렉토리 재귀 스캔 +yara -r rules.yar /var/www/html/ + +# 여러 규칙 파일 사용 +yara -r rules1.yar rules2.yar /var/www/html/ +``` + +### 출력 옵션 + +```bash +# 매칭된 문자열 표시 +yara -s rules.yar file.php + +# 메타데이터 표시 +yara -m rules.yar file.php + +# 태그만 표시 +yara -t webshell rules.yar file.php + +# 매칭 개수 제한 +yara -l 10 rules.yar file.php + +# 에러 무시 +yara -w rules.yar /var/www/html/ + +# 타임아웃 설정 (초) +yara -a 30 rules.yar large_file.bin +``` + +### 출력 예시 + +```bash +$ yara -s webshell_rules.yar suspicious.php + +PHP_Webshell_Generic suspicious.php +0x15:$exec1: eval( +0x25:$input2: $_POST + +PHP_Webshell_Obfuscated suspicious.php +0x35:$enc1: base64_decode( +0x50:$dyn1: $func($ +``` + +--- + +## Python 연동 + +### 기본 사용 + +```python +#!/usr/bin/env python3 +"""YARA Python 바인딩 사용 예시""" + +import yara +import os +from pathlib import Path + +class YaraScanner: + def __init__(self, rules_path): + """YARA 규칙 로드""" + if os.path.isdir(rules_path): + # 디렉토리의 모든 .yar 파일 컴파일 + rule_files = {} + for f in Path(rules_path).glob("*.yar"): + rule_files[f.stem] = str(f) + self.rules = yara.compile(filepaths=rule_files) + else: + # 단일 파일 + self.rules = yara.compile(filepath=rules_path) + + def scan_file(self, filepath): + """파일 스캔""" + try: + matches = self.rules.match(filepath) + + if matches: + return { + "file": filepath, + "malicious": True, + "matches": [ + { + "rule": m.rule, + "tags": m.tags, + "meta": m.meta, + "strings": [ + { + "offset": s[0], + "identifier": s[1], + "data": s[2].decode("utf-8", errors="replace")[:100] + } + for s in m.strings + ] + } + for m in matches + ] + } + + return {"file": filepath, "malicious": False} + + except yara.Error as e: + return {"file": filepath, "error": str(e)} + + def scan_data(self, data, filename="memory"): + """메모리 데이터 스캔""" + if isinstance(data, str): + data = data.encode() + + matches = self.rules.match(data=data) + + if matches: + return { + "source": filename, + "malicious": True, + "rules": [m.rule for m in matches] + } + + return {"source": filename, "malicious": False} + + def scan_directory(self, directory, extensions=None): + """디렉토리 스캔""" + if extensions is None: + extensions = {".php", ".phtml", ".inc", ".asp", ".aspx", ".jsp"} + + findings = [] + scanned = 0 + + for root, dirs, files in os.walk(directory): + # 숨김 디렉토리 제외 + dirs[:] = [d for d in dirs if not d.startswith(".")] + + for filename in files: + ext = os.path.splitext(filename)[1].lower() + if ext in extensions: + filepath = os.path.join(root, filename) + result = self.scan_file(filepath) + scanned += 1 + + if result.get("malicious"): + findings.append(result) + print(f"🚨 탐지: {filepath}") + for m in result["matches"]: + print(f" 규칙: {m['rule']}") + + return { + "scanned": scanned, + "findings": findings + } + + +# 사용 예시 +if __name__ == "__main__": + scanner = YaraScanner("webshell_rules.yar") + + # 파일 스캔 + result = scanner.scan_file("/var/www/html/upload/shell.php") + print(result) + + # 문자열 스캔 + code = '' + result = scanner.scan_data(code) + print(result) + + # 디렉토리 스캔 + results = scanner.scan_directory("/var/www/html") + print(f"스캔 완료: {results['scanned']}개 파일, {len(results['findings'])}개 위협") +``` + +### 콜백 사용 (대용량 스캔) + +```python +"""콜백을 사용한 효율적인 스캔""" + +import yara + +def match_callback(data): + """매치 발견 시 콜백""" + print(f"규칙: {data['rule']}") + print(f"태그: {data['tags']}") + print(f"메타: {data['meta']}") + + # CALLBACK_CONTINUE: 계속 스캔 + # CALLBACK_ABORT: 스캔 중단 + return yara.CALLBACK_CONTINUE + +rules = yara.compile(filepath="rules.yar") + +# 콜백과 함께 스캔 +matches = rules.match( + "/path/to/file", + callback=match_callback, + which_callbacks=yara.CALLBACK_MATCHES +) +``` + +--- + +## 실시간 모니터링 + +### watchdog + YARA + +```python +#!/usr/bin/env python3 +"""파일 시스템 실시간 모니터링 + YARA 스캔""" + +import yara +import os +import shutil +import logging +from datetime import datetime +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler + +# 로깅 설정 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +class WebshellMonitor(FileSystemEventHandler): + def __init__(self, rules_path, quarantine_dir="/var/quarantine"): + self.rules = yara.compile(filepath=rules_path) + self.quarantine_dir = quarantine_dir + self.extensions = {".php", ".phtml", ".inc", ".asp", ".aspx", ".jsp"} + + os.makedirs(quarantine_dir, exist_ok=True) + logger.info(f"YARA 규칙 로드 완료") + + def should_scan(self, filepath): + """스캔 대상 여부 확인""" + ext = os.path.splitext(filepath)[1].lower() + return ext in self.extensions + + def on_created(self, event): + """파일 생성 시""" + if event.is_directory: + return + if self.should_scan(event.src_path): + self.scan_file(event.src_path, "created") + + def on_modified(self, event): + """파일 수정 시""" + if event.is_directory: + return + if self.should_scan(event.src_path): + self.scan_file(event.src_path, "modified") + + def on_moved(self, event): + """파일 이동 시""" + if event.is_directory: + return + if self.should_scan(event.dest_path): + self.scan_file(event.dest_path, "moved") + + def scan_file(self, filepath, event_type): + """파일 스캔""" + try: + if not os.path.exists(filepath): + return + + matches = self.rules.match(filepath) + + if matches: + rules = [m.rule for m in matches] + logger.warning(f"🚨 웹쉘 탐지 [{event_type}]: {filepath}") + logger.warning(f" 매칭 규칙: {rules}") + + self.quarantine_file(filepath, rules) + self.send_alert(filepath, rules, event_type) + else: + logger.debug(f"✅ 정상: {filepath}") + + except Exception as e: + logger.error(f"스캔 오류 ({filepath}): {e}") + + def quarantine_file(self, filepath, rules): + """파일 격리""" + try: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + basename = os.path.basename(filepath) + quarantine_name = f"{timestamp}_{basename}" + quarantine_path = os.path.join(self.quarantine_dir, quarantine_name) + + # 메타데이터 저장 + meta_path = quarantine_path + ".meta" + with open(meta_path, "w") as f: + f.write(f"원본 경로: {filepath}\n") + f.write(f"탐지 시간: {datetime.now()}\n") + f.write(f"매칭 규칙: {rules}\n") + + # 파일 이동 + shutil.move(filepath, quarantine_path) + logger.info(f" 격리 완료: {quarantine_path}") + + except Exception as e: + logger.error(f"격리 실패: {e}") + + def send_alert(self, filepath, rules, event_type): + """알림 전송 (구현 필요)""" + # Slack, Telegram, Email 등으로 알림 + pass + + +def main(): + import argparse + + parser = argparse.ArgumentParser(description="YARA 기반 웹쉘 모니터링") + parser.add_argument("-r", "--rules", default="webshell_rules.yar", help="YARA 규칙 파일") + parser.add_argument("-d", "--directory", default="/var/www/html", help="모니터링 디렉토리") + parser.add_argument("-q", "--quarantine", default="/var/quarantine", help="격리 디렉토리") + args = parser.parse_args() + + event_handler = WebshellMonitor(args.rules, args.quarantine) + observer = Observer() + observer.schedule(event_handler, args.directory, recursive=True) + observer.start() + + logger.info(f"🔍 모니터링 시작: {args.directory}") + logger.info(f" 규칙: {args.rules}") + logger.info(f" 격리 디렉토리: {args.quarantine}") + + try: + while True: + import time + time.sleep(1) + except KeyboardInterrupt: + observer.stop() + logger.info("모니터링 종료") + + observer.join() + + +if __name__ == "__main__": + main() +``` + +### systemd 서비스로 등록 + +```ini +# /etc/systemd/system/yara-monitor.service + +[Unit] +Description=YARA Webshell Monitor +After=network.target + +[Service] +Type=simple +User=root +ExecStart=/usr/bin/python3 /opt/security/yara_monitor.py \ + --rules /opt/security/rules/webshell_rules.yar \ + --directory /var/www/html \ + --quarantine /var/quarantine +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +```bash +# 서비스 등록 및 시작 +sudo systemctl daemon-reload +sudo systemctl enable yara-monitor +sudo systemctl start yara-monitor +sudo systemctl status yara-monitor +``` + +--- + +## 공개 규칙 모음 + +### 주요 소스 + +| 저장소 | 규칙 수 | 주요 내용 | +|--------|---------|-----------| +| [Neo23x0/signature-base](https://github.com/Neo23x0/signature-base) | 3,000+ | 웹쉘, APT, 악성코드 | +| [Yara-Rules/rules](https://github.com/Yara-Rules/rules) | 2,000+ | 범용 | +| [InQuest/yara-rules](https://github.com/InQuest/yara-rules) | 500+ | 문서 악성코드 | +| [bartblaze/Yara-rules](https://github.com/bartblaze/Yara-rules) | 300+ | 랜섬웨어 | +| [kevthehermit/YaraRules](https://github.com/kevthehermit/YaraRules) | 200+ | 악성코드 | +| [cuckoosandbox/community](https://github.com/cuckoosandbox/community) | 500+ | 샌드박스 분석용 | + +### 규칙 다운로드 + +```bash +# 디렉토리 생성 +mkdir -p /opt/yara-rules +cd /opt/yara-rules + +# signature-base (웹쉘 전문) +git clone https://github.com/Neo23x0/signature-base.git + +# Yara-Rules (범용) +git clone https://github.com/Yara-Rules/rules.git yara-rules + +# 규칙 확인 +ls signature-base/yara/ +# apt_*.yar - APT 공격 +# crime_*.yar - 사이버 범죄 +# gen_*.yar - 일반 악성코드 +# thor_webshells.yar - 웹쉘 (추천) +``` + +### 규칙 통합 사용 + +```python +"""여러 규칙 소스 통합""" + +import yara +from pathlib import Path + +def compile_all_rules(rules_dirs): + """모든 규칙 디렉토리 통합 컴파일""" + rule_files = {} + + for rules_dir in rules_dirs: + for yar_file in Path(rules_dir).rglob("*.yar"): + # 파일명 중복 방지 + key = f"{rules_dir.name}_{yar_file.stem}" + rule_files[key] = str(yar_file) + + print(f"총 {len(rule_files)}개 규칙 파일 로드") + + return yara.compile(filepaths=rule_files) + +# 사용 +rules = compile_all_rules([ + Path("/opt/yara-rules/signature-base/yara"), + Path("/opt/yara-rules/yara-rules/malware"), +]) +``` + +--- + +## 다른 방식과 비교 + +### 탐지 방식 비교표 + +| 방식 | 원리 | 속도 | 알려진 위협 | 변형 | 제로데이 | AI | +|------|------|------|-------------|------|---------|-----| +| 해시 비교 | 파일 지문 | 0.01ms | ✓✓✓ | ✗ | ✗ | ❌ | +| YARA | 패턴 매칭 | 1-10ms | ✓✓ | ✓ | △ | ❌ | +| 정규식 | 텍스트 매칭 | 1-5ms | ✓ | △ | ✗ | ❌ | +| ML 분류기 | 학습된 모델 | 10-50ms | ✓✓ | ✓✓ | ✓ | ⚠️ | +| LLM | 코드 이해 | 100ms+ | ✓✓ | ✓✓✓ | ✓✓ | ✓ | + +### 각 방식의 장단점 + +**해시 비교** +``` +장점: 오탐 0%, 가장 빠름 +단점: 1비트만 달라도 탐지 못함 +용도: 알려진 악성코드 즉시 차단 +``` + +**YARA** +``` +장점: 유연한 패턴, 커뮤니티 활성화, 업계 표준 +단점: 규칙 작성 필요, 고급 난독화 취약 +용도: 알려진 패턴 + 변형 탐지 +``` + +**AI/ML** +``` +장점: 새로운 위협 탐지, 난독화 강함 +단점: 오탐 가능, 상대적으로 느림 +용도: 제로데이, 고도화된 공격 +``` + +### 권장 조합 + +``` +Layer 1: 해시 비교 (0.01ms) + │ 알려진 악성코드 즉시 차단 + │ 오탐: 0% + ▼ +Layer 2: YARA (1-10ms) + │ 패턴 기반 탐지 + │ 오탐: 매우 낮음 + ▼ +Layer 3: AI 모델 (50-100ms) + │ 난독화/제로데이 탐지 + │ 오탐: 낮음 (검토 필요) + ▼ +[최종 판정] +``` + +--- + +## 참고 자료 + +- [YARA 공식 문서](https://yara.readthedocs.io/) +- [YARA GitHub](https://github.com/VirusTotal/yara) +- [YARA 규칙 작성 가이드](https://yara.readthedocs.io/en/stable/writingrules.html) +- [Neo23x0 Signature Base](https://github.com/Neo23x0/signature-base) +- [VirusTotal YARA](https://www.virustotal.com/gui/hunting/yaradetect) diff --git a/docs/waf-integration.md b/docs/waf-integration.md new file mode 100644 index 0000000..7b2571b --- /dev/null +++ b/docs/waf-integration.md @@ -0,0 +1,1125 @@ +# HAProxy WAF 통합 가이드 + +ModSecurity와 CrowdSec을 HAProxy와 연동하여 웹 애플리케이션 방화벽(WAF)을 구성하는 방법을 설명합니다. + +## 목차 + +1. [개요](#개요) +2. [아키텍처](#아키텍처) +3. [ModSecurity 구성](#modsecurity-구성) +4. [CrowdSec 구성](#crowdsec-구성) +5. [HAProxy 연동](#haproxy-연동) +6. [별도 서버 구성](#별도-서버-구성) +7. [테스트 방법](#테스트-방법) +8. [운영 명령어](#운영-명령어) + +--- + +## 개요 + +### ModSecurity vs CrowdSec + +| 구분 | ModSecurity | CrowdSec | +|------|-------------|----------| +| **역할** | WAF (웹 애플리케이션 방화벽) | 침입 탐지/차단 시스템 (IDS/IPS) | +| **분석 대상** | HTTP 요청/응답 내용 | IP 기반 행위 패턴 | +| **탐지 방식** | 시그니처 기반 (OWASP CRS) | 행위 기반 + 커뮤니티 위협 인텔리전스 | +| **차단 범위** | 개별 악성 요청 | IP 단위 (반복 공격자) | +| **주요 탐지** | SQL Injection, XSS, RCE, LFI/RFI | 브루트포스, 스캐닝, DDoS, 알려진 악성 IP | +| **성능 영향** | 높음 (모든 요청 검사) | 낮음 (IP 조회만) | + +### 권장 조합 + +``` +CrowdSec (먼저) → ModSecurity (나중) +``` + +- **CrowdSec**: 알려진 악성 IP를 빠르게 차단 (낮은 지연) +- **ModSecurity**: 나머지 요청에 대해 상세 분석 (깊은 검사) + +--- + +## 아키텍처 + +### 동일 서버 구성 (권장) + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Host Server │ +│ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ HAProxy (host network) │ │ +│ │ TCP 80/443 + UDP 443 │ │ +│ │ │ │ +│ │ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ SPOE Agent │ │ SPOE Agent │ │ │ +│ │ │ (CrowdSec) │ │(ModSecurity)│ │ │ +│ │ └──────┬──────┘ └──────┬──────┘ │ │ +│ └───────────┼───────────────────┼──────────────────────────────────┘ │ +│ │ │ │ +│ │ TCP :8888 │ TCP :12345 │ +│ ▼ ▼ │ +│ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ +│ │ CrowdSec Bouncer │ │ ModSecurity SPOA │ │ CrowdSec Engine │ │ +│ │ (haproxy bouncer)│ │ (modsecurity-spoa)│ │ (crowdsec) │ │ +│ │ Podman Container │ │ Podman Container │ │ Podman Container │ │ +│ │ │ │ │ │ │ │ +│ │ - API Key 검증 │ │ - OWASP CRS 3.x │ │ - 로그 분석 │ │ +│ │ - LAPI 연동 │ │ - 요청 검사 │ │ - 시나리오 탐지 │ │ +│ └─────────┬─────────┘ └───────────────────┘ │ - 알림 발송 │ │ +│ │ └─────────┬─────────┘ │ +│ │ TCP :8080 │ │ +│ └──────────────────────────────────────────────┘ │ +│ (LAPI 연동) │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 별도 서버 구성 + +``` +┌──────────────────────┐ ┌──────────────────────┐ +│ HAProxy Server │ │ Security Server │ +│ │ │ │ +│ ┌────────────────┐ │ │ ┌────────────────┐ │ +│ │ HAProxy │ │ TCP │ │ CrowdSec │ │ +│ │ (host network)│──┼──:8888───┼─▶│ Bouncer │ │ +│ │ │ │ │ └───────┬────────┘ │ +│ │ │──┼─:12345───┼─▶│ │ │ +│ └────────────────┘ │ │ │ ModSecurity │ │ +│ │ │ │ SPOA │ │ +│ │ │ └────────────────┘ │ +│ │ │ │ +│ │ │ ┌────────────────┐ │ +│ │ │ │ CrowdSec │ │ +│ │ │ │ Engine │ │ +│ │ │ └────────────────┘ │ +└──────────────────────┘ └──────────────────────┘ +``` + +--- + +## ModSecurity 구성 + +### 디렉토리 구조 + +```bash +mkdir -p /opt/haproxy/modsecurity/{conf,rules,logs} +``` + +``` +/opt/haproxy/modsecurity/ +├── conf/ +│ ├── modsecurity.conf # ModSecurity 메인 설정 +│ └── crs-setup.conf # OWASP CRS 설정 +├── rules/ +│ └── (OWASP CRS 룰셋) +├── logs/ +│ └── modsec_audit.log +└── Dockerfile +``` + +### Dockerfile + +```dockerfile +# /opt/haproxy/modsecurity/Dockerfile +FROM alpine:3.19 + +# 빌드 의존성 설치 +RUN apk add --no-cache \ + git \ + build-base \ + autoconf \ + automake \ + libtool \ + pcre2-dev \ + yajl-dev \ + curl-dev \ + libxml2-dev \ + lmdb-dev \ + lua5.4-dev \ + geoip-dev \ + libmaxminddb-dev \ + linux-headers \ + haproxy + +# ModSecurity v3 빌드 +WORKDIR /build +RUN git clone --depth 1 https://github.com/owasp-modsecurity/ModSecurity.git && \ + cd ModSecurity && \ + git submodule init && \ + git submodule update && \ + ./build.sh && \ + ./configure --with-pcre2 --with-lmdb && \ + make -j$(nproc) && \ + make install + +# SPOA 에이전트 빌드 +RUN git clone --depth 1 https://github.com/haproxy/spoa-modsecurity.git && \ + cd spoa-modsecurity && \ + make MODSEC_INC=/usr/local/modsecurity/include \ + MODSEC_LIB=/usr/local/modsecurity/lib \ + APACHE2_INC=/usr/include/apache2 + +# OWASP CRS 다운로드 +RUN git clone --depth 1 https://github.com/coreruleset/coreruleset.git /opt/owasp-crs && \ + cp /opt/owasp-crs/crs-setup.conf.example /opt/owasp-crs/crs-setup.conf + +# 런타임 이미지 +FROM alpine:3.19 + +RUN apk add --no-cache \ + pcre2 \ + yajl \ + libcurl \ + libxml2 \ + lmdb \ + lua5.4-libs \ + geoip \ + libmaxminddb + +# 빌드 결과 복사 +COPY --from=0 /usr/local/modsecurity /usr/local/modsecurity +COPY --from=0 /build/spoa-modsecurity/modsecurity /usr/local/bin/modsecurity-spoa +COPY --from=0 /opt/owasp-crs /opt/owasp-crs + +# 라이브러리 경로 설정 +ENV LD_LIBRARY_PATH=/usr/local/modsecurity/lib + +# 설정 디렉토리 +VOLUME ["/etc/modsecurity", "/var/log/modsecurity"] + +# SPOA 포트 +EXPOSE 12345 + +# 실행 +ENTRYPOINT ["/usr/local/bin/modsecurity-spoa"] +CMD ["-f", "/etc/modsecurity/modsecurity.conf", "-a", "0.0.0.0", "-p", "12345", "-n", "4"] +``` + +### ModSecurity 설정 파일 + +```apache +# /opt/haproxy/modsecurity/conf/modsecurity.conf + +# 기본 설정 +SecRuleEngine On +SecRequestBodyAccess On +SecResponseBodyAccess Off + +# 요청 본문 설정 +SecRequestBodyLimit 13107200 +SecRequestBodyNoFilesLimit 131072 +SecRequestBodyLimitAction Reject + +# 임시 파일 경로 +SecTmpDir /tmp/ +SecDataDir /tmp/ + +# 로깅 설정 +SecAuditEngine RelevantOnly +SecAuditLogRelevantStatus "^(?:5|4(?!04))" +SecAuditLogParts ABIJDEFHZ +SecAuditLogType Serial +SecAuditLog /var/log/modsecurity/modsec_audit.log + +# 디버그 로그 (문제 해결시 활성화) +# SecDebugLog /var/log/modsecurity/debug.log +# SecDebugLogLevel 3 + +# 응답 상태 +SecStatusEngine On + +# 유니코드 매핑 +SecUnicodeMapFile unicode.mapping 20127 + +# PCRE 설정 +SecPcreMatchLimit 100000 +SecPcreMatchLimitRecursion 100000 + +# OWASP CRS 포함 +Include /opt/owasp-crs/crs-setup.conf +Include /opt/owasp-crs/rules/*.conf +``` + +### OWASP CRS 설정 + +```apache +# /opt/haproxy/modsecurity/conf/crs-setup.conf + +# Paranoia Level (1-4, 높을수록 엄격) +SecAction \ + "id:900000,\ + phase:1,\ + pass,\ + t:none,\ + nolog,\ + setvar:tx.blocking_paranoia_level=1" + +# 탐지 Paranoia Level +SecAction \ + "id:900001,\ + phase:1,\ + pass,\ + t:none,\ + nolog,\ + setvar:tx.detection_paranoia_level=1" + +# 차단 임계값 설정 +SecAction \ + "id:900110,\ + phase:1,\ + pass,\ + t:none,\ + nolog,\ + setvar:tx.inbound_anomaly_score_threshold=5,\ + setvar:tx.outbound_anomaly_score_threshold=4" + +# 특정 규칙 제외 (오탐 방지) +# SecRuleRemoveById 920350 +# SecRuleRemoveById 942100 + +# 특정 경로 제외 +SecRule REQUEST_URI "@beginsWith /api/upload" \ + "id:1001,\ + phase:1,\ + pass,\ + t:none,\ + nolog,\ + ctl:ruleRemoveById=920420" + +# 허용 IP (내부망) +SecRule REMOTE_ADDR "@ipMatch 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,100.64.0.0/10" \ + "id:1002,\ + phase:1,\ + pass,\ + t:none,\ + nolog,\ + ctl:ruleEngine=DetectionOnly" +``` + +### ModSecurity Quadlet + +```ini +# /etc/containers/systemd/modsecurity.container + +[Unit] +Description=ModSecurity SPOA Agent +After=network-online.target +Wants=network-online.target + +[Container] +Image=localhost/modsecurity-spoa:latest +ContainerName=modsecurity +AutoUpdate=local + +# 네트워크 설정 +PublishPort=12345:12345 + +# 볼륨 마운트 +Volume=/opt/haproxy/modsecurity/conf:/etc/modsecurity:ro,Z +Volume=/opt/haproxy/modsecurity/logs:/var/log/modsecurity:Z + +# 환경 변수 +Environment=LD_LIBRARY_PATH=/usr/local/modsecurity/lib + +# 리소스 제한 +Memory=512M +CPUQuota=100% + +[Service] +Restart=always +RestartSec=5 +TimeoutStartSec=60 + +[Install] +WantedBy=multi-user.target +``` + +### ModSecurity 이미지 빌드 + +```bash +# 이미지 빌드 +cd /opt/haproxy/modsecurity +podman build -t localhost/modsecurity-spoa:latest . + +# Quadlet 활성화 +systemctl daemon-reload +systemctl start modsecurity +systemctl enable modsecurity + +# 상태 확인 +systemctl status modsecurity +podman logs -f modsecurity +``` + +--- + +## CrowdSec 구성 + +### 디렉토리 구조 + +```bash +mkdir -p /opt/haproxy/crowdsec/{config,data,hub} +mkdir -p /opt/haproxy/crowdsec-bouncer +``` + +``` +/opt/haproxy/crowdsec/ +├── config/ +│ ├── config.yaml # CrowdSec 메인 설정 +│ ├── acquis.yaml # 로그 수집 설정 +│ └── local_api_credentials.yaml +├── data/ +│ └── (데이터베이스) +└── hub/ + └── (시나리오, 파서) + +/opt/haproxy/crowdsec-bouncer/ +└── config.yaml # HAProxy Bouncer 설정 +``` + +### CrowdSec Engine Quadlet + +```ini +# /etc/containers/systemd/crowdsec.container + +[Unit] +Description=CrowdSec Security Engine +After=network-online.target +Wants=network-online.target + +[Container] +Image=docker.io/crowdsecurity/crowdsec:latest +ContainerName=crowdsec +AutoUpdate=registry + +# 네트워크 설정 +PublishPort=8080:8080 +PublishPort=6060:6060 + +# 볼륨 마운트 +Volume=/opt/haproxy/crowdsec/config:/etc/crowdsec:Z +Volume=/opt/haproxy/crowdsec/data:/var/lib/crowdsec/data:Z +Volume=/opt/haproxy/crowdsec/hub:/var/lib/crowdsec/hub:Z + +# HAProxy 로그 접근 (호스트 로그) +Volume=/var/log:/var/log:ro + +# 환경 변수 +Environment=COLLECTIONS="crowdsecurity/haproxy crowdsecurity/http-cve crowdsecurity/linux" +Environment=GID=1000 + +[Service] +Restart=always +RestartSec=10 +TimeoutStartSec=120 + +[Install] +WantedBy=multi-user.target +``` + +### CrowdSec 설정 파일 + +```yaml +# /opt/haproxy/crowdsec/config/config.yaml + +common: + daemonize: false + log_media: stdout + log_level: info + log_dir: /var/log/ + working_dir: /var/lib/crowdsec/data/ + +config_paths: + config_dir: /etc/crowdsec/ + data_dir: /var/lib/crowdsec/data/ + simulation_path: /etc/crowdsec/simulation.yaml + hub_dir: /var/lib/crowdsec/hub/ + index_path: /var/lib/crowdsec/hub/.index.json + notification_dir: /etc/crowdsec/notifications/ + plugin_dir: /usr/local/lib/crowdsec/plugins/ + +crowdsec_service: + acquisition_path: /etc/crowdsec/acquis.yaml + parser_routines: 1 + +cscli: + output: human + +db_config: + log_level: info + type: sqlite + db_path: /var/lib/crowdsec/data/crowdsec.db + +api: + client: + insecure_skip_verify: false + credentials_path: /etc/crowdsec/local_api_credentials.yaml + server: + log_level: info + listen_uri: 0.0.0.0:8080 + profiles_path: /etc/crowdsec/profiles.yaml + online_client: + credentials_path: /etc/crowdsec/online_api_credentials.yaml + +prometheus: + enabled: true + level: full + listen_addr: 0.0.0.0 + listen_port: 6060 +``` + +### 로그 수집 설정 + +```yaml +# /opt/haproxy/crowdsec/config/acquis.yaml + +# HAProxy 로그 (호스트에서 rsyslog/journald로 수집) +filenames: + - /var/log/haproxy.log +labels: + type: haproxy +--- +# syslog에서 HAProxy 로그 수집 +filenames: + - /var/log/syslog + - /var/log/messages +labels: + type: syslog +--- +# HAProxy 컨테이너 로그 (journald) +source: journalctl +journalctl_filter: + - "_SYSTEMD_UNIT=haproxy.service" +labels: + type: haproxy +``` + +### HAProxy Bouncer Quadlet + +```ini +# /etc/containers/systemd/crowdsec-bouncer.container + +[Unit] +Description=CrowdSec HAProxy Bouncer +After=crowdsec.service +Requires=crowdsec.service + +[Container] +Image=docker.io/crowdsecurity/crowdsec-haproxy-bouncer:latest +ContainerName=crowdsec-bouncer +AutoUpdate=registry + +# 네트워크 설정 (HAProxy가 연결할 포트) +PublishPort=8888:8888 + +# 설정 파일 +Volume=/opt/haproxy/crowdsec-bouncer:/etc/crowdsec:Z + +# 환경 변수 (LAPI 연결 정보) +Environment=CROWDSEC_LAPI_URL=http://host.containers.internal:8080 +Environment=CROWDSEC_LAPI_KEY= + +[Service] +Restart=always +RestartSec=5 +TimeoutStartSec=30 +# ExecStartPre에서 API 키 설정 +ExecStartPre=/bin/sh -c 'podman exec crowdsec cscli bouncers add haproxy-bouncer -o raw > /opt/haproxy/crowdsec-bouncer/api_key.txt 2>/dev/null || true' + +[Install] +WantedBy=multi-user.target +``` + +### Bouncer 설정 파일 + +```yaml +# /opt/haproxy/crowdsec-bouncer/config.yaml + +# SPOE 서버 설정 +listen_addr: 0.0.0.0:8888 + +# CrowdSec LAPI 연결 +crowdsec_lapi_url: http://host.containers.internal:8080 +crowdsec_lapi_key: "${API_KEY}" # 자동 생성됨 + +# 캐시 설정 (성능 최적화) +cache: + enabled: true + ttl: 60s + size: 10000 + +# 로깅 +log_level: info +log_mode: stdout + +# 차단 응답 +ban_response_code: 403 +ban_response_msg: "Access Denied by CrowdSec" + +# Captcha (선택) +captcha: + enabled: false +``` + +### CrowdSec 초기 설정 + +```bash +# CrowdSec 시작 +systemctl daemon-reload +systemctl start crowdsec +systemctl enable crowdsec + +# 컬렉션 설치 확인 +podman exec crowdsec cscli collections list + +# HAProxy 컬렉션 설치 (필요시) +podman exec crowdsec cscli collections install crowdsecurity/haproxy +podman exec crowdsec cscli collections install crowdsecurity/http-cve + +# Bouncer API 키 생성 +podman exec crowdsec cscli bouncers add haproxy-bouncer -o raw + +# 생성된 키를 bouncer 설정에 추가 +# /opt/haproxy/crowdsec-bouncer/config.yaml의 crowdsec_lapi_key에 설정 + +# Bouncer 시작 +systemctl start crowdsec-bouncer +systemctl enable crowdsec-bouncer + +# 연결 확인 +podman exec crowdsec cscli bouncers list +``` + +--- + +## HAProxy 연동 + +### SPOE 설정 파일 + +#### CrowdSec SPOE + +``` +# /opt/haproxy/conf/crowdsec-spoe.conf + +[crowdsec] +spoe-agent crowdsec-agent + messages check-client-ip + option var-prefix crowdsec + timeout hello 2s + timeout idle 2m + timeout processing 10ms + use-backend crowdsec-backend + +spoe-message check-client-ip + args src=src dst=dst method=method path=path query=query version=req.ver headers=req.hdrs body=req.body + event on-frontend-http-request +``` + +#### ModSecurity SPOE + +``` +# /opt/haproxy/conf/modsecurity-spoe.conf + +[modsecurity] +spoe-agent modsecurity-agent + messages check-request + option var-prefix modsec + timeout hello 100ms + timeout idle 30s + timeout processing 1s + use-backend modsecurity-backend + +spoe-message check-request + args unique-id=unique-id src-ip=src dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body + event on-frontend-http-request +``` + +### HAProxy 설정 수정 + +아래 내용을 `/opt/haproxy/conf/haproxy.cfg`에 추가합니다. + +#### 방법 1: CrowdSec만 사용 + +```haproxy +# haproxy.cfg에 추가 (global 섹션 아래) + +# ============================================================================= +# CrowdSec 연동 (IP 기반 차단) +# ============================================================================= + +# CrowdSec Bouncer 백엔드 +backend crowdsec-backend + mode tcp + server crowdsec 127.0.0.1:8888 check + +# HTTPS Frontend에 SPOE 필터 추가 +frontend https_front + # ... 기존 설정 유지 ... + + # CrowdSec SPOE 필터 + filter spoe engine crowdsec config /usr/local/etc/haproxy/crowdsec-spoe.conf + + # CrowdSec 차단 (crowdsec.action이 설정되면 차단) + http-request deny deny_status 403 if { var(sess.crowdsec.action) -m str ban } + http-request deny deny_status 403 if { var(sess.crowdsec.action) -m str captcha } +``` + +#### 방법 2: ModSecurity만 사용 + +```haproxy +# haproxy.cfg에 추가 (global 섹션 아래) + +# ============================================================================= +# ModSecurity 연동 (WAF) +# ============================================================================= + +# ModSecurity SPOA 백엔드 +backend modsecurity-backend + mode tcp + server modsec 127.0.0.1:12345 check + +# HTTPS Frontend에 SPOE 필터 추가 +frontend https_front + # ... 기존 설정 유지 ... + + # 유니크 ID 생성 (ModSecurity 트랜잭션 추적용) + unique-id-format %{+X}o\ %ci:%cp_%fi:%fp_%Ts_%rt:%pid + unique-id-header X-Unique-ID + + # ModSecurity SPOE 필터 + filter spoe engine modsecurity config /usr/local/etc/haproxy/modsecurity-spoe.conf + + # ModSecurity 차단 (modsec.code가 0이 아니면 차단) + http-request deny deny_status 403 if { var(txn.modsec.code) -m int gt 0 } +``` + +#### 방법 3: CrowdSec + ModSecurity 함께 사용 (권장) + +```haproxy +# /opt/haproxy/conf/haproxy.cfg + +global + # ... 기존 global 설정 유지 ... + log stdout format raw local0 + +defaults + # ... 기존 defaults 설정 유지 ... + +# ============================================================================= +# WAF 백엔드 (CrowdSec + ModSecurity) +# ============================================================================= + +# CrowdSec Bouncer 백엔드 +backend crowdsec-backend + mode tcp + server crowdsec 127.0.0.1:8888 check + +# ModSecurity SPOA 백엔드 +backend modsecurity-backend + mode tcp + server modsec 127.0.0.1:12345 check + +# ============================================================================= +# HTTPS Frontend (WAF 통합) +# ============================================================================= + +frontend https_front + bind *:443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1 + bind quic4@:443 ssl crt /etc/haproxy/certs/ alpn h3 + http-response set-header alt-svc "h3=\":443\"; ma=86400" + + # 유니크 ID 생성 + unique-id-format %{+X}o\ %ci:%cp_%fi:%fp_%Ts_%rt:%pid + unique-id-header X-Unique-ID + + # ========================================================================= + # WAF 필터 (순서 중요: CrowdSec 먼저, ModSecurity 나중) + # ========================================================================= + + # 1. CrowdSec SPOE (IP 기반 빠른 차단) + filter spoe engine crowdsec config /usr/local/etc/haproxy/crowdsec-spoe.conf + + # 2. ModSecurity SPOE (상세 요청 분석) + filter spoe engine modsecurity config /usr/local/etc/haproxy/modsecurity-spoe.conf + + # ========================================================================= + # 차단 규칙 (순서 중요) + # ========================================================================= + + # CrowdSec 차단 (알려진 악성 IP) + http-request deny deny_status 403 content-type "text/plain" string "Access denied by CrowdSec" if { var(sess.crowdsec.action) -m str ban } + http-request deny deny_status 403 if { var(sess.crowdsec.action) -m str captcha } + + # ModSecurity 차단 (WAF 규칙 위반) + http-request deny deny_status 403 content-type "text/plain" string "Access denied by WAF" if { var(txn.modsec.code) -m int gt 0 } + + # ========================================================================= + # 내부 IP 우회 (선택사항) + # ========================================================================= + + # Tailscale IP는 WAF 우회 + acl is_tailscale src 100.64.0.0/10 + # http-request set-var(txn.skip_waf) bool(true) if is_tailscale + + # ========================================================================= + # 기존 라우팅 설정 (변경 없음) + # ========================================================================= + + # MCP 인증 + acl is_mcp hdr(host) -i mcp.inouter.com + acl valid_token req.hdr(Authorization) -m str "Bearer dcb7963ab3ef705f6b780818f78942a100efa3b55e3d2f99c4560b65da64c426" + http-request deny deny_status 401 if is_mcp !valid_token !is_tailscale + + # Map 기반 라우팅 + use_backend %[req.hdr(host),lower,map_dom(/usr/local/etc/haproxy/domains.map)] if { req.hdr(host),lower,map_dom(/usr/local/etc/haproxy/domains.map) -m found } + + default_backend default_backend + +# ... 나머지 backend 설정 유지 ... +``` + +### HAProxy 설정 적용 + +```bash +# SPOE 설정 파일 복사 (이미 conf 디렉토리에 있다면 생략) +# HAProxy 컨테이너에서 /usr/local/etc/haproxy/로 마운트됨 + +# 설정 검증 +podman exec haproxy haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg + +# HAProxy 리로드 (무중단) +systemctl reload haproxy + +# 또는 완전 재시작 +systemctl restart haproxy +``` + +--- + +## 별도 서버 구성 + +대규모 트래픽이나 보안 격리가 필요한 경우, CrowdSec과 ModSecurity를 별도 서버에서 운영할 수 있습니다. + +### HAProxy 서버 설정 + +```haproxy +# haproxy.cfg (HAProxy 서버) + +# CrowdSec Bouncer (원격 서버) +backend crowdsec-backend + mode tcp + server crowdsec 10.0.0.100:8888 check + +# ModSecurity SPOA (원격 서버) +backend modsecurity-backend + mode tcp + server modsec 10.0.0.100:12345 check +``` + +### 보안 서버 Quadlet + +```ini +# /etc/containers/systemd/crowdsec-bouncer.container (보안 서버) + +[Container] +# ... +# 모든 인터페이스에서 수신 +PublishPort=0.0.0.0:8888:8888 +``` + +### 방화벽 설정 + +```bash +# 보안 서버 방화벽 +firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="HAProxy서버IP" port port="8888" protocol="tcp" accept' +firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="HAProxy서버IP" port port="12345" protocol="tcp" accept' +firewall-cmd --reload +``` + +--- + +## 테스트 방법 + +### CrowdSec 테스트 + +```bash +# 1. Bouncer 연결 확인 +podman exec crowdsec cscli bouncers list + +# 2. 현재 차단 목록 확인 +podman exec crowdsec cscli decisions list + +# 3. 테스트 IP 수동 차단 +podman exec crowdsec cscli decisions add --ip 1.2.3.4 --reason "test ban" --duration 1h + +# 4. 차단 확인 (403 응답) +curl -I https://yourdomain.com --resolve yourdomain.com:443:1.2.3.4 + +# 5. 테스트 차단 해제 +podman exec crowdsec cscli decisions delete --ip 1.2.3.4 + +# 6. HAProxy SPOE 통계 확인 +echo "show stat" | nc localhost 9999 | grep crowdsec +``` + +### ModSecurity 테스트 + +```bash +# 1. SQL Injection 테스트 +curl -I "https://yourdomain.com/?id=1' OR '1'='1" +# 예상: 403 Forbidden + +# 2. XSS 테스트 +curl -I "https://yourdomain.com/?q=" +# 예상: 403 Forbidden + +# 3. Path Traversal 테스트 +curl -I "https://yourdomain.com/../../../etc/passwd" +# 예상: 403 Forbidden + +# 4. 로그 확인 +tail -f /opt/haproxy/modsecurity/logs/modsec_audit.log + +# 5. HAProxy SPOE 통계 확인 +echo "show stat" | nc localhost 9999 | grep modsec +``` + +### 통합 테스트 + +```bash +# 1. 정상 요청 테스트 +curl -I https://yourdomain.com +# 예상: 200 OK + +# 2. nikto 스캔 시뮬레이션 (CrowdSec 탐지) +for i in {1..100}; do + curl -s -o /dev/null "https://yourdomain.com/admin" & +done + +# 3. 차단 확인 +podman exec crowdsec cscli decisions list + +# 4. 메트릭 확인 +curl http://localhost:6060/metrics | grep crowdsec +``` + +--- + +## 운영 명령어 + +### CrowdSec 관리 + +```bash +# ===================== +# 차단 목록 관리 +# ===================== + +# 현재 차단 목록 조회 +podman exec crowdsec cscli decisions list + +# 특정 IP 차단 정보 조회 +podman exec crowdsec cscli decisions list --ip 1.2.3.4 + +# IP 수동 차단 +podman exec crowdsec cscli decisions add --ip 1.2.3.4 --reason "manual ban" --duration 24h + +# IP 대역 차단 +podman exec crowdsec cscli decisions add --range 1.2.3.0/24 --reason "subnet ban" --duration 24h + +# 차단 해제 +podman exec crowdsec cscli decisions delete --ip 1.2.3.4 + +# 모든 차단 해제 +podman exec crowdsec cscli decisions delete --all + +# ===================== +# 알림 및 시나리오 +# ===================== + +# 최근 알림 조회 +podman exec crowdsec cscli alerts list + +# 시나리오 목록 +podman exec crowdsec cscli scenarios list + +# 파서 목록 +podman exec crowdsec cscli parsers list + +# ===================== +# Hub 관리 (컬렉션/시나리오) +# ===================== + +# 업데이트 확인 +podman exec crowdsec cscli hub update + +# 모든 업그레이드 +podman exec crowdsec cscli hub upgrade + +# 컬렉션 설치 +podman exec crowdsec cscli collections install crowdsecurity/nginx +podman exec crowdsec cscli collections install crowdsecurity/http-cve + +# ===================== +# Bouncer 관리 +# ===================== + +# Bouncer 목록 +podman exec crowdsec cscli bouncers list + +# Bouncer 추가 +podman exec crowdsec cscli bouncers add my-bouncer + +# Bouncer 삭제 +podman exec crowdsec cscli bouncers delete my-bouncer + +# ===================== +# 메트릭 및 상태 +# ===================== + +# 엔진 상태 +podman exec crowdsec cscli metrics + +# Prometheus 메트릭 +curl http://localhost:6060/metrics +``` + +### ModSecurity 관리 + +```bash +# ===================== +# 로그 확인 +# ===================== + +# 감사 로그 실시간 확인 +tail -f /opt/haproxy/modsecurity/logs/modsec_audit.log + +# 차단된 요청만 필터링 +grep -A 20 "403" /opt/haproxy/modsecurity/logs/modsec_audit.log + +# 특정 규칙 ID로 필터링 +grep "942100" /opt/haproxy/modsecurity/logs/modsec_audit.log + +# ===================== +# 규칙 관리 +# ===================== + +# OWASP CRS 업데이트 +cd /opt/haproxy/modsecurity +podman exec modsecurity git -C /opt/owasp-crs pull + +# 규칙 비활성화 (crs-setup.conf 또는 별도 파일에 추가) +# SecRuleRemoveById 942100 + +# 특정 경로 제외 +# SecRule REQUEST_URI "@beginsWith /api/webhook" "id:1003,phase:1,pass,nolog,ctl:ruleEngine=Off" + +# ===================== +# 서비스 관리 +# ===================== + +# 컨테이너 재시작 +systemctl restart modsecurity + +# 로그 확인 +podman logs -f modsecurity +``` + +### HAProxy WAF 상태 확인 + +```bash +# SPOE 에이전트 상태 +echo "show stat" | nc localhost 9999 | grep -E "(crowdsec|modsec)" + +# 백엔드 상태 +echo "show servers state" | nc localhost 9999 | grep -E "(crowdsec|modsec)" + +# 필터 통계 +echo "show stat" | nc localhost 9999 | column -t -s ',' +``` + +### 화이트리스트 관리 + +```bash +# CrowdSec 화이트리스트 추가 +podman exec crowdsec cscli decisions add --ip 10.0.0.1 --type whitelist --duration 87600h --reason "Internal server" + +# ModSecurity 화이트리스트 (modsecurity.conf에 추가) +# SecRule REMOTE_ADDR "@ipMatch 10.0.0.1" "id:1,phase:1,pass,nolog,ctl:ruleEngine=Off" +``` + +--- + +## 문제 해결 + +### CrowdSec 연결 오류 + +```bash +# Bouncer 연결 확인 +podman exec crowdsec cscli bouncers list + +# API 키 재생성 +podman exec crowdsec cscli bouncers delete haproxy-bouncer +podman exec crowdsec cscli bouncers add haproxy-bouncer -o raw + +# Bouncer 재시작 +systemctl restart crowdsec-bouncer +``` + +### ModSecurity 오탐 처리 + +```bash +# 1. 감사 로그에서 규칙 ID 확인 +grep "id \"" /opt/haproxy/modsecurity/logs/modsec_audit.log | tail -20 + +# 2. 규칙 비활성화 (crs-setup.conf) +# SecRuleRemoveById 942100 + +# 3. 또는 특정 경로만 제외 +# SecRule REQUEST_URI "@beginsWith /api/" "id:1,phase:1,pass,nolog,ctl:ruleRemoveById=942100" + +# 4. 컨테이너 재시작 +systemctl restart modsecurity +``` + +### HAProxy SPOE 타임아웃 + +``` +# SPOE 타임아웃 조정 (haproxy.cfg) +spoe-agent crowdsec-agent + timeout processing 100ms # 기본 10ms에서 증가 +``` + +--- + +## 성능 튜닝 + +### CrowdSec Bouncer 캐시 + +```yaml +# /opt/haproxy/crowdsec-bouncer/config.yaml +cache: + enabled: true + ttl: 60s # 캐시 TTL + size: 50000 # 캐시 크기 (IP 수) +``` + +### ModSecurity 최적화 + +```apache +# modsecurity.conf +SecRequestBodyLimit 1048576 # 1MB로 제한 +SecRequestBodyNoFilesLimit 65536 # 64KB +SecPcreMatchLimit 50000 # PCRE 매칭 제한 +SecPcreMatchLimitRecursion 50000 +``` + +### HAProxy SPOE 워커 + +``` +# SPOE 설정 +spoe-agent modsecurity-agent + option async # 비동기 처리 + option pipelining # 파이프라이닝 + max-frame-size 16384 # 프레임 크기 +``` diff --git a/docs/webshell_rules.yar b/docs/webshell_rules.yar new file mode 100644 index 0000000..5cac93f --- /dev/null +++ b/docs/webshell_rules.yar @@ -0,0 +1,491 @@ +/* + * 웹쉘 탐지 YARA 규칙 + * + * 사용법: + * yara -r webshell_rules.yar /var/www/html/ + * yara -s webshell_rules.yar suspicious.php + * + * Author: Security Team + * Last Updated: 2025-02-02 + */ + +// ============================================ +// PHP 웹쉘 - 기본 패턴 +// ============================================ + +rule PHP_Webshell_Eval { + meta: + description = "eval()을 이용한 PHP 웹쉘" + severity = "high" + + strings: + $eval = /eval\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)/ nocase + + condition: + $eval +} + +rule PHP_Webshell_System { + meta: + description = "시스템 명령 실행 PHP 웹쉘" + severity = "critical" + + strings: + $func1 = "system(" nocase + $func2 = "exec(" nocase + $func3 = "shell_exec(" nocase + $func4 = "passthru(" nocase + $func5 = "popen(" nocase + $func6 = "proc_open(" nocase + + $input1 = "$_GET" + $input2 = "$_POST" + $input3 = "$_REQUEST" + + condition: + any of ($func*) and any of ($input*) +} + +rule PHP_Webshell_Assert { + meta: + description = "assert()를 이용한 PHP 웹쉘" + severity = "high" + + strings: + $assert = /assert\s*\(\s*\$_(GET|POST|REQUEST)/ nocase + + condition: + $assert +} + +rule PHP_Webshell_Preg_Replace { + meta: + description = "preg_replace /e 플래그 악용" + severity = "high" + + strings: + $preg = /preg_replace\s*\(\s*['"][^'"]*\/e['"]/ nocase + + condition: + $preg +} + +rule PHP_Webshell_Create_Function { + meta: + description = "create_function()을 이용한 웹쉘" + severity = "high" + + strings: + $cf = /create_function\s*\([^)]*\$_(GET|POST|REQUEST)/ nocase + + condition: + $cf +} + +rule PHP_Webshell_Backtick { + meta: + description = "백틱을 이용한 명령 실행" + severity = "high" + + strings: + $bt = /`[^`]*\$_(GET|POST|REQUEST)[^`]*`/ + + condition: + $bt +} + +// ============================================ +// PHP 웹쉘 - 난독화 +// ============================================ + +rule PHP_Webshell_Base64_Eval { + meta: + description = "Base64 인코딩된 eval" + severity = "critical" + + strings: + $pattern = /eval\s*\(\s*base64_decode\s*\(/ nocase + $b64_eval = "ZXZhbC" // base64("eval") + + condition: + $pattern or $b64_eval +} + +rule PHP_Webshell_Gzinflate { + meta: + description = "gzinflate 압축 난독화" + severity = "high" + + strings: + $gz1 = /eval\s*\(\s*gzinflate\s*\(/ nocase + $gz2 = /eval\s*\(\s*gzuncompress\s*\(/ nocase + $gz3 = /eval\s*\(\s*gzdecode\s*\(/ nocase + + condition: + any of them +} + +rule PHP_Webshell_Str_Rot13 { + meta: + description = "str_rot13 난독화" + severity = "medium" + + strings: + $rot = /eval\s*\(\s*str_rot13\s*\(/ nocase + + condition: + $rot +} + +rule PHP_Webshell_Chr_Obfuscation { + meta: + description = "chr() 함수를 이용한 난독화" + severity = "high" + + strings: + $chr = /chr\s*\(\s*\d+\s*\)\s*\.\s*chr\s*\(\s*\d+\s*\)/ + + condition: + #chr > 5 +} + +rule PHP_Webshell_Hex_Obfuscation { + meta: + description = "16진수 문자열 난독화" + severity = "medium" + + strings: + $hex = /\\x[0-9a-fA-F]{2}/ + + condition: + #hex > 10 +} + +rule PHP_Webshell_Variable_Function { + meta: + description = "변수를 함수로 호출" + severity = "high" + + strings: + $vf1 = /\$\w+\s*\(\s*\$_(GET|POST|REQUEST)/ + $vf2 = /\$\{\s*\$\w+\s*\}\s*\(/ + + condition: + any of them +} + +rule PHP_Webshell_Multi_Encoding { + meta: + description = "다중 인코딩 레이어" + severity = "critical" + + strings: + $enc1 = "base64_decode" nocase + $enc2 = "gzinflate" nocase + $enc3 = "gzuncompress" nocase + $enc4 = "str_rot13" nocase + $enc5 = "convert_uudecode" nocase + + condition: + 3 of them +} + +// ============================================ +// 유명 웹쉘 시그니처 +// ============================================ + +rule Webshell_C99 { + meta: + description = "C99 웹쉘" + severity = "critical" + family = "c99" + + strings: + $s1 = "c99shell" nocase + $s2 = "c99_buff_prepare" + $s3 = "c99_sess_put" + $s4 = "c99ftpbrutecheck" + $s5 = "c99fsearch" + + condition: + 2 of them +} + +rule Webshell_R57 { + meta: + description = "R57 웹쉘" + severity = "critical" + family = "r57" + + strings: + $s1 = "r57shell" nocase + $s2 = "r57_pwd_hash" + $s3 = "Safe_Mode Bypass" + $s4 = "| Encoder" + + condition: + 2 of them +} + +rule Webshell_WSO { + meta: + description = "WSO (Web Shell by oRb)" + severity = "critical" + family = "wso" + + strings: + $s1 = "Web Shell by oRb" nocase + $s2 = "wso_version" + $s3 = "FilesMan" + $s4 = "WSO" wide ascii + + condition: + 2 of them +} + +rule Webshell_B374k { + meta: + description = "B374k 웹쉘" + severity = "critical" + family = "b374k" + + strings: + $s1 = "b374k" nocase + $s2 = "b374k_config" + $s3 = "b374k 2" + + condition: + any of them +} + +rule Webshell_Weevely { + meta: + description = "Weevely 백도어" + severity = "critical" + family = "weevely" + + strings: + $pattern = /\$\w=\$\w\(\'\',\$\w\(\$\w\(\$\w/ + + condition: + $pattern and filesize < 5KB +} + +rule Webshell_China_Chopper { + meta: + description = "China Chopper 웹쉘" + severity = "critical" + family = "china_chopper" + + strings: + $cp1 = /@eval($_POST[/ nocase + $cp2 = /<%eval request\(/ nocase + $cp3 = /<%@ Page Language="Jscript"%><%eval(/ nocase + + condition: + any of them and filesize < 1KB +} + +rule Webshell_Ani_Shell { + meta: + description = "Ani-Shell 웹쉘" + severity = "critical" + + strings: + $s1 = "Ani-Shell" + $s2 = "ani_shell" + + condition: + any of them +} + +rule Webshell_PHPSpy { + meta: + description = "PHPSpy 웹쉘" + severity = "critical" + + strings: + $s1 = "phpspy" nocase + $s2 = "Php Spy" + + condition: + any of them +} + +// ============================================ +// 파일 업로드 공격 +// ============================================ + +rule PHP_File_Upload_Attack { + meta: + description = "파일 업로드를 통한 웹쉘" + severity = "high" + + strings: + $upload = "move_uploaded_file" nocase + $write1 = "file_put_contents" nocase + $write2 = "fwrite" nocase + + $exec1 = "eval(" nocase + $exec2 = "system(" nocase + $exec3 = "exec(" nocase + + condition: + ($upload or $write1 or $write2) and any of ($exec*) +} + +// ============================================ +// 숨겨진 백도어 +// ============================================ + +rule PHP_Hidden_Backdoor { + meta: + description = "HTTP 헤더를 통한 숨겨진 백도어" + severity = "critical" + + strings: + $hdr1 = /\$_SERVER\s*\[\s*['"]HTTP_/ nocase + $hdr2 = "getallheaders()" nocase + + $exec = /(eval|assert|system|exec)\s*\(/ nocase + + condition: + any of ($hdr*) and $exec +} + +rule PHP_Long_Encoded_String { + meta: + description = "긴 인코딩된 문자열 (난독화 의심)" + severity = "medium" + + strings: + $long_b64 = /['"][a-zA-Z0-9+\/=]{500,}['"]/ + + condition: + $long_b64 +} + +// ============================================ +// ASP/ASPX 웹쉘 +// ============================================ + +rule ASPX_Webshell_Generic { + meta: + description = "ASP.NET 웹쉘" + severity = "high" + + strings: + $asp1 = "Request.Form" nocase + $asp2 = "Request.QueryString" nocase + $asp3 = "Request.Item" nocase + + $exec1 = "Process.Start" nocase + $exec2 = "cmd.exe" nocase + $exec3 = "powershell" nocase + + condition: + any of ($asp*) and any of ($exec*) +} + +rule ASP_Eval { + meta: + description = "ASP Eval 웹쉘" + severity = "high" + + strings: + $eval = /Eval\s*\(\s*Request/ nocase + $exec = /Execute\s*\(\s*Request/ nocase + + condition: + $eval or $exec +} + +// ============================================ +// JSP 웹쉘 +// ============================================ + +rule JSP_Webshell_Generic { + meta: + description = "JSP 웹쉘" + severity = "high" + + strings: + $param = "request.getParameter" nocase + $exec1 = "Runtime.getRuntime().exec" nocase + $exec2 = "ProcessBuilder" nocase + + condition: + $param and ($exec1 or $exec2) +} + +rule JSP_Cmd_Shell { + meta: + description = "JSP 명령 실행 쉘" + severity = "critical" + + strings: + $rt = "Runtime.getRuntime()" + $cmd1 = "cmd.exe" nocase + $cmd2 = "/bin/sh" + $cmd3 = "/bin/bash" + + condition: + $rt and any of ($cmd*) +} + +// ============================================ +// 일반 의심 패턴 +// ============================================ + +rule Suspicious_PHP_Code { + meta: + description = "의심스러운 PHP 코드 패턴" + severity = "medium" + + strings: + $s1 = "$GLOBALS[" nocase + $s2 = "call_user_func_array" nocase + $s3 = "array_map" nocase + $s4 = "array_filter" nocase + $s5 = "ReflectionFunction" nocase + + $input = /\$_(GET|POST|REQUEST|COOKIE)/ + + condition: + 2 of ($s*) and $input +} + +rule Suspicious_Error_Suppression { + meta: + description = "에러 억제 + 위험 함수" + severity = "medium" + + strings: + $suppress = /@(eval|assert|system|exec|shell_exec|passthru)\s*\(/ + + condition: + $suppress +} + +// ============================================ +// 이미지 위장 웹쉘 +// ============================================ + +rule PHP_In_Image { + meta: + description = "이미지 파일 내 PHP 코드" + severity = "high" + + strings: + $gif = { 47 49 46 38 } // GIF header + $jpg = { FF D8 FF } // JPEG header + $png = { 89 50 4E 47 } // PNG header + + $php1 = "