无参数rce
无参rce,就是说在无法传入参数的情况下,仅仅依靠传入没有参数的函数套娃就可以达到命令执行的效果,这在ctf中也算是一个比较常见的考点,接下来就来详细总结总结它的利用姿势
核心代码
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) { eval($_GET['code']); }
这段代码的核心就是只允许函数而不允许函数中的参数,就是说传进去的值是一个字符串接一个()
,那么这个字符串就会被替换为空,如果替换后只剩下;
,那么这段代码就会被eval
执行。而且因为这个正则表达式是递归调用的,所以说像a(b(c()));
第一次匹配后就还剩
下a(b());
,第二次匹配后就还剩a();
,第三次匹配后就还剩;
了,所以说这一串a(b(c())),
就会被eval
执行,但相反,像a(b('111'));
这种存在参数的就不行,因为无论正则匹配多少次它的参数总是存在的。那假如遇到这种情况,我们就只能使用没有参数的php函数,
下面就来具体介绍一下:
1、getallheaders()
这个函数的作用是获取http
所有的头部信息,也就是headers
,然后我们可以用var_dump
把它打印出来,但这个有个限制条件就是必须在apache
的环境下可以使用,其它环境都是用不了的,我们到burp中去做演示,测试代码如下:
<?php highlight_file(__FILE__); if(isset($_GET['code'])){ if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) { eval($_GET['code']);} else die('nonono');} else echo('please input code'); ?>
可以看到,所有的头部信息都已经作为了一个数组打印了出来,在实际的运用中,我们肯定不需要这么多条,不然它到底执行哪一条呢?所以我们需要选择一条出来然后就执行它,这里就需要用到php
中操纵数组的函数了,这里常见的是利用end()
函数取出最后一位,这里的效果如下图所示,而且它只会以字符串的形式取出值而不会取出键,所以说键名随便取就行:
那我们把最前面的var_dump
改成eval
,不就可以执行phpinfo
了吗,换言之,就可以实现任意php代码的代码执行了,那在没有过滤的情况下执行命令也就轻而易举了,具体效果如下图所示:
2、get_defined_vars()
上面说到了,getallheaders()
是有局限性的,因为如果中间件不是apache
的话,它就用不了了,那我们就介绍一种更为普遍的方法get_defined_vars()
,这种方法其实和上面那种方法原理是差不多的:
可以看到,它并不是获取的headers
,而是获取的四个全局变量$_GET $_POST $_FILES $_COOKIE
,而它的返回值是一个二维数组,我们利用GET
方式传入的参数在第一个数组中。这里我们就需要先将二维数组转换为一维数组,这里我们用到current()
函数,这个函数的作用是返回数组中的当前单元,而它的默认是第一个单元,也就是我们GET方式传入的参数,我们可以看看实际效果:
这里可以看到成功输出了我们二维数组中的第一个数据,也就是将GET的数据全部输出了出来,相当于它就已经变成了一个一维数组了,那按照我们上面的方法,我们就可以利用end()
函数以字符串的形式取出最后的值,然后直接eval
执行就行了,这里和上面就是一样的了:
总结一下,这种方法和第一种方法几乎是一样的,就多了一步,就是利用current()
函数将二维数组转换为一维数组,如果大家还是不了解current()
函数的用法,可以接着往下看文章,会具体介绍的哦
这里还有一个专门针对$_FILES
下手的方法,可以参考这篇文章:https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE/
3、session_id()
这种方法和前面的也差不太多,这种方法简单来说就是把恶意代码写到COOKIE
的PHPSESSID
中,然后利用session_id()
这个函数去读取它,返回一个字符串,然后我们就可以用eval
去直接执行了,这里有一点要注意的就是session_id()
要开启session
才能用,所以说要先session_start()
,这里我们先试着把PHPSESSID
的值取出来:
直接出来就是字符串,那就非常完美,我们就不用去做任何的转换了,但这里要注意的是,PHPSESSIID
中只能有A-Z a-z 0-9
,-
,所以说我们要先将恶意代码16进制编码以后再插入进去,而在php中,将16进制转换为字符串的函数为hex2bin
那我们就可以开始构造了,首先把PHPSESSID
的值替换成这个,然后在前面把var_dump
换成eval
就可以成功执行了,如图:
成功出现phpinfo
,稳稳当当,这种方法我认为是最好的一种方法,很容易理解,只是记得要将恶意代码先16进制编码一下哦
4.php函数直接读取文件
上面我们一直在想办法在进行rce,但有的情况下确实无法进行rce时,我们就要想办法直接利用php函数完成对目录以及文件的操作, 接下来我们就来介绍这些函数:
1.localeconv
官方解释:localeconv() 函数返回一个包含本地数字及货币格式信息的数组。
这个函数其实之前我一直搞不懂它是干什么的,为什么在这里有用,但实践出真知,我们在测试代码中将localeconv()
的返回结果输出出来,这里很神奇的事就发生了,它返回的是一个二维数组,而它的第一位居然是一个点.
,那按照我们上面讲的,是可以利用current()
函数将这个点取出来的,但这个点有什么用呢?点代表的是当前目录!那就很好理解了,我们可以利用这个点完成遍历目录的操作!相当于就是linux
中的ls
,具体请看下图:
2.scandir
这个函数很好理解,就是列出目录中的文件和目录
3.current(pos)
这里首先声明,pos()
函数是current()
函数的别名,他们俩是完全一样的哈
这个函数我们前面已经用的很多了,它的作用就是输出数组中当前元素的值,只输出值而忽略掉键,默认是数组中的第一个值,如果要移动可以用下列方法进行移动:
4.chdir()
这个函数是用来跳目录的,有时想读的文件不在当前目录下就用这个来切换,因为scandir()
会将这个目录下的文件和目录都列出来,那么利用操作数组的函数将内部指针移到我们想要的目录上然后直接用chdir
切就好了,如果要向上跳就要构造chdir('..')
5.array_reverse()
将整个数组倒过来,有的时候当我们想读的文件比较靠后时,就可以用这个函数把它倒过来,就可以少用几个next()
6.highlight_file()
打印输出或者返回 filename 文件中语法高亮版本的代码,相当于就是用来读取文件的
例题解析——–GXYCTF 2019禁止套娃
这道题首先是一个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__); ?>
可以看出它是一个有过滤的无参rce,由于它过滤掉了et
,导致我们前两种的方法都用不了,而且它也过滤了hex bin
,第三种方法也不能像我们上面讲的一样先16进制编码了,而且我抓包以后都看不到PHPSESSID
的参数,估计第三种方法也用不了,但有了前面的铺垫,用第四种方法就可以很简单的解决了,首先遍历当前目录:
可以看到flag.php
是倒数第二个,那我们就把它反转一下,然后再用一个next()
就是flag.php
这个文件了
胜利就在眼前,直接highlight_file
读取这个文件就拿到flag了:
思路总结
scandir(current(localeconv()))是查看当前目录 加上array_reverse()是将数组反转,即Array([0]=>index.php[1]=>flag.php=>[2].git[3]=>..[4]=>.) 再加上next()表示内部指针指向数组的下一个元素,并输出,即指向flag.php highlight_file()打印输出或者返回 filename 文件中语法高亮版本的代码