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()