免责声明:本文为个人技术学习与工程实践笔记,所涉操作仅应在获得授权的环境中进行。因不当使用造成的后果由使用者自行承担。
本次比赛赛题覆盖面较广,遗憾的是受经费限制无法前往线下参赛,因此放弃了线下参赛名额。
Misc
Easy! 23333!
使用 010 Editor 打开,发现 504B 位于文件末尾,编写脚本将其反转
1 | # encoding=utf-8 |
解压得到 Just Kidding!,使用 VSCode 打开可发现其中存在零宽字符,直接解码即可

flag{maybe_you_kn0w_the_Unicode_Steganography}
这是隐写
下载音频文件后,导入 Audacity 查看波形

推测为 LSB 隐写,将其交由 Audio-Steganography 处理可解出 PNG,Audio-Steganography 脚本如下
1 | import wave |
得到的 PNG 为

经谷歌以图搜图,发现该字体为 Webdings

逐一比对后得到 flag

flag{8d9ad0457c1a8b603978085b0bffcf93}
数字游戏
首先求解数独

在该网站上解开后,可得主对角线与次对角线数值,二者即为压缩包密码
654917276261618641

解开后得到 key.txt 和 flag.txt

key.txt 为 Nonogram
1 | [[7, 1, 1, 1, 1, 7], [1, 1, 2, 1, 1, 1, 1], [1, 3, 1, 2, 1, 2, 1, 3, 1], [1, 3, 1, 3, 1, 3, 1], [1, 3, 1, 2, 1, 1, 2, 1, 3, 1], [1, 1, 1, 1, 4, 1, 1], [7, 1, 1, 1, 1, 1, 7], [2], [1, 1, 1, 2, 4, 1, 2, 1], [2, 1, 1, 1, 2, 1, 2, 2, 1], [1, 2, 3, 6, 1, 2], [4, 1, 2, 7, 1, 1, 2], [1, 2, 3, 2, 1, 1], [1, 4, 3, 1, 3, 1, 1], [2, 2, 2, 2, 4, 1, 1], [3, 2, 2, 1, 1, 1, 1, 3], [1, 1, 2, 6, 1], [1, 4, 1, 2], [7, 1, 1, 2, 1, 3, 1], [1, 1, 3, 3, 3, 1], [1, 3, 1, 1, 7, 1], [1, 3, 1, 3, 5, 1], [1, 3, 1, 1, 1, 1, 1, 1], [1, 1, 2, 4, 2, 1, 2], [7, 3, 3, 1, 2, 1, 1]] |
按坐标轴匹配后可得一个二维码,扫描所得到的即为 flag.txt 的密码

二维码密码为Take1tEasy
flag{c6ebcf84bcd54bac0803086a4630f673}
你也很困惑吗?
得到 flag.zip,发现其文件头尾为 51 49 00 00 11 06,推测是将其与压缩包文件头 50 4B 03 04 14 00 01 00 进行异或

经尝试,推测为十六进制逐字节异或,编写脚本执行验证:每次循环中密钥值递增 1,并通过取模运算将其限制在 0-255 的范围内


1 | f = open('flag.zip', 'rb') |
按此逻辑实现如下
1 | f = open('flag.zip', 'rb') |
得到

解压得到 rox.png 与 xor.png,其中 rox.png 为伪造的 flag

将 xor.png 与 rox.png 进行 RGB 通道异或
1 | from PIL import Image |
随后将得到的图片进行盲水印解码,得到 flag

得到


flag{AC4E331C-A2D0-CA2C-93D6-B9E22F19A373}
Re
Junk
去除永恒跳转花指令



直接使用 RC4 解密即可
flag{jUnkc0dE_C0oO00o0oo0ode}
Pwn
Easyguess
本题为最基础的伪随机数题目
1 | from pwn import* |
timemaster


可以通过 %lf 泄漏金丝雀(canary)
所泄漏的值为浮点数,需要转换为 bytes
取得金丝雀之后即可直接构造 ROP,属于常规的 ret2libc
1 | from pwn import * |
Guess

