Ruoyi SSTI 与 Thymeleaf bypass深探
原标题是对若依(RuoYi 4.8.x)监控模块一处 Thymeleaf/SpEL 模板注入的利用研究,含从双写绕过到反射链注入内存马的完整 RCE。本文不复现任何可用 payload、内存马注入步骤或反射链构造,而是从防守方提炼:这个缺陷“为什么能成立、过滤为何会被绕过、应如何修复与检测”。(受影响:RuoYi 4.8.1、4.8.2;已在 4.8.3 修复。)
一、风险根因:把不可信输入当作表达式解析
RuoYi 监控/缓存相关控制器在若干接口里使用了“片段表达式”机制,当用户可控参数被当作模板/表达式求值时,就为表达式注入(SSTI/SpEL)打开了大门。本质上这属于“不安全的动态求值”:只要能把字符串交给表达式引擎,就有机会调用引擎能力执行任意逻辑。防守第一原则是:不要把用户输入拼接/传递到模板或表达式求值的入口。
二、过滤为何会被“双写”绕过(仅概念)
防护层里有一个用于判断“字符串是否含表达式入口”的检测函数,它线性扫描 ${}、#{}、@{}、~{} 等形式。问题出在“重叠匹配”处理:当扫描到一个起始符后若下一字符不是 {,就把状态直接重置,不再重新判断“当前这个字符本身是否又是一个新的起始符”。于是在
\[{}、##{} 这类相邻/双写组合下,第二个起始符会被当作“上一次匹配失败的普通字符”消费掉,后续的 {} 就不再被识别——这就是双写绕过的机制。需要说清:这只是“黑名单/线性扫描类检测”的通病,而不是“换个写法就能为所欲为”的通行证。 ## 三、为何防护不能只靠“禁 T()/new” 另一层检测是拦截对象实例化与静态类访问(如 new X(...)、T(X))。修复后传统“T( 空格”这类绕过被堵,且不允许可控 new——但这会把对抗推向“用反射逐步获取类与方法并调用”的方向。换句话说,“基于关键字黑名单”的防护面对反射、语言特性(如表达式预处理、字面量替换)时往往挂一漏万。真正的出路是提级到安全版本 + 从架构上消除“可控输入参与表达式求值”。 ## 四、缓解与检测(重点) **缓解** 1. **升级**:升级至已修复的 RuoYi 4.8.3;监控/缓存类高危接口默认不对低权限用户开放。 2. **消除动态求值入口**:避免将请求参数作为 Thymeleaf 片段/SpEL 表达式解析;模板名/表达式应取自固定白名单常量。 3. **最小暴露**:监控、缓存、Druid 等后台模块加强访问控制与网络隔离,不给未授权可达。 4. **运行层防护**:考虑沙箱化表达式求值、限制反射与危险类,部署 RASP 拦截运行时命令执行/内存马注册。 **检测** - WAF/日志:监控参数中的 ${、#{、@{、~{、__*__ 预处理与字面量替换特征,并警惕 \]
{ 等双写变形。
- 内存马线索:巡检 Spring Filter/Servlet/Listener 的异常动态注册与无文件落地处理器。
- 行为面:Web 进程派生子进程、异常反射调用链是高置信信号。
五、启示
这个案例的教训是:模板引擎的“表达式求值能力”本身就是危险 sink,而“线性扫描+关键字黑名单”的防护容易被语言特性(双写、预处理、字面量替换、反射)绕过。安全建设应优先“不让不可信输入进入求值”,而不是依赖“检测表达式特征”。
本文仅用于安全防护能力建设、代码审计与应急响应研究,不附可用 payload 与内存马构造。请遵守《中华人民共和国网络安全法》等法律法规,仅在授权环境验证;因违规使用造成的一切后果由行为人自行承担。