序列化与反序列化

Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。

序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。

为什么需要序列化与反序列化

我们知道,当两个进程进行远程通信时,可以互相发送各种类型的数据,包括文本,图片,视频,音频等,而这些数据都会以二进制序列的形式在网络上传输.那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?
答案是可以的.如何做到呢?

这就需要序列化与反序列化了,换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列人中恢复出Java对象.

当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。

① 想把内存中的对象保存到一个文件中或者数据库中时候;
② 想用套接字在网络上传送对象的时候;
③ 想通过RMI传输对象的时候

一些应用场景,涉及到对象转化成二进制,序列化保证了能够成功读取到保存的对象.

几种常见的序列化和反序列化协议

  • XML&SOAP

XML是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点SOAP(Simple Object Access protocol)是一种被广泛应用的,基于XML为序列化和反序列化协议的结构化消息传递协议

  • JSON(javascript Object Notation)
  • Protobuf

序列化实现

只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列.(不是则会抛出异常)

Serializable 接口

Serializable接口

是Java提供的序列化接口,它是一个空接口

1
2
3
public interface Serializable{

}

Serializabel用来标识当前类可以被ObjectOutputStream序列化,以及被ObjectInputStream反序列化

Serializable接口的基本使用

通过ObjectOutputStream将需要序列化数据写入到流中,因为Java IO 是一种装饰着模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。
img

Serializable 接口的特点

  • 序列化类的属性没有实现 Serializable 那么在序列化就会报错

具体可以跟进 ObjectOutputStream#writeObject() 源码查看具体原因:

1
Exception in thread "main" java.io.NotSerializableException: com.example.seriable.Color
1
2
3
4
5
6
7
8
public class Student implements Serializable {
private String name;
private int age;
/**
* Color 类也是需要实现序列化接口的。
*/
private Color color;//这里如果没有实现序列化接口,那么在 Student 对象序列化时将会报错
}
  • 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。

Animal 是父类,它没有实现 Serilizable 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Animal {
private String color;

public Animal() {//没有无参构造将会报错
System.out.println("调用 Animal 无参构造");
}

public Animal(String color) {
this.color = color;

System.out.println("调用 Animal 有 color 参数的构造");
}

@Override
public String toString() {
return "Animal{" +
"color='" + color + '\'' +
'}';
}
}

BlackCat 是 Animal 的子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BlackCat extends Animal implements Serializable {
private static final long serialVersionUID = 1L;
private String name;

public BlackCat() {
super();
System.out.println("调用黑猫的无参构造");
}

public BlackCat(String color, String name) {
super(color);
this.name = name;
System.out.println("调用黑猫有 color 参数的构造");
}

@Override
public String toString() {
return "BlackCat{" +
"name='" + name + '\'' +super.toString() +'\'' +
'}';
}
}

SuperMain 测试类

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
public class SuperMain {
private static final String FILE_PATH = "./super.bin";

public static void main(String[] args) throws Exception {
serializeAnimal();
deserializeAnimal();
}

private static void serializeAnimal() throws Exception {
BlackCat black = new BlackCat("black", "我是黑猫");
System.out.println("序列化前:"+black.toString());
System.out.println("=================开始序列化================");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(black);
oos.flush();
oos.close();
}

private static void deserializeAnimal() throws Exception {
System.out.println("=================开始反序列化================");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
BlackCat black = (BlackCat) ois.readObject();
ois.close();
System.out.println(black);
}
}

Shiro反序列化漏洞——Shiro550(CVE-2016-4437)

Shiro框架

Apache Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能。Shiro框架直观、易用,同时也能提供健壮的安全性。

漏洞原理

Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会将用户的登录信息加密编码,然后存储在Cookie中。对于服务端,如果检测到用户的Cookie,首先会读取rememberMe的Cookie值,然后进行base64解码,然后进行AES解密再反序列化。

反过来思考一下,如果我们构造该值为一个cc链序列化后的字符串,并使用该密钥进行AES加密后再进行base64编码,那么这时候服务端就会去进行反序列化我们的payload内容,这样就可以达到命令执行的效果。

漏洞流程

获取rememberMe值 - > Base64解密 -> AES解密 - > 调用readobject 反序列化操作

影响版本

Apache Shiro <=1.2.4

特征判断

相应包中包含字段remember = deleteMe字段

漏洞点

在我们登录shiro之后,如果点击了remember选项,网站就会生成一个Cookie来记住你的登录信息

image-20230420160013114

image-20230420160028772