本题利用 pthread 创建了一个新线程,并且存在 gets 溢出。由于 pthread 创建线程的特殊性,新的栈帧空间会落在 libc 地址上,且距离 TLS 结构体仅有几百个字节;而 canary 的校验依赖于 TLS 结构体上的 canary,因此通过溢出覆盖即可绕过 canary。又因程序开启了 PIE,还需要泄漏 ELF 基址。经动态调试发现,在提示 Enter your guess 时若输入字母导致 scanf 接收到空值,即可泄漏 ELF 基地址
1 | from pwn import* |
Crypto
easybog
构建一个 lattice,定义块矩阵并插入 p 与 c,随后直接利用格基规约算法求解最短向量,可以发现第一行的相反向量即为解

1 | p = 85766816683407427477074053090759168259205489535331001301483049660772943816017 |
Web
expr
题目提供了 jar 包,通过代码审计可以定位到一处反序列化入口。该题并没有给出可直接使用的完整利用链,但其中引入了 fastjson 组件,且给出的 User 类包含以下关键内容:

可以发现这里存在一个 OGNL 表达式注入点,但被 filter 函数过滤,且过滤项较多。由此可以梳理出两个要点:一是需要让 getResult 方法被调用,二是表达式最终会被交给 OGNL 求值。
fastjson 在输出对象时会经 toString 路径触发 getter 调用,而 User#getResult 正是可能被触发的 getter 之一,这正是「序列化触发 → 表达式求值」这类链路的成因。该链路的构造方式也不复杂:把持有表达式串的 User 对象放入 JsonArray,再将其反射写入 BadAttributeValueExpException 的 val 字段,序列化后在反序列化阶段触发 toString 即可到达求值点。出于内容中性化考虑,此处不再列出可直接编译运行的生成代码。
题目对表达式设置了关键字黑名单:
1 | String[] BlackList = { |
该名单覆盖了引号、java、getRuntime、ProcessBuilder、start 等关键字。这也正好说明黑名单过滤的固有局限:它只能拦住名单中已列举的字面量,而字符串常量在运行时可以有多种等价构造方式,因此过滤只能提高构造成本,无法构成可靠的信任边界。
以本题为例,toChars() 可以把数字转换为字符,但该方法位于 java.lang.Character 类下,需要先取得该类;调试可知对字符串调用 charAt() 即可得到 java.lang.Character。

按这一思路可以逐字符拼出任意字符串:

配合聚合函数 concat 即可把各字符拼接为完整表达式,从而在不出现被过滤字面量的前提下表达同一语义:

由于双引号被过滤,构造时只能反复调用 true.toString().charAt(0).toChars(n)[0] 一类的写法:

IndexController 中存在以下代码,可见页面会把求值结果回显给请求方,表达式执行的结果因此可以被原样读出:

结合前述序列化链路,将构造好的序列化数据以 POST 方式提交到首页即可取得 flag,具体报文与完整 exp 不再列出。

防御启示
- 反序列化入口治理:避免用
ObjectInputStream处理不可信数据;确需反序列化时,使用 JEP 290 的ObjectInputFilter或组件自带的类白名单限制可还原的类型。BadAttributeValueExpException、TemplatesImpl、JdbcRowSetImpl等特征类名出现在流量或日志中时应触发告警。 - 表达式引擎隔离:OGNL、SpEL、Nashorn 等引擎不应接触用户可控字符串;确需动态求值时,用白名单限定可调用的方法与类,并在独立、最小权限的进程中执行。本题的字符拼接手法说明黑名单不是边界,白名单加沙箱才是。
- 组件治理:fastjson 一类反序列化组件应升级到仍在维护的版本,并按官方文档启用安全模式与 autoType 白名单;构建阶段应比对依赖清单,剔除业务并未使用的组件,缩小可被利用的类路径。
- 权限收敛:应用进程以非 root 账户运行,容器根文件系统只读并限制出站连接;flag 一类敏感文件不应位于应用进程可读路径中,避免表达式/命令执行直接转化为信息泄露。
- 二进制与编译期加固:本题二进制部分出现的可预测伪随机数(固定种子初始化的
srand/rand)、%lf等格式化字符串导致的金丝雀泄漏、缺少 PIE 等问题,在真实系统中同样会带来信息泄露与代码执行风险;构建时应统一开启 PIE、-fstack-protector-strong、NX 与 RELRO,安全相关数据一律使用密码学安全随机源生成。