一、背景
干渗透测试这几年,我发现一个规律:拿到一个目标,决策流程几乎是固定的。
nmap扫端口 → 看到Web端口就上whatweb → 识别完技术栈就上gobuster → 扫完目录就上nuclei → 发现参数就试试sqlmap
这个过程,说白了就是一套固定的决策树。但传统的自动化工具(比如那些nmap自动脚本)是硬编码的if-else,遇到特殊情况就卡死了。比如nmap发现8080,如果是个Jenkins,那策略和普通Tomcat又不一样。
我的想法是:用AI来做这个决策。
AI能看懂nmap结果,知道8080可能是Tomcat也可能是Jenkins;能理解whatweb的输出,判断这是WordPress还是自己写的CMS;能根据当前收集到的信息,决定下一步该干什么。
选Ollama有几个原因:
本地跑,测试数据不用上传,甲方放心
模型随便换,电脑配置高就用大点的,配置低就用小点的
API简单,几行代码就能调
目前用的qwen2.5:7b,我的MacBook Pro 16G内存能跑,响应2-5秒,能接受。
二、整体架构
整个系统的核心思路是数据驱动,不是流程驱动。
传统的自动化工具是这样的:
跑nmap → 代码里写死if判断 → 跑whatweb → 代码里写死if判断 → 跑gobuster
这套方案是这样的:
1774153379_69bf6ea33cdab8be3223c.png!small?1774153379479
关键区别在于:AI做决策的时候,看的不是"我上一步干了什么",而是"我现在知道了什么"。
比如说,如果nmap发现8080端口跑的是Jenkins,AI可能会决定先扫目录,因为Jenkins默认有/script路径;如果发现的是Tomcat,可能会先跑漏洞扫描,因为Tomcat有CVE-2017-12615这种经典漏洞。
这个能力,硬编码的if-else很难覆盖全,但AI可以。
数据流向图:
1774153746_69bf7012537e122ed7924.png!small?1774153746586
任何工具跑完,结果都送到聚合器;聚合器根据工具类型提取关键信息;提取完存进数据库;AI决策时从数据库读当前状态;AI决定下一个工具。闭环。
三、工具接入
框架计划接入的工具,按功能分三类:
3.1 信息收集类
工具 用途 解析器干什么的 AI什么时候用
nmap 端口扫描、服务识别 从输出里扒端口号、服务名、版本号 刚开始,或者发现新IP的时候
gobuster Web目录爆破 扒路径和状态码,200的加入URL列表 发现Web服务之后
whatweb 网站指纹识别 扒CMS、框架、Web服务器类型 发现Web端口之后,想知道这是啥系统
subfinder 子域名收集 扒子域名列表 目标是域名,想扩大攻击面
3.2 漏洞检测类
工具 用途 解析器干什么的 AI什么时候用
sqlmap SQL注入检测 判断有没有注入、什么数据库 发现URL带参数,比如?id=1
nuclei 模板化漏洞扫描 扒CVE编号、漏洞名、严重程度 识别完技术栈,扫一波已知漏洞
nikto Web服务器扫描 扒配置问题、危险文件 快速检查服务器配置
3.3 漏洞利用类
工具 用途 解析器干什么的 AI什么时候用
metasploit 漏洞利用框架 扒利用结果、session信息 匹配到CVE且有利用模块
自定义POC生成器 AI生成利用代码 扒生成的代码、执行结果 遇到没有现成工具的漏洞
四、核心模块怎么设计的
4.1 配置管理
1774153810_69bf705240568ef071879.png!small
配置长这样:
ollama:
model: "qwen2.5:7b"
temperature: 0.3
tools:
nmap:
path: "/usr/bin/nmap"
timeout: 300
wordlists:
dir: "/usr/share/wordlists/dirb/common.txt"
strategy:
max_steps: 15
用户只需要改自己关心的配置,其他的用默认值兜底。
4.2 工具注册表
每个工具都注册成一份元数据:
{
"name": "nmap",
"description": "端口扫描工具,发现目标开放的TCP/UDP端口",
"category": "recon",
"command_template": "nmap {{target}} -p {{ports}} -sV",
"parameters": {
"target": {"type": "string", "required": True},
"ports": {"type": "string", "default": "1-1000"}
},
"output_parser": "nmap_parser"
}
AI决策的时候能看到所有工具的描述,然后选一个合适的,填参数,返回给系统执行。
新增工具只需要加一份这样的元数据,再写个对应的解析器就行。
4.3 发现数据库
所有发现都存进数据库,供AI查询。
主要存这几类数据:
表 存什么
ports 端口号、服务名、版本号
services 服务名、版本、对应端口
web_technologies 技术名、版本、URL
urls URL、状态码
vulnerabilities 漏洞名、严重程度、位置、CVE编号
tool_history 工具名、参数、输出摘要、时间
数据库提供一个get_summary()接口,给AI看当前状态:
{
"ports": [22, 80, 443, 8080],
"has_web_service": true,
"web_technologies": ["Apache Tomcat 8.5.34"],
"urls_count": 12,
"vulnerabilities_count": 0
}
AI看到的是精简的状态,不是几千行的nmap输出。
4.4 命令执行器
负责跑命令,同时干几件事:
参数转义,防止命令注入
超时控制,防止工具卡死
危险命令拦截(比如rm -rf)
临时文件管理
1774153879_69bf7097cb4f671c5f7b7.png!small?1774153880101
4.5 结果聚合器
这是最关键的部分。不同工具的输出格式不一样,需要解析成统一的结构化数据。
1774153988_69bf7104331d02f6931da.png!small?1774153988786
举个例子,nmap输出长这样:
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.6
443/tcp open https Apache httpd 2.4.6
8080/tcp open http Apache Tomcat 8.5.34
解析器扒出来:
{
"ports": [80, 443, 8080],
"services": [
{"port": 80, "name": "http", "version": "Apache httpd 2.4.6"},
{"port": 8080, "name": "http", "version": "Apache Tomcat 8.5.34"}
]
}
whatweb输出:
http://target:8080 [200] Apache-Tomcat/8.5.34, Struts2
解析器扒出来:
{
"web_technologies": ["Apache Tomcat 8.5.34", "Struts2"]
}
这样AI看到的就是干净的数据,不用从一堆文本里找关键信息。
核心解析器代码片段(nmap解析器):
def parse_nmap(output):
result = {"ports": [], "services": []}
# 匹配端口行: "80/tcp open http Apache httpd 2.4.6"
pattern = r'(\d+)/(tcp|udp)\s+(\w+)\s+(\w+)\s*(.*)'
for line in output.split('\n'):
match = re.search(pattern, line)
if match:
port = int(match.group(1))
protocol = match.group(2)
state = match.group(3)
service = match.group(4)
version = match.group(5).strip()
result["ports"].append(port)
result["services"].append({
"port": port,
"name": service,
"version": version
})
return result
whatweb解析器代码片段:
def parse_whatweb(output):
result = {"technologies": []}
# 常见技术栈关键词
techs = ["nginx", "apache", "tomcat", "wordpress", "joomla",
"drupal", "struts2", "spring", "rails"]
for tech in techs:
if tech.lower() in output.lower():
result["technologies"].append(tech)
# 尝试扒版本号
version_pattern = f"{tech}[\\s\\[]*([0-9.]+)"
match = re.search(version_pattern, output, re.IGNORECASE)
if match:
result["technologies"].append(f"{tech} {match.group(1)}")
return result
nuclei解析器代码片段:
def parse_nuclei(output):
result = {"vulnerabilities": []}
# 匹配漏洞行: "[http] [high] CVE-2017-5638 [Struts2 RCE]"
pattern = r'\[(\w+)\]\s+\[(\w+)\]\s+([^:]+):?\s*(.*)'
for line in output.split('\n'):
match = re.search(pattern, line)
if match:
protocol = match.group(1)
severity = match.group(2)
name = match.group(3).strip()
detail = match.group(4).strip()
result["vulnerabilities"].append({
"name": name,
"severity": severity,
"description": detail,
"protocol": protocol
})
return result
4.6 AI决策引擎
决策引擎读数据库状态,调Ollama,决定下一步。
1774154044_69bf713cbe40abfa4e64b.png!small?1774154047514
核心代码片段:
def decide(target, step, max_steps):
读当前状态
summary = db.get_summary()
# 构建提示词
prompt = f"""
当前状态:
- 目标: {target}
- 已发现端口: {summary['ports']}
- 是否有Web服务: {summary['has_web_service']}
- 已识别技术栈: {summary['web_technologies']}
- 已发现漏洞: {summary['vulnerabilities_count']}
可用工具:
- nmap: 端口扫描
- whatweb: Web技术识别
- gobuster: 目录爆破
- nuclei: 漏洞扫描
- sqlmap: SQL注入检测
根据当前状态,决定下一步。返回JSON:
{{"tool": "工具名", "parameters": {{}}, "reasoning": "原因"}}
规则参考:
-
没端口信息 → nmap
-
有Web端口没技术信息 → whatweb
-
有技术信息没扫目录 → gobuster
-
没扫漏洞 → nuclei
-
发现高危漏洞 → 停止
"""
调Ollama
response = ollama.chat(
model="qwen2.5:7b",
messages=[{"role": "user", "content": prompt}]
)
解析JSON
try:
decision = json.loads(response['message']['content'])
return decision
except:
解析失败,用规则引擎兜底
return rule_based_decision(summary)
降级规则引擎:
def rule_based_decision(summary):
if summary['ports_count'] == 0:
return {"tool": "nmap", "parameters": {"scan_type": "quick"}}
if summary['has_web_service'] and not summary['web_technologies']:
return {"tool": "whatweb", "parameters": {}}
if summary['web_technologies'] and summary['urls_count'] < 5:
return {"tool": "gobuster", "parameters": {}}
if summary['vulnerabilities_count'] == 0:
return {"tool": "nuclei", "parameters": {}}
return {"action": "report"}
4.7 报告生成器
最后把数据库里的东西倒出来,生成报告。
def generate_report():
summary = db.get_summary()
report = f"""# 渗透测试报告
发现摘要
- 开放端口: {summary['ports']}
- Web技术: {summary['web_technologies']}
- 漏洞总数: {summary['vulnerabilities_count']}
- 高危: {summary['high_count']}
漏洞详情
"""
for vuln in db.get_high_severity_vulns():
report += f"""
{vuln['name']}
-
严重性: {vuln['severity']}
-
描述: {vuln['description']}
-
发现工具: {vuln['tool']}
"""
return report
五、跑起来的样子
在VulHub搭了个Struts2靶机,跑了一遍:
启动\] 目标: 192.168.1.100 \[步骤1\] 决策: 没端口信息,用nmap \[执行\] nmap -sS 192.168.1.100 \[结果\] 发现端口: 22, 80, 8080 \[步骤2\] 决策: 发现Web端口,用whatweb识别技术栈 \[执行\] whatweb http://192.168.1.100:8080 \[结果\] 技术栈: Apache Tomcat 8.5.34, Struts2 \[步骤3\] 决策: 识别了技术栈,用gobuster扫目录 \[执行\] gobuster dir -u http://192.168.1.100:8080 \[结果\] 发现路径: /admin, /examples, /manager \[步骤4\] 决策: 还没扫漏洞,用nuclei \[执行\] nuclei -u http://192.168.1.100:8080 \[结果\] 发现漏洞: CVE-2017-5638 (Struts2 RCE) \[步骤5\] 决策: 发现高危漏洞,生成报告 \[报告\] report_192.168.1.100.md \[完成\] 耗时: 6分钟 生成的报告: ## 渗透测试报告 ### 发现摘要 * 开放端口: 22, 80, 8080 * Web技术: Apache Tomcat 8.5.34, Struts2 * 漏洞总数: 1 * 高危: 1 ### 漏洞详情 #### CVE-2017-5638 * 严重性: high * 描述: Struts2远程代码执行漏洞 * 发现工具: nuclei 六、还存在的问题 实话实说,这套方案还有很多问题: 1. 决策质量看模型脸色 qwen2.5:7B在简单场景下还行,复杂场景就有点懵。比如同时发现多个Web服务,或者需要识别业务逻辑的时候,AI的判断就不太准。 2. 解析器写起来麻烦 每个工具的输出格式不一样,都得单独写解析器。像sqlmap这种输出特别复杂的,解析器写起来挺费劲。 3. 工具覆盖不全 目前只规划了nmap、whatweb、gobuster、nuclei这几个基础工具。更多专业工具需要慢慢接进来。 4. 交互式场景搞不定 遇到需要手动交互的漏洞(比如文件上传后要猜路径),AI没法处理,还得人上。 5. 没在真实项目里验证过 目前只在靶机里跑过,没在真实的授权项目里用过。实际效果怎么样,还不知道。 七、后续想做的 多模型搭配:小模型做快速决策,大模型做复杂分析。决策用7B,分析漏洞详情的时候切到32B。 经验积累:每次测试完,把成功的案例存起来,下次遇到类似目标可以直接复用。 人工确认:关键步骤让用户确认一下,防止AI犯错造成损失。 并行跑:多个独立任务同时跑,提高效率。 八、写在最后 做渗透测试这几年,最深的感受是:重复性判断占了太多时间。nmap结果看一眼就知道该上whatweb,whatweb结果看一眼就知道该上gobuster------这些判断不需要天才,需要的是经验和耐心。 如果AI能把这些体力活干了,人就能腾出手来做真正复杂的事情。 这个框架还只是设计方案,很多想法还没落地。但方向应该是对的:让AI承担重复决策,人专注技术突破。