十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从漏洞原理到自动化检测:编写高质量Nuclei模板实战指南

从漏洞原理到自动化检测:编写高质量Nuclei模板实战指南 最近挖了不少漏洞感触最深的一件事是很多人用 Nuclei 只会跑现成模板真遇到一个刚公开的漏洞想自己写检测规则却不知道从哪里下手。网上讲模板语法的文章不少但基本都是照着官方文档翻译一遍看完了还是不会写。这篇文章我不想重复那些基础语法而是从漏洞原理开始带你走一遍完整的分析、设计、编码、验证流程把一份能稳定复现、误报率低的 Nuclei 模板是怎么炼成的讲清楚。无论你是刚入门的安全工程师还是已经写过几个模板但总觉得哪里不太对劲的选手这篇应该都能给你一些启发。编写高质量的 Nuclei 模板从漏洞原理到自动化检测脚本1. 先理解 Nuclei 模板它不只是一段 YAML而是一套检测逻辑1.1 模板驱动扫描器的工作原理Nuclei 本质上是 ProjectDiscovery 开源的一个规则驱动扫描器这句话的重点不在扫描器而在规则驱动。传统扫描器把检测逻辑写死在代码里加一个新的检测项就要等版本更新Nuclei 反过来了扫描引擎是固定的所有检测规则都外置成一份份 YAML 模板引擎只负责加载模板、发请求、比对响应、输出结果。换句话说模板是剧本引擎是演员演员不关心演什么戏你给它什么剧本它就怎么演。这个设计带来的好处是巨大的写模板不需要编译代码不需要懂 Go只要会写 YAML、理解漏洞原理就能在几分钟内给一个新漏洞产出检测规则。同一个模板在本地跑是单目标扫描放到大规模资产收集流程里就是批量巡检一致性完全由模板保证不会因为人不同而出现他测出来了、我测不出来的情况。我在实际使用中最大的感受是Nuclei 模板特别像测试用例——你定义输入、定义期望输出引擎替你执行并判断是否命中。但问题也出在这很多人把它当成简单的发个请求、匹配个关键字工具忽略了模板背后其实是漏洞判定逻辑的工程化表达。写模板之前脑子里必须先把怎么证明这个漏洞存在这个问题想清楚。1.2 一份模板的骨架结构拆解先看一个最基础的模板长什么样这里用 Nacos 未授权访问做例子后面会详细讲id: nacos-unauth-access info: name: Nacos Unauthorized Access author: your-name severity: high description: Nacos API endpoints accessible without authentication, may leak configuration data. tags: nacos,unauthorized,exposure http: - method: GET path: - {{BaseURL}}/nacos/v1/auth/users?pageNo1pageSize9 matchers-condition: and matchers: - type: word part: body words: - username - pageItems condition: and - type: status status: - 200一个模板的结构可以拆成四大块id模板的唯一标识命名习惯是vendor-product-vuln比如nacos-unauth-access。这个 id 会在输出结果、去重、模板管理中反复用到尽量有意义。info元信息块描述漏洞名称、作者、危害等级、参考链接、标签。这部分表面上只是注释但实际影响模板的分类和过滤。协议块这是核心检测逻辑所在常见有http、dns、file、ssl、headless等。绝大多数 Web 漏洞都走http。匹配与提取在协议块内部用matchers定义什么特征算命中用extractors定义要从响应里提取什么数据。1.3 从漏洞原理到检测特征的思维模型我见过太多人写模板的第一步就是查 YAML 语法这是本末倒置。正确顺序应该是先回答四个问题再动笔写文件。第一这个漏洞为什么存在是参数过滤不严、配置错误、还是加密算法强度不够第二攻击者怎么触发它需要什么请求、什么参数、什么前置条件第三漏洞被触发后服务端返回什么特征是状态码变化、响应体内容变化、响应时间变化还是后续行为变化第四这个特征怎么和其他正常业务区分开如果随便一个页面都有这个特征那这个特征就是无效的。这四个问题想清楚模板的核心逻辑就有了。剩下的 YAML 语法只是把逻辑翻译成机器能理解的格式。比如检测一个 SQL 注入很多人一上来就匹配sql syntax error这类报错关键字结果目标站点是个报错信息很规范的框架根本匹配不到。反过来如果你先分析漏洞原理发现该框架在参数中拼接了注入 payload 后错误信息会包含传入语句的关键片段那么匹配规则就应该是响应中是否回显了我们传入的独特字符串而不是响应中是否包含通用报错关键字。这个思维模型是高质量模板和低质量模板的分水岭。2. 漏洞原理分析选目标、找特征、定判定逻辑2.1 案例一Nacos 未授权访问漏洞的原理与攻击面Nacos 是很多微服务架构里都在用的注册中心和配置中心它在默认部署状态下部分接口没有做严格的权限校验导致攻击者可以直接通过 HTTP 请求读取用户列表、配置信息、服务实例列表等敏感数据。这个漏洞的根源主要是两个一是 Nacos 的部分 API 设计上默认开放二是在很多生产环境中运维人员把 Nacos 直接暴露在公网又没有开启服务端身份验证。从攻击面来看最常被利用的几个接口有/nacos/v1/auth/users?pageNo1pageSize9未授权获取用户列表返回 JSON 中包含username、password字段密码是加密后的哈希。/nacos/v1/cs/configs?dataIdgrouptenant未授权拉取配置列表可能包含数据库连接串、密钥等。/nacos/v1/ns/instance/list?serviceNamexxx查看服务实例列表泄露内网拓扑。这个漏洞之所以适合做教学案例是因为它的检测特征非常明确而且这些接口在未授权状态下和已授权状态下的返回差异明显。我测试过不少线上 Nacos只要没开鉴权/nacos/v1/auth/users基本都会返回 200 和 JSON 数据而开启鉴权后返回的是 403 或者类似user not found的提示。2.2 从原理推导检测请求与响应特征现在我们用前面的思维模型来推导检测逻辑。漏洞为什么存在Nacos 默认不强制鉴权。攻击者怎么触发直接发一个 GET 请求。服务端返回什么特征200 状态码JSON 响应体里出现username、pageItems等结构字段。怎么区分正常业务正常业务页面不会返回这种特定结构的 JSON。所以检测请求就定为GET /nacos/v1/auth/users?pageNo1pageSize9 HTTP/1.1 Host: target检测特征定为状态码 200且响应体包含username和pageItems。我特意没有用password作为必须匹配的关键字原因有两个一是新版本 Nacos 对返回字段做了调整某些版本不直接返回password字段二是password这个单词太通用容易在页面脚本里误命中。用username加pageItems这个组合既能覆盖多个 Nacos 版本又能显著降低误报。这里有个细节很容易踩坑如果你直接把响应体匹配totalCount在低版本 Nacos 里也是可行的但在某个版本之后接口返回结构变了totalCount字段被移除模板就漏报了。所以写模板时字段选择要尽量选那些跨版本稳定存在的特征而不是当前版本恰好存在的特征。2.3 案例二SSTI 模板注入的检测思路再看一个稍微绕一点的例子服务端模板注入SSTI。这个漏洞的原理是用户输入被直接拼接进了模板引擎的渲染表达式常见的模板引擎有 Jinja2、Freemarker、Velocity、Twig 等。攻击者输入{{7*7}}如果服务端真的执行了表达式响应里就会渲染出49。检测的思路是发送一个带模板表达式的特殊 payload然后检查响应中是否回显了表达式计算结果。这里的关键不是用什么语法配 matcher而是选什么 payload 才能既有效又不出误报。很多人刚学写 SSTI 检测模板时会直接匹配49这在我看来是误报率最高的写法——目标页面里只要正常出现数字 49就被误报成漏洞。更好的做法是使用带字符串的数学表达式比如{{7*7}}在 Python 的 Jinja2 里结果是7777777这个字符串基本不可能出现在正常业务响应中。检测特征匹配7777777误报率就降下来了。另外要考虑不同模板引擎的语法差异。{{...}}是 Jinja2 和 Twig 的语法Freemarker 用的是${7*7}而 Velocity 的语法是#set($x7*7)。所以要覆盖多种模板引擎通常需要准备一组 payload 来分别探测。这就是后面要讲的 payloads 数组和 fuzzing 的用武之地。2.4 特征设计的三条原则综合上面两个案例我把检测特征的设计原则总结成三条写模板的时候可以拿来当自查清单第一是稳定性。选择的特征必须在目标系统的多数版本、多数部署方式下都稳定存在不要依赖某个版本独有的字段。如果条件允许最好能快速搜索一下该漏洞在其他扫描器或公开 PoC 里的响应判断逻辑看看别人用了哪些稳定特征。第二是特异性。特征要尽量只有这个漏洞才会出现避免和正常业务内容撞车。通用报错信息、通用数字、通用英文单词都属于低特异性特征能不碰就不碰。可以用组合条件多个关键字、状态码、响应头来提升特异性。第三是可解释性。模板是给别人看的也是给自己三个月后复盘看的。matcher 的命名、info 里的 description、选择的特征都要让人一眼看懂为什么这样匹配。我自己 review 过的模板里最痛苦的就是那种 matcher 写得天马行空、毫无注释的模板出了问题根本没法排查。3. 把检测逻辑写成 Nuclei 模板从小白到进阶的落地过程3.1 第一步写好模板头部信息别小看元数据模板头部就是id和info块很多新手觉得这部分随便写写就行其实不然。id是模板的唯一标识如果命名不规范在模板库大规模使用时会出现重复和混乱。我的习惯是遵循组件-漏洞类型的格式比如nacos-unauth-access、jenkins-script-console-rce。info块里的severity危害等级要根据漏洞实际危害来定不要为了看起来严重一律填critical。Nacos 这种可能泄露配置和凭据的未授权访问我一般定high如果能直接 RCE 才考虑critical。tags也值得花点心思它决定了后续扫描时能不能通过-tags参数快速筛选比如tags: nacos,unauthorized,exposure就比tags: nacos好用得多。reference字段建议填上漏洞公告或相关 CVE 的官方链接方便后来人回溯漏洞原始信息。我见过不少模板info块只写了个名字和 severity漏洞详情全靠猜这种模板在正式的漏洞管理流程里非常不友好。3.2 核心请求块method、path、body、headers 的选择逻辑http块的核心是请求定义。最基础的是指定请求方法和路径但有几个细节值得展开说说。路径支持数组。比如 Nacos 未授权访问我不会只测一个用户列表接口而是同时测几个接口path: - {{BaseURL}}/nacos/v1/auth/users?pageNo1pageSize9 - {{BaseURL}}/nacos/v1/cs/configs?dataIdgrouptenant引擎会按顺序发这几个请求任何一个命中都算检测成功。数组的好处是可以在一个模板里覆盖多个相关接口提升检测面。{{BaseURL}}这个变量指的是命令行传入的目标地址包括协议和端口几乎所有 Web 模板都要用到。如果你写的是绝对地址的 API不依赖传入目标域名那可以用self-contained: true让模板完全忽略输入的 BaseURL直接请求模板里写死的 URL。这个选项在对接某些固定服务时很有用但多数场景下还是推荐用{{BaseURL}}保证模板的可移植性。对于 POST 请求body和headers是搭配着来的method: POST path: - {{BaseURL}}/api/check headers: Content-Type: application/json body: {name:{{payload}}}这个例子里body用单引号包裹整个 JSON 字符串外层再写{{payload}}就会让 payloads 里的测试值逐个替换进去。这里很容易犯的错是 YAML 引号嵌套混乱导致模板加载报错。我后面会专门讲编码转义的问题。3.3 匹配器与提取器的正确打开方式matchers是模板的灵魂它的本质是对响应内容做断言。Nuclei 支持status、word、regex、binary、size、dsl等类型每一种都有不同的适用场景。status最常用于配合其他条件单独使用很容易误报因为 200 状态码太普通了。word是最常用的文本匹配适合响应体里的固定关键字。regex适合内容格式不固定但有规律的情况比如匹配一个 20 位十六进制字符串的 token。dsl是最灵活的方式可以写复杂的表达式比如matchers: - type: dsl dsl: - contains(body, root) status_code 200这个表达式同时判断了响应体和状态码相当于把多个条件写进了一个 matcher。我个人在写复杂判定逻辑时比较偏好 dsl因为可读性比matchers-condition: and配多个 matcher 更直观。extractors则是负责从响应里把数据捞出来最常见的用途是配合多请求联动把第一步请求中获取的 token、session 等传给第二步请求。extractors: - type: json name: token json: - .data.token internal: true这里的internal: true表示提取结果只在模板内部使用不会输出到扫描结果里。这种设计非常巧妙可以让模板像流水线一样一步响应成为下一步的输入。3.4 多请求联动用 extractor 把上一步结果传给下一步有些漏洞不是一个请求就能测出来的最常见的是要先登录拿 token再携带 token 去访问敏感接口。Nuclei 的http块天然支持多个请求按顺序执行通过extractors和变量引用就能串起来。我写过一个用来检测某系统越权访问的模板逻辑是http: - method: POST path: {{BaseURL}}/login headers: Content-Type: application/json body: {username:admin,password:admin} extractors: - type: json name: token json: - .data.access_token internal: true - method: GET path: {{BaseURL}}/api/admin/users headers: Authorization: Bearer {{token}} matchers-condition: and matchers: - type: word part: body words: - username - type: status status: - 200两个请求会按顺序执行第一个请求提取access_token存入模板变量token第二个请求通过{{token}}引用。整个过程对使用者完全透明输出结果里只会看到最终的命中情况。用这个功能时有三个细节需要特别注意。第一要确保第一个请求真的返回了能用的 token否则第二个请求会带一个空值过去结果不准确。我通常会在第一个请求后面加一个简单 matcher比如要求 200 和包含token字段不满足就提前终止。第二internal: true千万别漏漏了这个 token 会出现在最终扫描结果里造成凭据泄露。第三如果 token 出现在响应头里而不是 JSON 里可以用type: kval来提取比如extractors: - type: kval kval: - set_cookie3.5 用 payloads 做批量输入测试payloads机制是 Nuclei 模板里实现批量输入的核心它通过模板变量展开生成多个请求。拿 SSTI 检测举例payloads: ssti: - {{7*7}} - ${7*7} - % 7*7 % - #{7*7} http: - method: GET path: - {{BaseURL}}/search?q{{ssti}} matchers: - type: word part: body words: - 7777777 - 49这里定义了一个名为ssti的 payload 数组路径里的{{ssti}}会被逐个替换成数组里的每个值引擎会生成 4 个请求分别发送。配合 matcher 里的多个关键字就能覆盖不同模板引擎的回显结果。payloads 还有一个很方便的用法是通过attack: clusterbomb做多变量笛卡尔积组合不过这会显著增加请求数量。我之前写一个登录接口的弱口令检测模板时用了两组 payload用户名和密码结果请求数瞬间从几十涨到几千。所以用 payloads 一定要有数量意识测试前先估算请求总量别把一个扫描模板变成 DoS 工具。另外payload 里如果包含{{或}}这样的特殊字符在 YAML 里不会触发 Nuclei 的模板变量解析引擎只会把它当作普通字符串替换进去。这一点很多刚接触的人容易担心实际测试下来是完全可用的因为 Nuclei 变量替换发生在 YAML 解析之后而 payload 里的花括号只是数据。4. 模板质量保障测试、调试、防误报的实战经验4.1 本地调试别上来就跑全量先用 debug 模式看响应写好的模板直接对真实目标跑这是最危险也最没效率的做法。我现在的习惯是先用一个本地起好的测试环境验证没有测试环境就先用-debug模式在单个目标上跑务求看清每一个请求的完整内容和响应原文。调试最常用的几个命令参数-debug打印每个请求的完整请求头和响应头这是定位为什么没匹配到的第一工具。-v输出更多过程信息比-debug轻量适合大致观察运行情况。-stats实时显示已发送请求数、匹配数等统计信息适合观察模板是否产生大量请求。-silent只输出命中结果适合已经稳定的模板跑批。我调试时最常见的操作是nuclei -t template.yaml -u http://127.0.0.1:8080 -debug看请求是否如期发出、响应体是否包含预期关键字、matcher 的 part 是否指对了位置。大部分匹配不上的问题在-debug输出里一眼就能看出来——要么是响应体里关键字确实不存在要么是part写错了位置关键字明明在 header 里却去 match body。4.2 误报与漏报如何平衡宁可少报也不要满屏假阳性模板的误报和漏报是一对矛盾关键在于场景取舍。在做漏洞管理流程时误报的代价不仅仅是多处理几条告警更严重的是会让安全团队对扫描结果失去信任最终发现真正漏洞时反而被忽视。所以我的原则是宁可漏报也要把误报压到极低。实际操作中有几个有效的降误报手段。第一是组合条件不要只靠一个关键字加一个状态码、加一个路径上下文特征都行。Nacos 那个模板就是一个例子username加pageItems加 200三重确认后才算命中。第二是选择高特异性 payloadSSTI 检测用7777777而不是49就是这个道理。第三是通过matchers-condition: and把多个维度的特征用与组合起来让误报的概率成倍下降。漏报的问题相对不那么致命但也不能太离谱。常见漏报原因是单点特征闭门造车只测了一个接口、一个版本。缓解办法是在写模板时多看几个可信参考源把不同版本的响应差异考虑进去。比如 Nacos 用户列表接口在某个版本返回结构变化了如果我没看别人提交的模板可能就会漏掉那个版本。4.3 模板性能与扫描节奏控制模板写得再准如果发出去的请求量太大实际操作中也会被目标设备的防护策略拦掉或者直接把目标打挂了。控制请求节奏有几个关键参数stop-at-first-match: true第一个请求命中后就停止后续请求这在 path 是数组时能显著减少请求数。max-redirects限制重定向跟随次数避免陷入重定向循环。read-all: false只读取部分响应体对超大响应体可以明显减轻内存压力。pre-condition在发送正式请求前先发一个探测请求只有探测通过才继续可以过滤掉大量无关目标。pre-condition是我觉得很多模板忽视的一个好功能。比如检测某个管理后台的漏洞时先做一个轻量的路径探测如果目标根本没有这个后台就没必要发出完整攻击载荷。4.4 编码、转义与大小写最容易翻车的隐藏细节最后这部分是实战中踩坑最多的几乎每个模板都会遇到。YAML 里的特殊字符处理是第一个坑。比如 matcher 的关键字里包含单引号必须注意 YAML 的引号嵌套。我写 payload 和 matcher 时习惯统一用双引号包裹 YAML 字符串然后在需要的时候用反斜杠转义内部引号。第二个坑是 URL 编码。Nuclei 模板里如果要把查询参数写成 URL 编码后的形式可以直接在 path 里写编码后的字符串也可以使用 helper 函数path: - {{BaseURL}}/search?q{{url_encode(../../etc/passwd)}}这里的url_encode是 Nuclei 内置的 helper 函数会在运行时对括号里的内容做 URL 编码。类似的 helper 还有base64、md5、sha1、randstr写模板时遇到需要动态生成的字符串优先用这些内置函数而不是自己在 payload 里写死。第三个坑是大小写问题。HTTP 响应体的大小写是不可控的如果你匹配的关键字是username但目标返回的是UserName那就漏报了。拿不准的时候可以在响应体里搜索前先用to_lower之类的转换或者准备多个大小写变体。Nuclei 的wordmatcher 是大小写敏感的这点要特别留意。第四个坑是响应体里的转义字符。JSON 响应里的中文和特殊字符经常被转义成 Unicode 形式比如\u5bc6如果你匹配中文关键字会匹配不上。我的做法是尽量匹配 ASCII 的字段名避免匹配中文内容。5. 常见问题与排查技巧实录5.1 高频问题速查表我把平时写模板和帮同事排查时遇到的高频问题整理成一个速查表照着查能省不少时间。症状可能原因解决办法模板加载报 YAML 解析错误缩进不正确、引号嵌套错误用 VSCode 的 YAML 插件检查确认列表项对齐请求已发出但总是 no matchpart指向了错误位置-debug查看响应确认关键字在 body 还是 header关键字匹配不上且响应为 JSONJSON 内容被转义匹配 ASCII 字段名避免匹配中文或转义字符提取的变量为空正则或 JSON 路径写错先用curl拿到响应单独验证提取表达式请求数量过多导致扫描很慢payloads 展开成笛卡尔积加stop-at-first-match精简 payload 数组目标出现大量 403 被拦截请求特征明显触发了防护模拟真实浏览器 UA合理调节并发和延时5.2 版本兼容与模板迁移的踩坑记录Nuclei 版本迭代比较快模板语法也有过几次不兼容升级。我印象最深的是早期http块用request关键字后来统一改成直接用method、path平铺的方式。如果你在 GitHub 上看到一些比较老的模板直接往新版本 Nuclei 里扔会报错。建议长期维护模板库的话指定 Nuclei 版本再配合 CI 做模板校验。我自己的做法是写了一个简单的模板校验脚本在提交模板前跑一遍nuclei -validate -t template.yaml确保语法兼容。以后更新 Nuclei 大版本时先跑一遍全量校验能筛出很多兼容性问题。另外官方模板库nuclei-templates的内容质量参差不齐引用的时候不能盲目相信。我见过一些社区模板为了凑热度匹配逻辑写得很草率误报率极高。用别人的模板没问题但要在自己的环境里测试验证过再上线尤其是关键业务的目标环境。5.3 从单模板到自动化漏扫闭环模板写出来不是终点真正体现价值的是把它纳入自动化漏洞管理流程。我目前的使用方式是这样组织的本地维护一个模板目录按资产类型或业务线划分比如web/、api/、cloud/每个模板都经过测试和 review 后才进入正式目录。扫描任务通过配置文件指定模板目录和目标域名输出格式选 JSON方便后续脚本做汇总和去重。nuclei -t ./custom-templates -l targets.txt -json -o results.json再配合定时任务每天晚上自动跑一轮核心资产巡检命中结果推送到团队协作工具。这里有一个经验自动化扫描不需要把模板跑得越全越好而是要分层。第一层用轻量指纹模板确认目标用了什么组件、什么版本第二层基于指纹结果只跑相关的漏洞模板这样可以大幅减少无效请求和误报。整个过程其实不需要太复杂的平台一个定时任务加一个消息推送就能跑起来重点是模板质量要过关。最后再分享一点个人经验写模板这几年我最深的体会是模板写得好的前提永远是对漏洞本身的理解足够深。语法只是载体逻辑才是核心。同样的漏洞有人写的模板只匹配一个静态关键字换台服务器就失灵有人写的模板能从响应结构到状态码到字段内容层层校验跑了大半年依然稳定。差别不在写 YAML 的速度而在分析漏洞时愿不愿意多想几步。最后分享一个对新手最有用的小技巧写模板前先不要碰编辑器用 curl 或 Postman 把检测请求手工打一遍看真实响应内容把响应保存下来。写 matcher 的时候对着真实响应写而不是凭想象写。这一步虽然简单但能帮你避掉至少一半的调试时间。
返回列表