如何远程利用PHP绕过Filter以及WAF规则

 

一、前言

在最近的3篇文章中,我主要关注的是如何绕过WAF规则集,最终获得远程命令执行权限。在本文中,我将与大家分享如何利用PHP绕过过滤器(filter)、输入限制以及WAF规则,最终实现远程代码执行。通常我在写这类文章时,总有人问我:“是否真的有人会写出存在问题的代码?”,这些提问者通常不是渗透测试人员。为避免再次被提问,这里我给出统一回答:是的,这种情况的确存在!(大家可以搜索一下[1][2]

在各种测试场景中,我准备使用2个存在漏洞的PHP脚本进行测试。第一个脚本如下所示。该脚本非常简单直白,主要用来复现远程代码执行漏洞场景(实际环境中我们可能需要经过一番努力才能获得该条件):

图1. 第1个PHP脚本

显然,上述代码中第6行非常危险。代码第三行尝试拦截诸如systemexec或者passthru之类的函数(PHP中还有许多函数可以执行系统命令,但这里我们先重点关注这3个函数)。这个脚本运行在部署了CloudFlare WAF的Web服务器上(像往常一样,我使用的是CloudFlare WAF,这是比较简单又广为人知的一个解决方案,但并不意味着CloudFlare WAF不安全。其他WAF或多或少也有相同问题)。第二个脚本则处于ModSecurity OWASP CRS3保护之下。

 

二、尝试读取/etc/passwd

首先我尝试使用system()读取/etc/passwd,具体请求为/cfwaf.php?code=system(“cat /etc/passwd”);

图2. CloudFlare WAF会拦截我的第一次尝试

如上图所示,CloudFlare会拦截我的请求(可能是因为存在/etc/passwd特征),然而如果大家之前读过我关于未初始化变量的上一篇文章,就知道我们可以使用类似cat /etc$u/passwd之类的方法轻松绕过这个限制。

图3. 绕过CloudFlare WAF,但输入过滤机制拦截了我们的请求

CloudFlare WAF已被成功绕过,但脚本拦截了我们的请求,因为我们尝试使用system函数。那么是否存在一种语法,可以让我们在不使用“system”字符串的情况下使用system函数?我们来阅读下PHP官方文档中关于字符串的描述

 

三、PHP字符串的转义表示法

  • 以八进制表示的\[0–7]{1,3}转义字符会自动适配byte(如"\400" == “\000”
  • 以十六进制的\x[0–9A-Fa-f]{1,2}转义字符表示法(如“\x41"
  • 以Unicode表示的\u{[0–9A-Fa-f]+}字符,会输出为UTF-8字符串(自PHP 7.0.0引入该功能)

并非所有人都知道PHP中可以使用各种语法来表示字符串,再配合上“PHP可变函数(Variable function)”后,我们就拥有能绕过filter以及waf规则的一把瑞士军刀。

 

四、PHP可变函数

PHP支持可变函数这种概念。这意味着如果一个变量名后面跟着圆括号,那么PHP将寻找与变量值同名的函数,并尝试执行该函数。除此之外,可变函数还可以用于实现回调、函数表等其他使用场景。

这意味着类似$var(args);“string”(args);的语法实际上与function(args);等效。如果我们能使用变量或者字符串来调用函数,那么我们就可以在函数名中使用转义字符。如下所示:

第3种语法是以十六进制表示的转义字符组合,PHP会将其转换成“system”字符串,然后使用"ls"作为参数调用system函数。现在再试一下存在漏洞的脚本:

图5. 绕过用户输入过滤

这种技术并不适用于所有PHP函数,可变函数不能用于诸如echoprintunset()isset()empty()include以及require等语言结构,用户需要使用自己的封装函数,才能以可变函数方式使用这些结构。

 

五、改进用户输入过滤

如果我在存在漏洞的脚本中排除类似双引号以及单引号之类的符号,结果会如何?我们是否可以在不使用双引号的情况下绕过代码限制?来试一下:

图6. 在$_GET[code]中排除"'符号

根据代码第3行,现在该脚本在$_GET[code]查询参数中禁止使用"'符号,应该能拦截之前我使用的payload:

图7. 现在脚本禁止使用"

幸运的是,在PHP中我们不一定需要引号来表示字符串。PHP支持我们声明元素的类型,比如$a = (string)foo;,在这种情况下,$a就包含字符串"foo",此外,如果不显示声明类型,那么PHP会将圆括号内的数据当成字符串来处理:

在这种情况下,我们有两种方法可以绕过新的过滤器:第一种是使用类似(system)(ls);之类的语法,但在code参数中我们不能使用”system”字符串,因此我们可以通过拼接字符串(如(sy.(st).em)(ls);)来完成该任务。第二种是使用$_GET变量。如果我发送类似?a=system&b=ls&code=$_GET[a]($_GET[b]);之类的请求,那么$_GET[a]就会被替代为字符串”system”,并且$_GET[b]会被替换为字符串”ls”,最终我可以绕过所有过滤器!

现在我们来试一下第一个payload:(sy.(st).em)(whoami);

图8. 成功绕过WAF及过滤器

试一下第二个payload:?a=system&b=cat+/etc&c=/passwd&code=$_GET[a]($_GET[b].$_GET[c]);

图9. 成功绕过WAF及过滤器

这里我们还可以使用其他技巧,比如我们可以在函数名和参数内插入注释(这种方法在绕过某些WAF规则集方面非常有用,这些规则集会拦截特定的PHP函数名)。以下语法都是有效语法:

 

六、get_defined_functions

这个PHP函数会返回一个多维数组,其中包含已定义的所有函数列表,包括内部函数及用户定义的函数。我们可以通过$arr[“internal”]访问内部函数,通过$arr[“user”]访问用户定义的函数,如下所示:

这种方法也可以在不使用函数名的情况下使用system函数。如果我们grep查找“system”,就可以发现该函数的索引值,然后利用该索引值调用system函数来执行代码:

图10. 1077 = system

显然,这种方法也能绕过CloudFlare WAF及脚本过滤器:

图11. 使用get_defined_functions绕过限制

 

七、字符数组

我们可以将PHP中的每个字符串当成一组字符来使用(基本上与Python相同),并且我们可以使用$string[2]或者$string[-3]语法来引用单个字符。这种方法也有可能绕过基于PHP函数名的防护规则。比如,我们可以使用$a=”elmsty/ “;这个字符串构造出system(“ls /tmp”);语句。

如果我们足够幸运,就可以在脚本文件名中找到我们所需的所有字符。利用这种方法,我们可以使用类似(__FILE__)[2]之类的语句获取我们所需的所有字符:

 

八、OWASP CRS3

部署OWASP CRS3后,我们面临的形式更加严峻。首先,利用前文介绍的技术,我们只能绕过Paranoia Level 1,这的确有点出乎意料,因为Paranoia Level 1只是CRS3规则中的一部分子集,并且这一级的功能是用来避免出现误报情况。在Paranoia Level 2形式更加艰难,因为此时部署了942430规则:“Restricted SQL Character Anomaly Detection (args): # of special characters exceeded”。这里我能做的只是执行不带参数的单条命令,如lswhoami等,不能执行在CloudFlare WAF防护环境中可用的system(“cat /etc/passwd”)命令:

 

九、先前研究成果

Web Application Firewall Evasion Techniques #1

https://medium.com/secjuice/waf-evasion-techniques-718026d693d8

Web Application Firewall Evasion Techniques #2

https://medium.com/secjuice/web-application-firewall-waf-evasion-techniques-2-125995f3e7b0

Web Application Firewall Evasion Techniques #3

https://www.secjuice.com/web-application-firewall-waf-evasion/

(完)