做漏洞检测的人,大概率都遇到过这种场景:手里攒了一堆POC脚本,有的用Python写的,有的用Perl,有的干脆就是某个框架里的插件。每次要批量检测资产的时候,都得先折腾一遍环境依赖,脚本之间参数还不统一,调来调去半天就没了。后来社区里开始流行用YAML模板描述漏洞检测规则,前端只负责调度,后端用统一的引擎去解释这些模板,这才算把规则和引擎解耦开来。今天要聊的OA-EXPTOOL就是这个思路下的产物,它的核心玩法是:把一次完整的漏洞检测抽象成一个YAML文件,请求怎么发、响应怎么判断、需要提取什么变量,全部写进模板里。你不需要会写代码,只要搞懂模板里的几个关键字段,就能定制出自己的检测规则。
这篇东西适合两类人看。一类是刚接触漏洞检测、想快速上手模板编写的新手,另一类是手里已有检测工具、想批量定制自己检测规则的进阶选手。我会从模板最基本的字段开始拆解,一步步讲到请求构造、响应匹配、通用片段复用这几个核心环节,最后把我踩过的坑和排查问题的方式也一并整理出来。看完你至少能独立写出一个能跑的检测规则模板,并且知道怎么调试它。
1. 为什么是YAML模板:把检测逻辑变成可读的配置文件
1.1 脚本式检测的痛点
早期做漏洞检测,大家习惯直接用脚本写死检测逻辑。比如用Python的requests库发一个GET请求,然后判断响应里有没有特定字符串。这种方式在小规模场景下没什么问题,但一旦检测目标从几台机器变成几百上千个IP的时候,痛点就出来了。
首先是重复劳动。同一个检测逻辑,因为目标环境不同,可能需要复制出好几个版本。其次是维护成本,漏洞利用细节更新了,你得去翻脚本、改代码、重新测试,一不小心还会把别的检测逻辑改坏。最麻烦的是扩展性,新来一个漏洞,要重新写一遍请求、判断、输出的全套流程,代码风格还不一定统一。
1.2 YAML模板解决了什么
YAML模板的核心思想是"数据与逻辑分离"。引擎负责执行,模板负责描述。你要做的不是写代码,而是回答三个问题:请求怎么发?怎样算命中?命中后提取什么?
每一个规则模板是一个独立的YAML文件,包含基本信息、请求块、匹配块、提取块这几个部分。引擎读取这个文件后,会按照里面的描述自动完成任务。好处是显而易见的:规则和引擎彻底解耦,写规则不需要了解引擎的内部实现;规则之间天然隔离,新增一个漏洞就是一个新文件,互不影响;规则以文本形式存储,方便做版本管理、多人协作,甚至可以在社区分享。
1.3 一套模板适配多场景
还有一个被很多人忽略的点是跨平台性。YAML本身就是一种纯文本格式,不依赖任何特定操作系统。同一份模板,在Windows、Linux、macOS上都能跑;同一份模板,既可以用命令行工具跑,也可以集成到自己的Web平台上,甚至可以被其他安全工具调用。我在实际项目中曾经把这套模板直接嵌入到自己的资产管理平台里,新发现一个资产就自动跑一遍模板,效果很理想。
2. 模板编写前的知识储备:YAML语法与HTTP基础
2.1 YAML语法快速上手
写YAML模板,最低限度要掌握这几种语法:
- 键值对。
key: value,注意冒号后面必须有一个空格。 - 嵌套结构。用缩进表示层级关系,缩进推荐使用两个空格,不要用Tab。
- 列表。用短横线开头,表示一个数组元素。
- 字符串。默认不需要引号,但如果值包含特殊字符,建议用单引号包起来。
- 多行文本。可以用
|保留换行,用>折叠成一行。
举个简单例子:
info: name: example-check severity: medium requests: - method: GET path: - /index.php这里info是一个映射,包含name和severity两个键;requests是一个列表,列表里有一个元素,这个元素里有method和path两个键,而path的值又是一个列表。写错缩进是最常见的低级错误,所以我建议刚开始写模板的时候,用一个支持YAML语法高亮的编辑器,可以帮你提前暴露格式问题。
2.2 HTTP协议核心概念
模板本质上是操作HTTP请求和响应。请求块需要定义方法、路径、请求头、请求体;匹配块需要对响应的状态码、头信息、正文做判断。因此你需要对HTTP的方法语义、状态码分类、请求头作用、Cookie机制、URL编码规则有基本认知。
比如状态码,200代表成功,302代表重定向,403代表禁止访问,500代表服务端异常。很多检测规则是"只要响应状态码不是200且没有报错特征就算可疑",这种判断就需要你对状态码范围有清晰的认识。再比如请求头,User-Agent、Content-Type、Referer这些字段在某些场景下会成为匹配条件。
2.3 环境准备与工具链
实际操作前,建议准备好以下环境:
- 一个支持YAML的编辑器,比如VS Code加上YAML插件。
- 一个HTTP代理工具用于调试请求和响应,比如Burp Suite。
- 一个本地靶场环境。没有的话可以用Docker起一个轻量级的Web服务来做测试。
- 目标工具的二进制文件或源码。确保它支持加载自定义YAML规则。
提示:在本地搭建靶场做测试时,建议使用容器化的临时环境。这样即使模板写炸了,也只是影响容器内的服务,不会波及宿主机。
3. 模板基础结构拆解:从一份最小示例说起
下面是OA-EXPTOOL中一份典型模板的基础结构。你可以把它理解成一个最小可运行的骨架,后面所有复杂功能都是在这个骨架之上叠加的。
id: CVE-2020-0001 info: name: 示例漏洞 - 路径遍历检测 author: demo_author severity: high description: | 检测目标是否存在路径遍历漏洞。 如果目标存在该漏洞,可读取服务器上的敏感文件。 reference: - https://example.com/path/traversal requests: - method: GET path: - /../../../../etc/passwd matchers: - type: regex regex: - "root:.*:0:0:"这个模板做了三件事:声明规则的基本信息,构造一个GET请求并访问路径,然后判断响应正文是否匹配正则表达式。所有规则模板都围绕这三层展开。
3.1 id与info块:规则的身份证
id是模板的唯一标识。命名上建议使用漏洞编号或自定义的命名规则,比如CVE-2020-0001、xxx-oa-login-bypass。引擎在去重、缓存结果、日志输出时都会用到这个ID,所以尽量不要重复。
info块里记录规则的名词解释信息。name是规则名称,建议能做到"见名知义";severity是风险等级,通常有info、low、medium、high、critical五档;description是漏洞描述,可以用多行文本详细说明;reference是参考链接列表,指向漏洞公告或分析文章。
注意:
id不能包含空格和特殊字符,最好统一使用大写字母、数字、连字符和下划线。
3.2 requests块:一次检测的核心动作
requests是模板的主体,它定义了一次或多次HTTP请求。每次请求可以指定:
method:请求方法,常见的有GET、POST、PUT、DELETE等。path:请求路径,支持多个路径,也可以包含变量。headers:请求头,可以是固定的,也可以是动态生成的。body:请求体,通常配合POST方法一起用。matchers:匹配规则,决定本次请求是否命中漏洞。extractors:变量提取规则,把响应里的某些值提取出来给后续步骤使用。
一个模板里可以有多组请求,后一组请求可以引用前一组请求提取出来的变量。这种链式请求在检测需要先登录、再访问敏感接口的漏洞时特别有用。
3.3 matchers块:判断漏洞是否存在的关键
matchers是模板里最核心的决策部分。引擎会根据你定义的匹配条件去检查响应内容。常见类型有这么几种:status(状态码)、word(关键字)、regex(正则表达式)、binary(二进制数据)、dsl(表达式)。多个匹配条件可以组合使用,用condition字段指定是and还是or。
matchers: - type: word words: - "root:x:0:0" part: body condition: and以上配置的含义是:响应正文同时包含字符串root:x:0:0,就算匹配成功。part指定匹配响应的哪部分,常见值有body、header、status。
4. 核心请求构造:方法、路径、请求头与动态变量
4.1 method与path:最小化检测单元
在构造请求时,method和path是最直观的字段。你要检测一个未授权访问漏洞,通常就是发一个GET请求到目标路径;你要检测一个登录接口的SQL注入,就得用POST请求,把注入语句放进请求体。
path可以写多个值,引擎会按顺序逐个发起请求。这个特性在检测多个变体路径时很好用,比如同一个漏洞可能出现在/user/info和/api/user/info两个路径,就可以一次性写在同一个请求组里。
4.2 动态变量:让请求带点随机性
很多漏洞检测场景需要在请求参数里加入随机字符串。比如检测一个反射型XSS,如果每次都使用固定的payload,可能会被WAF拦截,或者因为缓存策略造成误判。YAML模板支持在字段中引用随机变量,这也是一大优势。
path: - "/search?keyword={{randstr}}"执行时{{randstr}}会被替换成一个随机字符串。类似的内置变量还有随机数字、时间戳、IP地址等。这个机制让同一个模板在多次执行时产生不同的请求特征,从整体上降低了被拦截的概率。
提示:如果你在模板中用了多个随机变量,引擎通常会在同一次请求内保持它们的值一致,这样更接近真实用户的行为特征。
4.3 请求头与Cookie:模拟真实客户端
很多Web应用会校验请求头,尤其是User-Agent、Referer、Origin这几个字段。模板的headers字段允许你自定义这些值。有些目标的响应会返回Set-Cookie,引擎支持自动处理Cookie,后续请求会带上它。当然也可以手动指定Cookie,比如预先从某个接口获取凭证后固定写入。
headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Referer: https://example.com/admin/ Cookie: sessionid=abc1234.4 请求体构造:POST场景的常见写法
POST请求通常需要指定body字段。可以是一个JSON字符串,也可以是表单格式。引擎会根据Content-Type头决定如何编码:如果请求头里写的是application/json,body里的内容会被当作JSON发送;如果写的是application/x-www-form-urlencoded,body里的内容会被当作表单发送。
method: POST path: - /api/login headers: Content-Type: application/x-www-form-urlencoded body: "username=admin&password={{randstr}}"一个容易踩的坑是:body里的内容包含特殊字符时,需要确保YAML解析器正确识别。最简单的方法是给body值加上双引号,让YAML把它当作一个整体字符串。
5. 响应匹配与条件判定:状态码、关键字、正则、表达式与多条件组合
5.1 状态码匹配:最直观的命中断言
状态码匹配通常用在"目标接口存在性判断"上。比如要探测某个路径是否存在,如果返回状态码是200,说明接口是存在的;如果是404,则说明路径不存在。
matchers: - type: status status: - 200 - 301 - 302一个常见的组合用法是:先用状态码判断接口是否存在,再用正文关键字判断是否是漏洞特征。这种分层判断能显著降低误报率。
5.2 关键字匹配:零基础也能上手的判断方式
关键字匹配是最容易理解的方式。只要响应里包含你指定的字符串,就算命中。它的优点是稳定、不易出错;缺点是过于依赖特定字符串,如果目标版本更新后把特征改了,规则就失灵了。
matchers: - type: word words: - "SQLSTATE[" - "Warning: mysql_" part: body上面这个例子会匹配正文里任何一个值出现,就认为命中。如果你的目标是有多个特征,希望所有特征同时出现才算命中,务必把condition字段设为and。
5.3 正则匹配:更灵活的瘦身方式
正则表达式能匹配一段模式而非固定字符串,适合检测那些特征变化较多的漏洞。比如不同版本的中间件报错信息可能不同,但UUID、路径风格、关键字的位置有一定规律可循,这时候正则就派上用场。
matchers: - type: regex regex: - "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" part: body5.4 表达式匹配:应对复杂逻辑场景
当单个匹配类型不够用时,可以用dsl表达式做更复杂的逻辑判断。比如要求响应状态码大于等于500且正文包含某个错误堆栈关键字,单靠status和word组合不一定直观,用dsl就能实现。
matchers: - type: dsl dsl: - "status_code >= 500 && contains(body, 'stack trace')"这种情况下,模板看起来像是"在配置里写代码",但它依然是声明式的,引擎负责解释执行。写dsl表达式时要注意变量名大小写,不同的模板引擎可能对字段名有不同约定。
5.5 多匹配条件组合:降低误报率的正确姿势
实战中单一条件往往不够可靠。比如你判断一个文件上传漏洞,可能要同时满足:响应状态码是200,返回内容里包含上传后的路径,路径里包含你设置的随机文件名。这时多个matchers组合使用就非常关键。
matchers: - type: status status: - 200 - type: word words: - "{{randstr}}" part: body matchers-condition: and引擎执行时会按顺序检查所有匹配条件,只有全部满足(或按or逻辑中任意一个满足)才算命中。合理组合多个条件,是减少误报、提高检测准确率的核心手段。
6. 通用片段复用:提取变量、多步骤请求与公共模板
6.1 变量提取:把响应当中的数据挖出来
有些漏洞场景需要从响应里提取某个值,再用于后续请求。比如登录后返回一个Token,extractors就可以把Token从响应里抓出来,存成变量,下一个请求引用它。
extractors: - type: regex name: token regex: - "csrf_token\":\"([a-f0-9]{32})\"" part: body执行过后,变量token就保存了提取到的值。你可以在后续请求的path、body、headers字段里用{{token}}引用它。这个功能把"先登录、再访问受保护接口"这类的多步骤漏洞检测变得完全自动化。
6.2 多步骤请求:模拟真实的攻击链路
很多安全检测不是一次请求就完成的。比如一个漏洞需要先通过一个接口获取上传凭证,再携带凭证上传恶意文件,然后访问上传后的文件确认内容被解析。这些步骤在模板里可以按顺序写多组requests,每一组可以引用前面的提取变量。
requests: - method: GET path: - /api/get-upload-token extractors: - type: regex name: upload_token regex: - "token\":\"([a-f0-9]{32})\"" - method: POST path: - /api/upload headers: X-Token: "{{upload_token}}" body: "file={{randstr}}" matchers: - type: word words: - "upload success"这种链式结构让模板能应对真实攻击链路更长的漏洞。
6.3 公共模板片段:减少重复劳动
当你积累了足够多的模板后,会发现很多模板里有一段相同的请求逻辑,像是"获取登录态"或"生成加密签名"这类步骤。OA-EXPTOOL允许把这类重复逻辑抽成公共模板片段,在主模板里通过include之类的机制引用进来。这样既能保持主模板简洁,又能在多处复用一个已经调好的逻辑,维护时只需要改公共片段,不用逐个去翻模板。
提示:在抽取公共片段之前,先确认目标引擎是否支持该能力。如果不支持,可以退一步,用脚本生成模板的方式来复用公共片段,效果一样。
7. 实战案例:一个OA系统弱口令检测模板的完整拆解
理论讲了这么多,来看一个具体案例。假设目标是一个常见的OA系统,登录接口是/login.php,登录成功后跳转到/index.php。检测思路是:尝试一组弱口令,如果登录成功,则判定存在弱口令风险。
id: demo-oa-weak-password info: name: 某OA系统弱口令检测 author: demo severity: high description: | 尝试使用弱口令登录目标OA系统,若登录成功则存在弱口令风险。 reference: - https://example.com/oa/security requests: - method: POST path: - /login.php headers: Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) body: "username=admin&password=admin&submit=login" matchers: - type: word words: - "location.href='/index.php'" part: body这个模板虽然简单,但体现了前面讲的几个关键点:固定请求头模拟真实客户端、POST表单提交、用登录重定向特征判断是否成功。实际项目里,你还可以用extractors提取登录后的Cookie,再用第二组请求访问一个只有登录后才能访问的接口来二次确认,这样误报率会更低。
再复杂一点的应用场景:假设目标后台是动态生成加密参数的,比如需要先获取一个验证码,然后连同验证码一起提交登录。这就可以用多组requests和extractors写成一个流水线,验证码提取后作为变量传给登录请求。模板写好后,每次执行都会重复同样的流程,不需要手动干预。
8. 模板调试与常见问题排查实录
8.1 验证模板的正确性
模板写完之后,不要直接跑到资产上执行。先在本地靶场环境用-validate之类的参数检查模板语法是否合法。还要测试一下匹配逻辑是否正确:故意配置一个不存在的路径,确认模板不会误报;再配置一个确定存在的路径,确认模板能正确命中。
8.2 高误报率的常见原因
最典型的原因是匹配条件过于宽松。比如word匹配时用了太短的字符串,像"admin"、"index"这种常用词,很容易在其他无关响应里出现。解决办法是尽量使用更长、更独特的特征字符串,最好是漏洞特有的报错信息或关键路径片段。
另一个原因是把and写成了or。多个匹配条件之间的逻辑关系决定了准确率。如果你希望所有条件同时成立,一定要用and,而不是默认逻辑。
8.3 漏报的常见原因
漏报通常是因为请求构造不完整。比如目标接口需要带Cookie才能返回漏洞特征,但模板没有带上Cookie;又比如目标接口接受POST参数,但模板用GET发请求。遇到这种情况,先用Burp Suite手动访问目标,把完整的请求流程录下来,再对照着修改模板。
还有一种情况是提取器写错。extractors里的正则表达式如果匹配不到任何内容,变量就是空的,后续请求引用了空变量,自然得不到预期结果。调试时可以先用一个简单的word匹配器把响应内容输出出来,确认提取表达式写的对不对。
8.4 超时与QPS控制
批量执行模板时,如果目标响应慢,或者请求频率过高,会导致引擎报错甚至触发目标防护。注意调整引擎的超时参数和最大并发数。写模板时可以稍微保守一点,尤其是涉及登录、上传这类消耗资源较多的接口,控制在较低的并发水平更稳妥。
8.5 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 模板语法校验失败 | 缩进错误或冒号后缺空格 | 用支持YAML语法高亮的编辑器矫正格式 |
| 始终匹配成功 | word条件过短或condition逻辑错误 | 使用更长的特征字符串,检查and/or配置 |
| 始终匹配失败 | 请求方法错误或路径不对 | 用抓包工具对照实际请求流程 |
| 后续请求引用空变量 | 提取器正则写错 | 输出响应正文确认提取规则 |
| 目标响应超时 | 并发过高或请求头不完整 | 调低并发,使用真实浏览器UA头 |
| 模板在其他工具上不兼容 | 用了目标引擎不支持的特殊语法 | 参考目标引擎文档,替换为通用写法 |
9. 模板编写规范与经验分享
9.1 做好命名和注释
模板多了之后,命名混乱是最大的噩梦。建议一套固定的命名规范:类型前缀加上漏洞编号,比如cve-2021-xxxx、oa-login-bypass。在description里写清楚检测目标和适用版本,在关键请求旁用#写注释,方便自己一个月后还能快速看懂。
9.2 复用和整理自己的模板库
模板写到一定数量后,建议按目标类型或漏洞类型建目录。常见的分类维度有:按设备厂商(某厂商路由器、某厂商防火墙)、按服务器类型(Tomcat、WebLogic)、按应用类型(CMS、OA系统、邮箱系统)。目录结构清晰与否,直接决定了你后续能不能快速找到想要的规则。
9.3 跟随目标版本更新维护
漏洞检测规则有一个和漏洞利用共同的问题:版本更新会让旧特征失效。在模板里记录目标的适用版本范围,一旦发现目标更新后检测失效,要及时更新匹配特征。同时留意安全社区里其他人分享的同类模板,看到更可靠的匹配逻辑时可以吸收进来。
9.4 安全使用边界
写模板、调模板、跑模板,整个过程都应该在授权范围内进行。没有授权就去批量检测公网资产,性质完全不一样。做漏洞检测是为了发现和修复问题,不是制造问题。对自己负责,对目标负责,这个底线要守住。
我在实际使用中慢慢摸索出一个习惯:每次写新模板前,先翻一遍自己的模板库里有没有相似案例。很多看起来全新的检测需求,最后都能从老模板里找到七八成能复用的结构。YAML模板的好处就在这里,改改路径、换换特征、调调逻辑,一个新漏洞的检测规则就出来了。掌握这套写法之后,你会发现做批量检测的效率和以前完全不是一个量级。