OPEN HYPER STEP
← 목록으로 (화이트햇 보안)
SECURITY · 83 / 84
security
CHAPTER 83 / 84
읽기 약 2
FUNCTION

실전 보안 도구: 웹 취약점 스캐너


핵심 개념

⚠️ 모듈 1~5 통합 최종 프로젝트. URL 입력 → 보안 헤더·SQL/XSS 패턴·디렉터리 탐색 → JSON/HTML 리포트.

본문

도구 아키텍처

📋 코드 (6줄)
secscan scan <URL>
  ├─ 1. 보안 헤더 점검 (CH.66 모듈)
  ├─ 2. SQL Injection 패턴 탐지 (CH.890 응용)
  ├─ 3. XSS 패턴 탐지 (CH.891 응용)
  ├─ 4. 디렉터리 탐색 (CH.892 응용)
  └─ 5. JSON/HTML 리포트 생성

통합 스캐너 코드

PYTHON📋 코드 (213줄)
#!/usr/bin/env python3
# secscan_full.py — 통합 웹 취약점 스캐너
# ⚠️ 본인 소유 또는 허가된 환경에서만 사용하세요.
import argparse
import json
import sys
import time
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.parse import urljoin, urlparse

import requests
from bs4 import BeautifulSoup


REQUIRED_HEADERS = {
    'Strict-Transport-Security': 'HSTS 필수',
    'Content-Security-Policy': 'CSP 필수',
    'X-Content-Type-Options': 'MIME sniffing 차단',
    'X-Frame-Options': 'Clickjacking 차단',
    'Referrer-Policy': 'Referrer 정보 제한',
}

XSS_PAYLOADS = [
    '<script>alert(1)</script>',
    '"><svg onload=alert(1)>',
    "javascript:alert(1)",
]

SQLI_PAYLOADS = [
    "' OR '1'='1",
    "' UNION SELECT NULL--",
    "1' AND SLEEP(3)--",
]

COMMON_DIRS = [
    '.git/HEAD', '.env', 'admin/', 'backup.zip',
    'wp-admin/', 'phpinfo.php', 'config.php.bak',
]


@dataclass
class Finding:
    severity: str  # high / medium / low / info
    category: str
    title: str
    detail: str
    evidence: str = ''


@dataclass
class ScanReport:
    target: str
    started_at: str
    finished_at: str = ''
    findings: list[Finding] = field(default_factory=list)


def check_headers(url: str) -> list[Finding]:
    findings = []
    try:
        r = requests.get(url, timeout=10, allow_redirects=True)
    except requests.RequestException as e:
        return [Finding('high', 'connection', '연결 실패', str(e))]

    for header, desc in REQUIRED_HEADERS.items():
        if header not in r.headers:
            findings.append(Finding(
                severity='medium',
                category='headers',
                title=f'{header} 누락',
                detail=desc,
            ))
    return findings


def check_xss_reflection(url: str, params: dict[str, str]) -> list[Finding]:
    findings = []
    for payload in XSS_PAYLOADS:
        for key in params:
            test = {**params, key: payload}
            try:
                r = requests.get(url, params=test, timeout=8)
            except requests.RequestException:
                continue
            if payload in r.text:
                findings.append(Finding(
                    severity='high',
                    category='xss',
                    title=f'반사 XSS 가능 — 파라미터 "{key}"',
                    detail='페이로드가 응답에 그대로 반사됨',
                    evidence=payload[:80],
                ))
                break
    return findings


def check_sql_errors(url: str, params: dict[str, str]) -> list[Finding]:
    sql_errors = ['SQL syntax', 'mysql_fetch', 'pg_query', 'ORA-', 'SQLite/JDBCDriver']
    findings = []
    for payload in SQLI_PAYLOADS:
        for key in params:
            test = {**params, key: payload}
            try:
                t0 = time.monotonic()
                r = requests.get(url, params=test, timeout=10)
                elapsed = time.monotonic() - t0
            except requests.RequestException:
                continue
            for err in sql_errors:
                if err.lower() in r.text.lower():
                    findings.append(Finding(
                        severity='high',
                        category='sqli',
                        title=f'SQL 에러 노출 — 파라미터 "{key}"',
                        detail=f'에러 시그니처 "{err}" 응답 본문에 포함',
                        evidence=payload,
                    ))
                    break
            # Time-based 추정 (SLEEP 페이로드 + 응답 지연)
            if 'SLEEP' in payload and elapsed > 2.5:
                findings.append(Finding(
                    severity='high',
                    category='sqli',
                    title=f'Time-based SQL 의심 — 파라미터 "{key}"',
                    detail=f'응답 지연 {elapsed:.1f}s',
                    evidence=payload,
                ))
    return findings