可以看到在登录之后生成了一串base64来作为登录用户的Cookie。实际上后端是对用户登录信息进行序列化,然后进行AES加密后base64,这便是我们的Cookie。

如果我们能构造恶意的序列化代码,然后使用相同的方式加密传入,那么后端就会相应的解密反序列化,然后就会执行我们的恶意代码。

加密过程分析

可以按两次shift搜索Cookie有关的类方法等。问题是出现在org.apache.shiro:shiro-web-1.2.4下的CookieRememberMeManager类中

image-20230420160541857

rememberSerializedIdentity()方法中会对我们传入的序列化字符串serialized进行base加密并将其作为Cookie。跟进,看哪里调用了该方法

AbstractRememberMeManager类的rememberIdentity()方法中,先对传入的byte通过convertPrincipalsToBytes()方法处理,再次查看哪里调用了rememberIdentity()

image-20230420161423900

最终跟进到onSuccessfulLogin()方法中,再次跟进,可以看到该方法被rememberMeSuccessfulLogin()调用,这里应该就是”记住我”的功能点了,我们再这里下个断点调试

image-20230420161647748

使用root登录,跟到了rememberMeSuccessfulLogin()中,单步调试img

向下跟进到onSuccessfulLogin()方法中,调用forgetIdentity()方法对subject进行处理,subject对象表示单个用户的状态和安全操作,包含认证、授权等,跟进

img

forgetIdentity()中,对subject进行了处理,继续跟进forgetIdentity()

img

跟进forgetIdentity()方法,getCookie()方法获取请求的cookie,接着会进入到removeFrom()方法

img

跟进removeFrom()方法,removeForm主要在response头部添加字段Set-Cookie: rememberMe=deleteMe

img

再回到onSuccessfulLogin()方法中,如果设置rememberMe则进入rememberIdentity(),跟进看是怎么处理的

img

rememberIdentity()中,调用了convertPrincipalsToBytes()对用户名进行了处理,跟进img

convertPrincipalsToBytes()方法中,先对用户名进行序列化,然后使用encrypt()进行加密,跟进encrypt()

img

encrypt()中,可以看到使用的加密算法是AES,使用AES算法对cookie进行加密。在这里可以看见一些加密的信息,跟进看一下

img

encrypt()中,可以看到使用的加密算法是AES,使用AES算法对cookie进行加密。在这里可以看见一些加密的信息,跟进看一下img

img

getEncryptionCipherKey()方法返回加密的密钥,我们跟进看一下加密的密钥是什么img

看value write,哪里给encryptionCipherkey赋值

img

value write

setEncryptionCipherKey()赋值的,跟进看哪里调用了

img

setEncryptionCipherKey()方法

setCipherKey()中同时给加解密密钥赋值,继续跟进

img

setCipherKey()方法

AbstractRememberMeManager()方法中赋值,可以看见这里这个常量应该就是密钥

img

AbstractRememberMeManager()方法

可以看见密钥*DEFAULT_CIPHER_KEY_BYTES*是一个常量,这里就是漏洞利用的关键点

img

加密密钥:kPH+bIxk5D2deZiIxcaaaA==

加密完成后,回到rememberIdentity(),这里的bytes就是加密之后的cookie

img

跟进rememberSerializedIdentity(),最终将我们加密之后的cookie先进行base64编码,再存储到当前会话的Cookie中

img

跟进rememberSerializedIdentity(),最终将我们加密之后的cookie先进行base64编码,再存储到当前会话的Cookie中img

至此就是根据用户信息加密生成Cookie的完整过程

解密过程分析

下面我们来调试一下解密过程,在AbstractRememberMeManager.getRememberedPrincipals()下一个断点

在bp中发个包,注意此时我们要把Cookie中的sessionID删除,不然后端不会解析我们的加密串

接着跟进CookieRememberMeManager.getRememberedSerializedIdentity(),在其中获取cookie然后将其base64解密

接着跟进AbstractRememberMeManager.convertBytesToPrincipals()img

跟进decrypt()img

继续跟进JcaCipherService.decrypt(),这里的解密密钥通过getDecryptionCipherKey()获得,为固定值img

此处代码的主要逻辑是先生成一个初始向量iv,可以看见使用了arraycopy()函数,iv是从ciphertext中复制的,也就是说初始向量是我们密文的一部分。接着将密文减去初始向量,然后再将其解密。

最终是调用的是java自带的AES解密方法,解密之后返回并将其反序列化img

跟进deserialize()img