java反序列化
序列化与反序列化
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接口
是Java提供的序列化接口,它是一个空接口
1  | public interface Serializable{  | 
Serializabel用来标识当前类可以被ObjectOutputStream序列化,以及被ObjectInputStream反序列化
Serializable接口的基本使用
通过ObjectOutputStream将需要序列化数据写入到流中,因为Java IO 是一种装饰着模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。
Serializable 接口的特点
- 序列化类的属性没有实现 Serializable 那么在序列化就会报错
 
具体可以跟进 ObjectOutputStream#writeObject() 源码查看具体原因:
1  | Exception in thread "main" java.io.NotSerializableException: com.example.seriable.Color  | 
1  | public class Student implements Serializable {  | 
- 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
 
Animal 是父类,它没有实现 Serilizable 接口
1  | public class Animal {  | 
BlackCat 是 Animal 的子类
1  | public class BlackCat extends Animal implements Serializable {  | 
SuperMain 测试类
1  | public class SuperMain {  | 
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来记住你的登录信息


可以看到在登录之后生成了一串base64来作为登录用户的Cookie。实际上后端是对用户登录信息进行序列化,然后进行AES加密后base64,这便是我们的Cookie。
如果我们能构造恶意的序列化代码,然后使用相同的方式加密传入,那么后端就会相应的解密反序列化,然后就会执行我们的恶意代码。
加密过程分析
可以按两次shift搜索Cookie有关的类方法等。问题是出现在org.apache.shiro:shiro-web-1.2.4下的CookieRememberMeManager类中

在rememberSerializedIdentity()方法中会对我们传入的序列化字符串serialized进行base加密并将其作为Cookie。跟进,看哪里调用了该方法
在AbstractRememberMeManager类的rememberIdentity()方法中,先对传入的byte通过convertPrincipalsToBytes()方法处理,再次查看哪里调用了rememberIdentity()

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

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

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

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

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

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

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

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

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

getEncryptionCipherKey()方法返回加密的密钥,我们跟进看一下加密的密钥是什么
看value write,哪里给encryptionCipherkey赋值

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

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

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

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

加密密钥:
kPH+bIxk5D2deZiIxcaaaA==
加密完成后,回到rememberIdentity(),这里的bytes就是加密之后的cookie

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

跟进rememberSerializedIdentity(),最终将我们加密之后的cookie先进行base64编码,再存储到当前会话的Cookie中
至此就是根据用户信息加密生成Cookie的完整过程
解密过程分析
下面我们来调试一下解密过程,在AbstractRememberMeManager.getRememberedPrincipals()下一个断点
在bp中发个包,注意此时我们要把Cookie中的sessionID删除,不然后端不会解析我们的加密串
接着跟进CookieRememberMeManager.getRememberedSerializedIdentity(),在其中获取cookie然后将其base64解密
接着跟进AbstractRememberMeManager.convertBytesToPrincipals()
跟进decrypt()
继续跟进JcaCipherService.decrypt(),这里的解密密钥通过getDecryptionCipherKey()获得,为固定值
此处代码的主要逻辑是先生成一个初始向量iv,可以看见使用了arraycopy()函数,iv是从ciphertext中复制的,也就是说初始向量是我们密文的一部分。接着将密文减去初始向量,然后再将其解密。
最终是调用的是java自带的AES解密方法,解密之后返回并将其反序列化
跟进deserialize()