def check_directory_listing(base_url: str) -> list[Finding]:
    findings = []
    for path in COMMON_DIRS:
        url = urljoin(base_url, path)
        try:
            r = requests.head(url, timeout=5, allow_redirects=False)
        except requests.RequestException:
            continue
        if r.status_code == 200:
            findings.append(Finding(
                severity='medium',
                category='directory',
                title=f'민감 경로 노출 — {path}',
                detail=f'HTTP 200 OK at {url}',
                evidence=url,
            ))
    return findings


def render_html(report: ScanReport) -> str:
    sev_color = {'high': '#ef4444', 'medium': '#f59e0b', 'low': '#3b82f6', 'info': '#6b7280'}
    rows = []
    for f in report.findings:
        rows.append(
            f'<tr>'
            f'<td style="color:{sev_color.get(f.severity, "#000")}">{f.severity.upper()}</td>'
            f'<td>{f.category}</td>'
            f'<td>{f.title}</td>'
            f'<td>{f.detail}</td>'
            f'</tr>'
        )
    return f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>secscan report</title>
<style>body{{font-family:monospace;max-width:900px;margin:2rem auto;padding:0 1rem}}
table{{border-collapse:collapse;width:100%}}
th,td{{border:1px solid #ddd;padding:8px;text-align:left}}
th{{background:#f4f4f4}}</style>
</head><body>
<h1>secscan 리포트</h1>
<p>대상: {report.target}<br>시작: {report.started_at}<br>종료: {report.finished_at}</p>
<p>발견: <strong>{len(report.findings)}</strong>건</p>
<table><tr><th>SEV</th><th>카테고리</th><th>제목</th><th>설명</th></tr>
{"".join(rows)}
</table></body></html>"""


def main():
    parser = argparse.ArgumentParser(description='⚠️ 허가된 환경 전용 보안 스캐너')
    parser.add_argument('url', help='대상 URL')
    parser.add_argument('--params', default='', help='쿼리 파라미터 key=value,key2=value2')
    parser.add_argument('-o', '--output', help='리포트 파일 (.json 또는 .html)')
    args = parser.parse_args()

    params = dict(p.split('=', 1) for p in args.params.split(',') if '=' in p) if args.params else {}

    report = ScanReport(
        target=args.url,
        started_at=datetime.now(timezone.utc).isoformat(),
    )
    report.findings.extend(check_headers(args.url))
    if params:
        report.findings.extend(check_xss_reflection(args.url, params))
        report.findings.extend(check_sql_errors(args.url, params))
    report.findings.extend(check_directory_listing(args.url))
    report.finished_at = datetime.now(timezone.utc).isoformat()

    if args.output and args.output.endswith('.html'):
        Path(args.output).write_text(render_html(report))
    else:
        out = json.dumps(asdict(report), indent=2, default=str)
        if args.output:
            Path(args.output).write_text(out)
        else:
            print(out)

    print(f'[secscan] {len(report.findings)} findings', file=sys.stderr)


if __name__ == '__main__':
    main()

실행 예시

BASH📋 코드 (6줄)
# 본인 소유 사이트 점검
$ python secscan_full.py https://my-site.local --params "q=test,id=1" -o report.html
[secscan] 7 findings

# JSON 리포트
$ python secscan_full.py https://my-site.local -o report.json

AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

이 스캐너 코드를 분석해서 누락된 검사 항목과
오탐 줄이는 방법, 성능 개선안을
실제 PR 단위로 만들어줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

내 사이트 URL을 줄게 (본인 소유).
어떤 페이로드와 파라미터 조합으로 스캔하면
실전 OWASP Top 10 커버할지 가이드해줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내가 점검한 사이트의 스캔 결과 JSON을 줄게.
severity별 임팩트 평가 + CVSS 점수 추정 +
우선 수정해야 할 Top 5를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 OSS 보안 스캐너 Top 5(Nuclei/ZAP/Nikto 등)와
이 자작 스캐너의 실전 격차를 솔직히 알려줘.

⭐ 이것만 기억하세요
실전 보안 도구: 웹 취약점 스캐너 이 3가지만 확실히 잡으세요
1.모듈 1~5의 모든 기법(헤더 점검·XSS/SQLi 패턴·디렉터리 탐색·argparse·dataclass·JSON 리포트)을 통합한 실전 도구
2.severity 4단계(high/medium/low/info)로 발견 사항을 분류 — 우선순위 매기기 가능
3.⚠️ 이 도구는 본인 소유 또는 명시적 허가된 환경에서만 사용 — 무단 스캔은 정보통신망법 위반


공유하기
진행도 83 / 84