文章总结: 本文为2026春节逆向题目解题报告,涵盖Windows和Android多道题目。作者通过IDA、jadx等工具逆向分析程序,识别XOR、AES等加密算法,提取加密数据并编写解密脚本成功获取各题Flag。文章详细记录了脱壳、资源提取、密钥派生等步骤,展示了实战经验。主要结论是所有题目均已解决,关键发现涉及多种打包和加密技术。建议掌握逆向工具与加密算法分析技巧。 综合评分: 88 文章分类: CTF,逆向分析,实战经验,安全工具,移动安全
设置为密码输入框的默认文本,是一个提示而非密码本身。
加密算法逆向
文件格式
flag.png.encrypted 的格式如下:
复制代码 隐藏代码
偏移 大小 内容
0x00 4 魔数 "CM26" (0x36324D43)
0x04 4 明文 CRC-32 校验值 (~CRC32)
0x08 8 IV 初始化向量
0x10 N 密文数据(8 字节对齐,含填充)
实际数据:
复制代码 隐藏代码
00000000:434d3236 a245848df5697360 01cb35bc CM26.E...is`..5.
00000010:fbdd1b922befe32310eb814c4504 4895 ....+..#...LE.H.
...
密钥派生
函数 0x140008720 实现完整的解密流程:
① CRC-64 初始化 (0x140008640)
使用多项式 0xC96C5795D7870F42(CRC-64/ECMA-182)构建 256 项查找表。初始值 0xFFFFFFFFFFFFFFFF。
② CRC-64 更新 (0x140008500)
依次处理 14 字节盐值 "52pojie_2026_\x00" 和用户输入的密码:
复制代码 隐藏代码
crc64_update(ctx, "52pojie_2026_\x00", 14); // 盐值
crc64_update(ctx, password, strlen(password)); // 密码
③ CRC-64 终结 (0x140008580)
对 CRC 值追加 4 字节计数器并取反:
复制代码 隐藏代码
crc = crc64_update(crc, counter_bytes, 4);
hash = ~crc;
最终得到 64 位密钥哈希。
分组加密
块密码 (0x140008080),8 字节分组,CBC 模式:
密钥变换(每个分组前执行):
复制代码 隐藏代码
key = ROL(key, 3); // 循环左移 3 位
for (int i = 0; i < 8; i++) {
high_byte = key >> 56;
key = (key << 8) | AES_SBOX[high_byte]; // S-Box 字节替换
}
解密公式:
复制代码 隐藏代码
明文[i] = 密文[i] ⊕ 变换后密钥[i] ⊕ IV[i]
IV 更新(CBC 模式):
复制代码 隐藏代码
IV = 当前密文块 // 用于下一块解密
完整性校验
CRC-32 验证 (0x140008480 + 0x1400082E0),多项式 0xEDB88320:
- 解密过程中对所有明文计算 CRC-32
- 终结时比对
~CRC32与文件头偏移0x04处存储的值 - 匹配则密码正确
已知明文攻击
题目提示”暴力枚举不可取”,引导我们使用已知明文攻击。
原理
PNG 文件固定以 8 字节魔数开头:
复制代码 隐藏代码
8950 4E 470D 0A 1A 0A
这恰好等于加密的分组大小(8 字节)。由解密公式:
复制代码 隐藏代码
明文 = 密文 ⊕ 密钥 ⊕ IV
可以反推:
复制代码 隐藏代码
密钥 = 密文 ⊕ IV ⊕ 明文
计算
从加密文件中提取:
复制代码 隐藏代码
IV(偏移 0x08): F5 69736001 CB 35 BC
第一密文块(偏移 0x10): FB DD1B92 2B EF E3 23
PNG 文件头(已知明文): 8950 4E 470D 0A 1A 0A
逐字节异或:
| 字节 | 密文 | IV | 明文 | 密钥 = CT⊕IV⊕PT | | — | — | — | — | — | | 0 | FB | F5 | 89 | 87 | | 1 | DD | 69 | 50 | E4 | | 2 | 1B | 73 | 4E | 26 | | 3 | 92 | 60 | 47 | B5 | | 4 | 2B | 01 | 0D | 27 | | 5 | EF | CB | 0A | 2E | | 6 | E3 | 35 | 1A | CC | | 7 | 23 | BC | 0A | 95 |
得到变换后密钥(LE 字节序):87 E4 26 B5 27 2E CC 95
对应 64 位整数:0x95CC2E27B526E487
逆推原始 CRC-64 哈希
① 逆 AES S-Box
变换后密钥的每个字节都经过了 S-Box 替换。对每个字节查 AES 逆 S-Box 表,还原 ROL(hash, 3) 的值:
复制代码 隐藏代码
inv_sbox = [0] * 256
for i inrange(256):
inv_sbox[AES_SBOX[i]] = i
original_bytes = bytes([inv_sbox[b] for b in tk_bytes])
# rol3_value = 0xAD27C33DD223AEEA
② 逆循环左移
复制代码 隐藏代码
crc64_hash = ROR(rol3_value, 3)
# crc64_hash = 0x55A4F867BA4475DD
完整解密
利用恢复的密钥,无需知道密码即可解密全部数据:
复制代码 隐藏代码
import struct
key = 0x55A4F867BA4475DD # 恢复的 CRC-64 哈希
current_iv = enc[8:16] # 8 字节 IV
ciphertext = enc[16:] # 密文数据
plaintext = bytearray()
for block_start inrange(0, len(ciphertext), 8):
block = ciphertext[block_start:block_start+8]
saved_ct = list(block)
# 密钥变换:ROL 3 + S-Box×8
key = ((key << 3) | (key >> 61)) & 0xFFFFFFFFFFFFFFFF
for _ inrange(8):
high = (key >> 56) & 0xFF
key = ((key << 8) & 0xFFFFFFFFFFFFFFFF) | AES_SBOX[high]
key_bytes = struct.pack('<Q', key)
# 解密:明文 = 密文 ⊕ 密钥 ⊕ IV
for i inrange(8):
plaintext.append(block[i] ^ key_bytes[i] ^ current_iv[i])
current_iv = bytes(saved_ct) # CBC: IV 更新为当前密文块
# 移除 PKCS 填充
pad = plaintext[-1]
plaintext = plaintext[:-pad]
运行结果:
复制代码 隐藏代码
首 8 字节: 89504e470d0a1a0a ← 合法 PNG 文件头!
填充字节: 2(移除 2 字节填充)
解密数据: 350 字节
成功解密出 flag.png,图像为像素字体 “HEX_ME”(产品标志)。
提取 Flag
解密后检查 PNG 的元数据,发现 tEXt 块中隐藏了 flag:
复制代码 隐藏代码
Chunk: tEXt (Software)
→ Pixilart (Pixel Art Editor)
Chunk: tEXt (Comment)
→ Post-processed with a hex editor ← 提示 flag 是用 hex 编辑器写入的
Chunk: tEXt (Description)
→ flag{EncrypTIoN_Is_haRd_52p0jIE_2o26_m62Tc4uj78maAq1C} ← FLAG!
Comment 字段的 “Post-processed with a hex editor” 与题目名 “HEX_ME” 相呼应,暗示 flag 是通过十六进制编辑器写入 PNG 元数据的。
Flag
复制代码 隐藏代码
flag{EncrypTIoN_Is_haRd_52p0jIE_2o26_m62Tc4uj78maAq1C}
Android 中级题(二)
分析过后发现是在so层,分析困难度较大,放弃了。
Web 中级题
先打开玩了一下,发现验证码巨长,我又听不太懂说的啥,感觉是个大坑,换一条路。
WASM 逆向分析
工具链
使用 wabt 工具链进行 WASM 反编译:
复制代码 隐藏代码
# 提取 WASM 二进制
node -e "eval(require('fs').readFileSync('assets/verify.wasm.js','utf8')); \
require('fs').writeFileSync('verify.wasm', Buffer.from(getWasmBuffer()))"
# 反编译为 WAT 文本格式(23,344 行)
wasm2wat verify.wasm -o verify.wat
# 反编译为 C 代码(363,903 行)
wasm2c verify.wasm -o verify.c
gen 函数对应 w2c_verify_gen_0(verify.c 第 357396 行),是整个题目的核心。
gen() 函数内部流程
第一步:获取 17 字节随机数
复制代码 隐藏代码
// verify.c:357436-357439
var_i0 = var_l3 + 80; // 缓冲区地址
var_i1 = 17; // 长度
w2c_wbg_getRandomValues(instance, var_i0, var_i1);
// random[0..16] 存储在 var_l3+80 到 var_l3+96
通过 hook 确认:getRandomValues 在整个 gen() 过程中仅被调用一次,请求恰好 17 字节。
第二步:构建 37 字节种子缓冲区
复制代码 隐藏代码
// verify.c:357778-357836
var_l9 = malloc(37, 1); // 分配 37 字节
// bytes 0-3: random[0..3] XOR uid 各字节(字节序反转)
var_l9[0] = random[0] ^ (uid & 0xFF); // XOR uid 低 8 位
var_l9[1] = random[1] ^ ((uid >> 8) & 0xFF); // XOR uid 次低 8 位
var_l9[2] = random[2] ^ ((uid >> 16) & 0xFF); // XOR uid 次高 8 位
var_l9[3] = random[3] ^ ((uid >> 24) & 0xFF); // XOR uid 高 8 位
// bytes 4-11: random[0..7] 直接复制
memcpy(var_l9 + 4, random, 8);
// bytes 12-19: random[8..15] 直接复制
memcpy(var_l9 + 12, random + 8, 8);
// byte 20: random[16] 直接复制
var_l9[20] = random[16];
至此前 21 字节已填充完毕。
第三步:HMAC-SHA256 计算填充 bytes 21-36
复制代码 隐藏代码
// verify.c:357849-358037 (简化)
// 初始化 SHA-256 状态(HMAC 内层)
// 从地址 1295967 加载 14 字节初始值
memcpy(buffer, mem+1295967, 14);
// 将前 64 字节数据与 0x36 XOR(HMAC ipad)
for (int i = 0; i < 64; i += 4) {
buffer[i] ^= 0x36;
buffer[i+1] ^= 0x36;
buffer[i+2] ^= 0x36;
buffer[i+3] ^= 0x36;
}
// SHA-256 压缩(内层 hash = SHA256(ipad || seed_buffer))
w2c_verify_f9(instance, sha_state, buffer, 1);
// 将同一 buffer 再与 0x6A XOR(0x36 ^ 0x6A = 0x5C = HMAC opad)
for (int i = 0; i < 64; i += 4) {
buffer[i] ^= 0x6A; // 0x36 ^ 0x6A = 0x5C
buffer[i+1] ^= 0x6A;
buffer[i+2] ^= 0x6A;
buffer[i+3] ^= 0x6A;
}
// SHA-256 压缩(外层 hash = SHA256(opad || inner_hash))
w2c_verify_f9(instance, sha_state2, buffer, 1);
这实际上是一个 HMAC-SHA256 计算。最终取前 16 字节写入 var_l9[21..36],完成 37 字节种子缓冲区。
第四步:Base64-like 编码(37 字节 → 50 字符)
复制代码 隐藏代码
// verify.c:358799-358933 (简化伪代码)
char *code_array = malloc(200, 4); // 50 个 u32 元素
int char_idx = 0;
int bit_acc = 0; // 位累加器
int bit_pos = 0; // 当前位位置
byte *ptr = seed; // 指向 37 字节种子
for (int byte_idx = 0; byte_idx < 37; byte_idx++) {
// 将当前字节加入位累加器
bit_acc = *ptr | (bit_acc << 8);
// 循环提取 6-bit 块
while (bit_pos + 2 > 5) {
int idx = (bit_acc >> (bit_pos + 2)) & 0x3F;
char ch = charset[idx]; // charset 在内存地址 1295903
code_array[char_idx++] = ch;
bit_pos -= 6;
}
bit_pos += 8;
ptr++;
}
// 处理最后剩余的 bits
if (remaining_bits > 0) {
int idx = (last_byte << (6 - remaining_bits)) & 0x3F;
code_array[char_idx++] = charset[idx];
}
字符表位于 WASM 线性内存地址 1295903:
复制代码 隐藏代码
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!
编码过程类似 Base64:每 6 bit 索引一个字符,37 字节 = 296 bit → 296/6 = 49.33 → 上取整得到 50 个字符。
第五步:计算验证哈希
WASM 内部对编码后的 50 字符验证码做 8230 次 SHA-256 迭代,生成 64 位 hex 字符串作为 hash h。
第六步:生成 TTS 语音
WASM 内部包含一个 TTS 引擎,将 50 个字符逐个合成为中文语音朗读(根据 voice 参数选择方言)。生成 24kHz 16bit 单声道 WAV 音频,约 38 秒。
语音朗读方式:
- 普通话(
c):区分大小写,如”大写A”、”小写b”、”数字3″、”问号”、”叹号” - 粤语(
y):粤语发音 - 中文美式(
e):美式英语发音
第七步:清零所有中间数据
最关键的保护措施:在构建返回对象之前,WASM 将以下数据全部清零/释放:
- 37 字节种子缓冲区
- 50 元素字符数组
- 所有 SHA-256 中间状态
- HMAC 计算缓冲区
这意味着验证码明文从未通过任何 JS 导入函数传出——它完全在 WASM 内部生成、编码、哈希、清零,最终只有 hash 和音频数据通过 JS 导入的 set 函数设置到返回对象上。
既然数据在 WASM 内部被生成后立即销毁,那就修改 WASM 字节码,在数据被销毁之前将其”泄漏”到 JS 层。
WASM 二进制热补丁 + 双通道执行
补丁 1:消除 XOR 混淆
WASM 内部在将字符传递给后续处理前,会对每个字符的 ASCII 值做 XOR 0xCC。这会将正常 ASCII 字符变成高位字节,无法作为可识别字符传出。
在 WASM 字节码中搜索所有 i32.const 204; i32.xor 指令序列并替换为 i32.const 0; i32.xor(等价于 no-op):
复制代码 隐藏代码
原始字节码:41 CC 0173 → i32.const204 (0xCC); i32.xor
补丁字节码:41800073 → i32.const0; i32.xor
复制代码 隐藏代码
const xorPattern = Buffer.from([0x41, 0xCC, 0x01, 0x73]);
const xorReplace = Buffer.from([0x41, 0x80, 0x00, 0x73]);
let p = 0;
while (p < patchedWasmBuffer.length - 4) {
const idx = patchedWasmBuffer.indexOf(xorPattern, p);
if (idx === -1) break;
xorReplace.copy(patchedWasmBuffer, idx);
p = idx + 1;
}
补丁 2:重定向函数调用
在 WASM 内部,每个编码后的字符会通过 call 19(一个内部字符串构建函数 w2c_verify_f19)进行处理。我们将偏移量 33810 处的 call 19 重定向为 call 4——而 call 4 恰好是 getRandomValues 的导入函数。
这样,每个字符的 ASCII 值会作为 len 参数传递到我们 hook 的 getRandomValues 回调中:
复制代码 隐藏代码
WASM 偏移 33810:
原始字节码:1013 → call19 (内部函数)
补丁字节码:1004 → call4 (getRandomValues 导入)
复制代码 隐藏代码
patchedWasmBuffer[33810] = 0x04;
JS 层 hook:捕获字符
Hook getRandomValues 导入函数。第一次调用是真正的随机数请求(17 字节),后续调用是我们补丁注入的字符泄漏——len 参数就是字符的 ASCII 值:
复制代码 隐藏代码
imports.wbg.__wbg_getRandomValues = function(arg0, arg1) {
randomCallCount++;
if (randomCallCount === 1) {
// 第一次:真正的 getRandomValues,注入固定随机数
arr.set(fixedRandom);
} else {
// 后续:补丁注入的字符泄漏
// arg1 (len) = 字符的 ASCII 值
if (len >= 33 && len <= 122 && CHARSET.includes(String.fromCharCode(len))) {
capturedChars.push(String.fromCharCode(len));
}
}
};
双通道执行策略
由于补丁修改了 XOR 常量,打补丁的 WASM 生成的 hash 与原始 WASM 不同。因此需要两次执行:
复制代码 隐藏代码
Pass 1(打补丁的 WASM)
├── 注入固定 17 字节随机数
├── XOR 被置零 → 字符保持明文 ASCII
├── call19 → call4 → 每个字符通过 getRandomValues 泄漏
└── 输出:捕获 50 个明文字符
Pass 2(原始未修改的 WASM)
├── 注入相同的 17 字节随机数
├── 正常执行所有逻辑
└── 输出:正确的 SHA-256 hash
验证:SHA-256(捕获的 code, 8230 次) == Pass 2 的 hash
└── Match: true ✓
关键在于:相同的随机数 → 相同的验证码。voice 参数只影响语音合成,不影响验证码内容和 hash。
最终验证
复制代码 隐藏代码
let current = Buffer.from(code, 'utf-8');
for (let i = 0; i < 0x2026; i++) {
current = crypto.createHash('sha256').update(current).digest();
}
const computedHash = current.toString('hex');
console.log(`Match: ${computedHash === originalHash}`);
// → Match: true
脚本
复制代码 隐藏代码
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
constCHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!";
// 加载 WASM 二进制
globalThis.window = globalThis;
globalThis.atob = (b64) =>Buffer.from(b64, 'base64').toString('binary');
eval(fs.readFileSync('assets/verify.wasm.js', 'utf-8'));
const originalWasmBuffer = Buffer.from(globalThis.getWasmBuffer());
// 生成固定随机数(确保两次执行使用相同的种子)
const fixedRandom = crypto.randomBytes(17);
// ====== 创建打补丁的 WASM ======
const patchedWasmBuffer = Buffer.from(originalWasmBuffer);
// 补丁 1: XOR 0xCC → XOR 0x00
const xorPattern = Buffer.from([0x41, 0xCC, 0x01, 0x73]);
const xorReplace = Buffer.from([0x41, 0x80, 0x00, 0x73]);
let p = 0;
while (p < patchedWasmBuffer.length - 4) {
const idx = patchedWasmBuffer.indexOf(xorPattern, p);
if (idx === -1) break;
xorReplace.copy(patchedWasmBuffer, idx);
p = idx + 1;
}
// 补丁 2: call 19 → call 4 (偏移 33810)
patchedWasmBuffer[33810] = 0x04;
// ====== 通用 WASM 执行函数 ======
functionrunWasm(wasmBuf, onGetRandom, onSetProp) {
// 构建所有 wasm-bindgen 所需的导入函数
// 其中 getRandomValues 和 set 被 hook
const imports = { wbg: { /* ... 所有导入 ... */ } };
constmodule = newWebAssembly.Module(wasmBuf);
const instance = newWebAssembly.Instance(module, imports);
instance.exports.__wbindgen_start();
// 调用 gen(734555, "c")
const ret = instance.exports.gen(734555, voicePtr, voiceLen);
return result;
}
// ====== Pass 1: 打补丁版本捕获字符 ======
const capturedChars = [];
runWasm(patchedWasmBuffer, (arr, ptr, len) => {
if (firstCall) {
arr.set(fixedRandom); // 注入固定随机数
} else {
capturedChars.push(String.fromCharCode(len)); // 捕获字符
}
}, ...);
// ====== Pass 2: 原始版本获取正确 hash ======
let originalHash = null;
runWasm(originalWasmBuffer, (arr) => {
arr.set(fixedRandom); // 注入相同的随机数
}, (obj, key, val) => {
if (key === 'h') originalHash = val;
});
// ====== 验证 ======
const code = capturedChars.join(''); // 50 字符
let current = Buffer.from(code, 'utf-8');
for (let i = 0; i < 0x2026; i++) {
current = crypto.createHash('sha256').update(current).digest();
}
console.log(`Match: ${current.toString('hex') === originalHash}`); // true
console.log(`FLAG: flag{${code}}`);
运行结果
复制代码 隐藏代码
$ node extract_code.js
WASM buffer loaded: 4001674bytes
Fixed randombytes: 23a43e72bb29cfb8fa3130be2e48a8d298
=== STEP 1: Run patched WASM to capture characters ===
[PATCHED] getRandomValues: wrote fixed random23a43e72bb29cfb8fa3130be2e48a8d298
[PATCHED] Hash: 80e2e0c9d0238767c8b9b8ba8f9338003977e6bd8be1c1360bb6d89392e745f6
[PATCHED] Captured 50 chars: "Eje1CIoKpNk7kC?4?JeWVI5iQnkyjOF7XN7T408Q4HDtu3Vv6a"
=== STEP 2: Run original WASM with same randombytes ===
[ORIGINAL] getRandomValues: wrote fixed random
[ORIGINAL] Hash: 49cc46a21f1b91c242181dbdb226736343420d952198c85bbb96d3557f231b31
=== STEP 3: Verify ===
Captured code (50 chars): "Eje1CIoKpNk7kC?4?JeWVI5iQnkyjOF7XN7T408Q4HDtu3Vv6a"
Original hash: 49cc46a21f1b91c242181dbdb226736343420d952198c85bbb96d3557f231b31
Computed hash: 49cc46a21f1b91c242181dbdb226736343420d952198c85bbb96d3557f231b31
* Match:true
========================================
CODE:Eje1CIoKpNk7kC?4?JeWVI5iQnkyjOF7XN7T408Q4HDtu3Vv6a
FLAG:flag{Eje1CIoKpNk7kC?4?JeWVI5iQnkyjOF7XN7T408Q4HDtu3Vv6a}
========================================
Windows 高级题
啥都分析不出来,放弃了。
MCP 中级题
这一题是直接交给AI来做的,用Claude跑了有一个小时,后面给出了一个python脚本,直接运行就得到了正确的 flag。
复制代码 隐藏代码
import subprocess
import json
import hashlib
MCP_URL = "https://9863968daeea51ea32f40575dd41dd113.52pojie.cn:3000/mcp"
PASSPHRASE = "玄霄密令"
definit_mcp():
"""初始化 MCP 传输层会话,从响应头提取 Mcp-Session-Id"""
data = json.dumps({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "solver", "version": "1.0"}
}
}).encode()
result = subprocess.run(
["curl", "-s", "-D", "-", "-X", "POST", MCP_URL,
"-H", "Content-Type: application/json",
"-H", "Accept: application/json, text/event-stream",
"--data-binary", "@-"],
input=data, capture_output=True, timeout=30
)
for line in result.stdout.split(b"\n"):
ifb"mcp-session-id:"in line.lower():
return line.split(b":", 1)[1].strip().decode()
raise RuntimeError("无法获取 MCP Session ID,请检查服务器连接")
deftool_call(mcp_sid, tool_name, args):
"""发起工具调用,解析 SSE 响应"""
data = json.dumps({
"jsonrpc": "2.0", "id": 1,
"method": "tools/call",
"params": {"name": tool_name, "arguments": args}
}, ensure_ascii=False).encode("utf-8")
result = subprocess.run(
["curl", "-s", "-X", "POST", MCP_URL,
"-H", "Content-Type: application/json",
"-H", "Accept: application/json, text/event-stream",
"-H", f"Mcp-Session-Id: {mcp_sid}",
"--data-binary", "@-"],
input=data, capture_output=True, timeout=30
)
for line in result.stdout.split(b"\n"):
if line.startswith(b"data:"):
try:
return json.loads(line[5:].strip().decode("utf-8"))
except Exception:
pass
raise RuntimeError(f"工具调用 {tool_name} 无响应,原始输出:{result.stdout[:200]}")
defget_content(r):
"""从工具调用响应中提取 JSON 内容"""
try:
return json.loads(r["result"]["content"][0]["text"])
except Exception:
return {}
defmain():
print("=" * 50)
print("新岁数字异界 MCP CTF 自动解题脚本")
print("=" * 50)
# 第一步:建立 MCP 传输层会话
print("\n[1/9] 初始化 MCP 会话...")
mcp_sid = init_mcp()
print(f" MCP Session ID: {mcp_sid}")
# 第二步:建立应用层会话
print("[2/9] 启动应用层会话...")
r = tool_call(mcp_sid, "start_session", {})
app_sess = get_content(r).get("session_id", "")
ifnot app_sess:
raise RuntimeError("无法获取应用会话 ID")
print(f" App Session ID: {app_sess}")
# 第三步:读取所有公开文档(必须,否则审阅会返回 ctf_clues_required)
print("[3/9] 读取公开文档(解锁审阅权限)...")
docs_resp = get_content(tool_call(mcp_sid, "list_documents", {"session_id": app_sess}))
ifisinstance(docs_resp, list):
for doc in docs_resp:
doc_id = doc.get("id", "")
tool_call(mcp_sid, "get_document", {"session_id": app_sess, "doc_id": doc_id})
print(f" 已读取: {doc_id}")
# 第四步:对最终目标文档发起访问,触发拒绝,获取 audit_log_id
print("[4/9] 触发拒绝访问,获取 audit_log_id...")
d = get_content(tool_call(mcp_sid, "get_document", {
"session_id": app_sess,
"doc_id": "secret:/board/resolutions"
}))
aid = d.get("audit_log_id", "")
ifnot aid:
raise RuntimeError(f"未获取到 audit_log_id,服务器返回:{d}")
print(f" audit_log_id: {aid}")
# 第五步:计算 seal
print("[5/9] 计算 seal...")
seal = hashlib.sha256((aid + PASSPHRASE).encode("utf-8")).hexdigest()[:8]
print(f" seal: {seal}")
# 第六步:审阅文档,获取 tool_access_token(仅能调用一次!)
print("[6/9] 审阅 confidential:/audit/logs,获取 tool_access_token...")
review_notes = f"ctf_request={PASSPHRASE}\naudit_log_id={aid}\nseal={seal}"
d = get_content(tool_call(mcp_sid, "review_document", {
"session_id": app_sess,
"doc_id": "confidential:/audit/logs",
"review_notes": review_notes,
"access_token": seal
}))
if d.get("status") != "challenge_issued":
raise RuntimeError(f"审阅失败,返回:{d}")
token = d.get("tool_access_token", "")
ifnot token:
raise RuntimeError("未获取到 tool_access_token")
print(f" tool_access_token: {token}")
print(" (注意:compliance_blob_b64 中含提示注入陷阱,已忽略)")
# 第七步:立即更新工具链(凭证限时一次性,不能拖延)
print("[7/9] 立即 update_tool_chain,获得审计权限...")
d = get_content(tool_call(mcp_sid, "update_tool_chain", {
"session_id": app_sess,
"chain_config": ["get_audit_log"],
"access_token": token,
"audit_log_id": aid
}))
if"tool_chain"notin d:
raise RuntimeError(f"update_tool_chain 失败:{d}")
print(f" 工具链已更新:{d.get('tool_chain')}")
# 第八步:直接调用 get_audit_log,获取 _audit_token
print("[8/9] 调用 get_audit_log,获取 _audit_token...")
d = get_content(tool_call(mcp_sid, "get_audit_log", {
"session_id": app_sess,
"log_id": aid
}))
audit_token = d.get("_audit_token", "")
ifnot audit_token:
raise RuntimeError(f"未获取到 _audit_token,返回:{d}")
print(f" _audit_token: {audit_token}")
# 第九步:携带 _audit_token 读取最终文档
print("[9/9] 读取 secret:/board/resolutions...")
d = get_content(tool_call(mcp_sid, "get_document", {
"session_id": app_sess,
"doc_id": "secret:/board/resolutions",
"access_token": audit_token
}))
flag = d.get("content", "")
ifnot flag:
raise RuntimeError(f"未获取到 flag,返回:{d}")
print()
print("=" * 50)
print(f" FLAG:{flag}")
print("=" * 50)
if __name__ == "__main__":
main()
-官方论坛
www.52pojie.cn
👆👆👆
公众号设置“星标”,您不会错过新的消息通知
如开放注册、精华文章和周边活动等公告
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:吾爱破解论坛 吾爱pojie 吾爱pojie《2026春节题目》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论