前言
翻了好久前写过的关于php复杂语法变量解析的文章,发现很多地方存在问题。因此又查阅文档重新理解了一遍该语法,谈谈个人的理解,并记录在此。
题目简析
一道很早之前的题目,代码(简化):
$str= @(string)$_GET['str'];
eval('$str="'.addslashes($str).'";');
通过eval执行php代码获得flag,addslashes函数将字符串中的特殊字符转义,“{}”在双引号中可以标记变量边界来解析,利用该方式来达到代码执行的目的。
eg:
通过payload:${assert($_GET[cmd])}}即可获取shell,也可以利用system函数来执行命令读取flag等。
变量解析之复杂语法
当字符串用双引号或heredoc结构定义时,其中的变量将会被解析。共有两种语法规则,一种简单规则,一种复杂规则,这里讨论复杂规则。
复杂规则语法的显著标记是用花括号包围的表达式。任何具有string表达的标量变量,数组单元或对象属性都可使用此语法,表达方式{$var_name}或${var_name}。
根据php官方文档,这里提示在PHP5以后可以使用{$}来调用函数、方法等。看下面的例子:
不难理解,{${getname()}} => {$s1ye}
,以函数返回值命名变量。这里我在函数中加入了echo "s1ye";
,可以发现先执行了getname函数并输出了“s1ye”,接着才执行了echo(优先级)。
为了方便理解payload,利用以下代码进行测试:
构造了一个类似phpinfo的简化函数,利用变量解析在双引号中的复杂语法,先执行了test函数输出了“just for test”并返回true,可以看到,返回一条警告并且变量a的值是空的。
为什么Notice为”Undefined variable: 1”呢?返回TRUE,变量应该为$TRUE的。前面官方文档说过了,$ + string的变量会被解析,而TRUE是bool类型,并且是个常量,当返回TRUE并命名变量时php解析器将TRUE转换为了string类型。
由于没有该变量导致赋值给a变量时返回为空(并不能用数字开头来命名变量)。
理解payload
直接利用payload{${phpinfo()}}或者${${phpinfo()}}
会返回phpinfo信息,但都会报错。
上面已经讲过了报错的原因,这里就很好理解了。接下来逐步的来分析payload成功执行的过程:
eval函数将字符串当作php代码执行,因此,通过图中代码清晰可见相当于定义了str变量,赋值为一个字符串”{${phpinfo()}}”。
$str = "{${phpinfo()}}"
,花括号定义了变量的边界,因此该条语句先执行括号中内容,获取函数返回值,并以返回值的string命名变量再赋值给str变量(同上面分析的test函数一样)。
到了这里只要修改”{${?}}”中的?为其他php代码就可以达到写文件读文件或者getshell等操作了,只需要注意addslashes函数即可。
思考
先看一下原题代码
eval('$str="'.addslashes($str).'";');
在双引号中可以利用花括号定义变量边界,调用函数等,如果修改为单引号包裹addslashes函数,还能执行代码了吗。
$str = @(string)$_GET['str'];
eval("$str='".addslashes($str)."';");
很容易就发现虽然addslashes函数部分内容被单引号包裹,但是变量str却变成了双引号包裹。这说明还是可以被利用的,直接尝试上面的payload:
发现报错,变量同样被双引号包裹(其实并没有被双引号包裹),这里却报错了。但是payload2${${phpinfo()}}
却可以正常执行。
其实这里只要稍微思考一下就可以理解了,用两个例子来解释:
可以看到payload1不能执行成功的原因就是并没有被双引号包裹,所以外层标记变量边界的花括号无用。而payload${phpinfo()}/${${phpinfo()}}
(无论加几个”${}”都无差,只不过报错更多而已)相当于$a=’string’,a==(phpinfo()的返回值string形式),即执行函数后利用返回值定义变量并赋值,因此可以执行成功。
总结
整体看下来感觉还是很绕的,总结就是遇到问题除了百度谷歌,要多认真读官方文档, 其实文档已经写得很清楚了。