这是一道考验利用无参函数达到命令执行的题目。
1 |
|
源码分析
先来看这段正则:
1 | /[^\W]+\((?R)?\)/ |
它会找到所有符合 “函数名()” 的部分,并替换成空字符串。
所以我们传入的必须是无参函数。
php内置无参函数:
- end() - 读取数组最后一个元素
- localeconv() – 函数返回一个包含本地数字及货币格式信息的数组 第一个是.
- pos() – 返回数组中的当前单元, 默认取第一个值
- current() - 读取数组的第一个元素
- next – 将内部指针指向数组下一个元素并输出
- scandir() – 扫描目录
- array_reverse() – 翻转数组
- array_flip() - 键名与数组值对调
- readfile()
- array_rand() - 随机读取键名
- var_dump() - 输出数组,可以用print_r替代
- file_get_contents() - 读取文件内容,show_source,highlight_file echo 可代替
- get_defined_vars() - 返回由所有已定义变量所组成的数组
- phpinfo() -显示php详细内容
分析几个常见 wp
参数祸水东引
1 | ?code=eval(end(current(get_defined_vars())));&cmd=system('more f*'); |
get_defined_vars()
返回当前作用域中定义的所有变量
1 | [ |
所以这返回的是个大数组,第一层是变量名(_GET
, _POST
等)
相似的函数还有getallheaders()
current()
我们调用 current(get_defined_vars())
,意思是:
从这个大数组中,取“第一个元素的值”
也就是:
1 | current([ |
结果是 **$_GET**
这个数组。
我们现在已经拿到了**$_GET**
这个数组
所以end($_GET)
就是取**$_GET**
这个数组的最后一个,也就是我们自己发过去的命令。
最后由于题目中的 eval 是执行 code 字符串,我们还需要在外面包一个 eval 来执行我们发过去的命令。
拼接出目标文件
1 | ?code=readfile(next(array_reverse(scandir(pos(localeconv()))))); |
localeconv()
这个函数返回一个数组,包含“本地数字/货币格式”的设置,
我们来看看 PHP 官方文档里 localeconv()
返回的数组顺序:
1 | php |
所以第一个是 "decimal_point" => "."
那这个结果在所有机器上都是一样的吗?
几乎 100% 都是一样的,因为:
默认系统 locale 设置是英文或 UTF-8 编码环境(如 en_US.UTF-8
)
在这些默认环境下,小数点符号几乎一定是 "."
pos()
pos() 是 current() 的别名。
它会返回数组中第一个元素的值
所以这一步的意思是:
从 localeconv()
这个数组中,取出第一个值。
最终结果是:一个字符串 "."
这就等价于当前目录。
scandir()
作用:扫描目录,返回这个目录下的所有文件和文件夹名,作为数组
比如你的目录有这些文件:
index.php
flag
那:
scandir(‘.’);
返回的就是:
[“.”, “..”, “flag”, “index.php”]
默认就多了这两个 "."
和 ".."
。 代表当前目录和上层目录。
这上面两个函数基本是固定的,怎么读到目前文件可以随意组合,比如说:
1 | /?code=readfile(array_rand(array_flip(scandir(pos(localeconv()))))); |
**array_flip()和array_reverse()**作用相同。
array_rand()
从数组中随机抽一个 key,这里因为只有两个文件所以很容易就抽中了 flag.
array_reverse()
作用: 把数组的顺序反过来
原本:
1 | [".", "..", "flag", "index.php"] |
变成:
1 | ["index.php", "flag", "..", "."] |
next()
作用:next() 是移动数组指针并返回下一个值。但在一串函数里直接用它,其实意思就是:返回数组的第二个元素(因为第一是 current)
这一步是选中你想读的那个文件名,尽量猜到 flag
如果第二个元素也没有,那么再嵌套一层 next,直到读到为止。
或者我们也可以用:
array_values(scandir(pos(localeconv())))[2]
来直接访问某个具体下标
readfile()
读取文件内容并直接输出到浏览器
从构造者的脑回路角度来说:
我不能用字符串!那我就用函数构造出 **/flag**
的路径
我不能传参数,那我就层层嵌套函数调用
我不能写 $_GET['cmd']
,那就不要用外部变量
我想用 readfile()
输出文件,那就构造文件名
我用 scandir('.')
找到所有文件
我通过 array_reverse + next
去“猜” flag 所在位置
请求头
1 | eval(next(getallheaders())); |
PHP 内置函数,用于获取 HTTP 请求头,返回一个关联数组。
比如你发送请求时带了:
1 | X-My-Header: system('ls'); |
那么:
1 | getallheaders() = [ |
flag 在源代码里,需要右键查看。