Java反序列化漏洞
0x00 序列化与反序列化基础
什么是序列化与反序列化
序列化是将复杂的数据结构或对象转换为字节序列的过程,以便于在网络上传输、存储或持久化,在序列化过程中,对象的状态被转换成字节流,使得它可以被写入文件或通过网络发送。
反序列化则是序列化的逆过程,即将序列化后的字节流转换回原始的数据结构或对象,在反序列化过程中,从序列化后的数据中提取出原始对象的状态,并将其重新构建为内存中的对象,这使得数据可以从持久化的状态重新恢复为原始的可操作对象,以便进行进一步的处理或使用。
这两个过程通常用于在不同系统之间进行数据交换,例如在客户端和服务器之间、不同编程语言之间、或者将对象持久化到数据库或磁盘上。
常见的序列化有二进制格式的,譬如Java Serialization、Ruby Marshal等;人类可读格式的,如JSON、XML、YAML等;以及混合格式的,如Python pickle、PHP Serialization等。
序列化与反序列化在Java中的实现
Java序列化
在Java中,如果要使一个类可序列化,只需该类实现Serializable
接口,Serializable
是一个标记接口,不包含任何方法,只是为了表示该类是可序列化的。
1 | import java.io.Serializable; |
然后,使用ObjectOutputStream
类可以将对象序列化成字节流,再将字节流保存到文件或通过网络传输。如下将Person
对象序列化到person.ser
文件中。
1 | import java.io.FileOutputStream; |
对该对象进行序列化时,Java会将对象转换为字节流并以特定的格式进行存储,通过查看person.ser
文件的十六进制内容,可以发现Java序列化数据是以aced
开头的,Base64编码则是rO0
,这也是Java序列化数据的魔术数(Magic Number),用于标识Java序列化的数据。
在其之后紧跟着的是序列化规范的版本号,例如0005
表示Java序列化规范的版本号是5,再之后的内容就是具体的对象数据,包括类名、字段名、字段类型和字段值等。
1 | xxd person.ser |
Java反序列化
而当Java收到序列化数据时,会使用ObjectInputStream
类从字节流中反序列化对象。如下从person.ser
文件中反序列化Person
对象。
1 | import java.io.FileInputStream; |
这样,一个简单的序列化与反序列化过程就介绍完毕了。
serialVersionUID版本控制
在Serializable
接口中,可以定义一个名为serialVersionUID
的static final long
字段。这个字段是用来控制对象的序列化版本。如果不显式地定义serialVersionUID
,Java会自动生成一个,但一旦修改了类的结构,生成的serialVersionUID
可能会发生变化,导致旧版本的序列化对象无法被正确反序列化。
1 | private static final long serialVersionUID = 1L; |
不可序列化的
可以使用transient
关键字来标记类的字段,告诉Java序列化机制不要将这些字段包含在序列化中。这在某些情况下很有用,比如一些涉及敏感信息的字段不希望被序列化。
除此之外,静态成员变量也是无法被序列化的,原因是因为序列化是针对对象及其实例变量的,而静态成员变量是类变量,属于类的状态,而非对象的状态。
1 | public class Person implements Serializable { |
Externalizable接口
要使一个类可序列化,除了实现Serializable
接口外,还可以实现Externalizable
接口,该接口继承自Serializable
。
在实现Externalizable
接口时,必须实现一个类的无参构造器,还需实现writeExternal
方法来定义对象的序列化方式,以及readExternal
方法来定义对象的反序列化方式,这样可以更精确地控制对象的序列化和反序列化过程。
1 | import java.io.Externalizable; |
0x01 Java反序列化漏洞的前世今生
发展历史
Java反序列化漏洞这一类型的漏洞利用,早在2011年就已在Spring中出现了第一个致使远程代码执行的Java反序列化利用,但在当时,反序列化这类问题并没有引起广泛的注意。
直至2015年1月加州AppSec安全会议上,Chris Frohoff和Gabe Lawrence发表《Marshalling Pickles》主题演讲,内容涵盖跨平台的反序列化漏洞、反序列化漏洞采取的多种形式以及可以找到反序列化漏洞的位置,除此之外,还包含了一些使用常用库中的类来攻击Java序列化的新颖技术,这些技术在随后也以Ysoserial工具的形式发布了。
再到2015年底才开始有其他研究人员使用这些技术和工具来利用Bamboo、WebLogic、WebSphere、ApacheMQ和Jenkins等知名产品,从那时起,Java反序列漏洞这一主题就被推到了公众视野中,并引起了广泛的关注。
简单的反序列化利用:应用程序逻辑操纵
在《Marshalling Pickles》主题演讲中,Chris Frohoff和Gabe Lawrence演示了一个存在逻辑缺陷的网站,攻击者通过修改反序列化数据中的属性值,从而达到对管理员用户的未授权登录。如下是相关的序列化类代码。
1 | package com.qualcomm.isrm.appsec; |
存在逻辑缺陷的代码如下,如下代码片段对传入的Cookie值进行了反序列化转变为对象的操作(如下readObject
方法),并获取了对象中的name,判断是否管理员name,是则进入到管理员用户页面。
攻击者只需抓包,修改Cookie中的序列化数据,将其中的name修改为管理员的name,即可达到恶意目的。
当然,在现代网站中,如上情景出现的概率不大,且这种反序列化逻辑操纵的危害也有限。
反序列化危害升级:任意代码执行
那么,Chris Frohoff和Gabe Lawrence就继续在演讲中对反序列化的危害做出了升级的利用,即达到任意代码执行,他们在演讲中对这种利用方式又称之为Property-Oriented Programming或Object Injection,即面向属性编程或对象注入。
面向属性编程,简称POP,原本是一种编程范式,旨在通过属性的方式来描述程序的行为和结构。放在这里也这么称呼是由于在Java反序列化攻击中,攻击者可以控制反序列化对象的所有属性,这与二进制中的面向返回编程(ROP,Return-Oriented Programming)攻击类似,攻击者利用程序中已有的gadget代码片段,并通过构建一系列的gadget调用构造完整链,来实现特定的攻击目的,如执行恶意代码。
作者也在演讲中详细指出了关于构造完整gadget链的要点。首先,gadget类是需要从应用程序中寻找,其次是构成完整gadget链的三部分:
- 以在反序列化过程中或之后执行的kick-off gadget开始。
- 以执行任意代码或命令的sink gadget结束。
- 使用其他gadget来启动gadget的执行直至结束gadget。
当完整的gadget链构造成功后,将其序列化并发送至应用程序中存在漏洞的反序列化处,最后在反序列化时,gadget链就会在应用程序中执行。
反序列化任意代码执行攻击的局限性
由于反序列化任意代码执行攻击的复杂性,也相应地带来了一定的局限性,在演讲中也指出了条件与注意事项。
- 只能使用应用程序可用的类。
- 存在漏洞代码的ClassLoader与gadgets问题。
- gadgets类必须实现Serializable/Externalizable接口。
- 库或类版本的差异问题。
- Static类型常量约束问题。
0x02 Java反序列化漏洞Kick-off Gadget
重写readObject致使任意代码执行
反序列化漏洞利用的第一步就是利用某个重写了readObject
方法的类,通常它也作为kick-off gadget。如下将演示一个类重写了readObject
方法,并且其中存在恶意的行为,当这个类对象被序列化后,再被由ObjectInputStream
读取对象进行反序列化时,恶意的行为便会被触发。
如下User
类,实现了Serializable
接口,在其中重写了readObject
方法,且readObject
方法中使用到了Runtime.getRuntime().exec
方法来执行系统命令。
当这个类被序列化后被保存至本地,在随后读取该文件,并对该类进行反序列化时,原本是调用readObject
方法将存储在文件中的Java对象读入至内存中,并返回一个Object对象。但由于User
类中存在一个重写的readObject
方法,那么该方法就会被优先执行,最终系统命令被执行,即弹出一个计算器。
但在实际应用程序中,几乎不可能存在这么简单、直接的利用方式,原因很简单,几乎不可能有开发人员会在重写的readObject方法中编写恶意操作的代码,更具体地说,如上情况kiff-off入口类和sink危害类都是User
类,且产生恶意行为就存在于readObject方法中,不太可能会有这么巧的事情。
通常更现实的情况是如下两种两种情况:
- 重写了readObject方法的A类存在触发B类的危害点,从kiff-off直接到sink。
- 重写了readObject方法的A类作为kiff-off,但其中并不能直接触发到B类的危害点,需要不断地找两者之间的其他类方法作为中间调用链,直至到达B sink类。
第二种情况更常见,寻找中间链也是极其复杂与繁琐的一个过程。
readObject方法执行过程分析
如上演示了重写的readObject()
方法在反序列化过程中会被执行,但究其原因,仅仅就近原则四个字可不够解释得通。如下将详细分析这个反序列化命令执行的过程,将断点打至该方法行以进行调试。
跟进其中,发现调用了重载方法。
1 | public final Object readObject() |
进入这个重载方法中,在其中又调用了readObject0
方法。
1 | private final Object readObject(Class<?> type) throws IOException, ClassNotFoundException { |
readObject0
方法如下,该方法将读取从ObjectOutputStream
写入的对象的字节表示。
1 | private Object readObject0(Class<?> type, boolean unshared) throws IOException { |
TC_OBJECT
是Java序列化机制中用于标识对象开始的特殊标记之一,当读取到(byte)0x73
时(即TC_OBJECT
常量),意味着这是一个对象序列化数据的开始,那接下来便会调用readOrdinaryObject
方法对对象做进一步处理。
在readOrdinaryObject
方法中,首先调用了readClassDesc
方法用于读取对象序列化数据流中的类描述符,包括类名、序列化版本号、字段等等信息,如若类实现了Externalizable
接口,便执行readExternalData
方法,否则就执行readSerialData
方法。
继续进入到readSerialData
方法,在其中通过了hasReadObjectMethod
方法来判断对象是否有重写readObject
方法,如果有重写,便调用invokeReadObject
方法调用对象中的readObject
方法。
进入到invokeReadObject
方法,成功弹出计算器。
0x03 URLDNS链分析
在前面提到过,要成功执行反序列化攻击,需要有开头的Kick-off入口类(重写了readObject方法的类)、存在恶意行为的Sink类(执行恶意操作的结束类)以及中间Gadget链(用于将Kick-off与Sink连成一条完整链)。
如下以分析一个在实际现实中存在的Gadget链作为补充理解,理解URLDNS链对后续学习CommonsCollections6会有极大帮助,因为在CC6中也使用到了
HashMap
类作为Kick-off,也会涉及到hashCode
方法。
Sink:URL#hashCode
Java中的java.net.URL
类提供许多方法用于解析、构建和处理URL,包括获取URL的协议、主机、端口、路径等信息,以及打开连接、读取内容等操作,该类使Java程序可以很方便地与互联网上的资源进行交互和通信。
URL
类由Serializable
实现,意味着该类可序列化,满足反序列化漏洞必备的条件之一。
1 | public final class URL implements java.io.Serializable |
在URL
类中存在一个hashCode
方法,该方法的作用是用于计算URL对象的哈希,但它还有一个奇怪的副作用,当调用该方法时,会对成员变量hashCode
进行判断,如果该值不等于-1则直接返回该值,而成员变量hashCode
的默认值就是-1,这也就意味着在正常情况下会调用handler.hashCode
方法。
1 | private int hashCode = -1; |
调用的handler.hashCode
方法,即java.net.URLStreamHandler#hashCode
,这个方法调用了getProtocol
、getHostAddress
等方法,根据这些方法名称,可以大致判断出这是在获取一个URL的几大部分,比如协议、主机名等信息。
1 | protected int hashCode(URL u) { |
而在getHostAddress
方法中,又调用了InetAddress.getByName
方法,这个方法会对提供的host进行解析从而获取它的IP地址,这样将会触发一个DNS查询。
那么这个DNS查询就是URLDNS链的所触发的恶意行为,虽然这个恶意行为的影响未必比得上远程代码执行,但作为反序列化漏洞的一个检测方式,还是很合适的,当然前提是服务器端的DNS流量能够出网。
1 | protected synchronized InetAddress getHostAddress(URL u) { |
编写如下代码片段进行测试,可以发现,确实能够对指定的host触发DNS查询。
1 | import java.net.URL; |
作为补充说明的一点是,在URL
类中,除了hashCode
方法外,equals
方法也能达到同样的恶意行为。
Kick-off:HashMap#readObject
java.util.HashMap
是Java中常用的集合类之一,它提供了一种快速的查找机制,可以根据键来快速查找对应的值,HashMap的常见操作包括插入元素、获取元素、删除元素、判断是否包含某个键等。
在HashMap
类中有对readObject
方法进行重写,重写后的readObject
方法首先读取了HashMap
的结构状态,然后为其包含的所有项开启一个循环,循环时从流中读取key和value。最后调用putVal
方法,在其中通过hash
方法获取key的哈希,还有key、value。
1 | private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { |
hash
方法如下,这个方法会调用传入的对象的hashCode
方法。
1 | static final int hash(Object key) { |
结合如上段落的分析,当传入一个URL对象到这个hash
方法,便会执行java.net.URL#hashCode
方法,这样就能够触发恶意行为。
继续观察HashMap
类,发现在put
方法对putVal
方法进行了调用,此处同样调用了hash
方法。
1 | public V put(K key, V value) { |
既然如此,那就可以将一个URL
对象放置在HashMap
中,通过调用HashMap
的put
方法,从而在反序列化执行readObject
方法时触发hash
方法的执行,该方法将调用传入的URL
对象的hashCode
方法,如此便可达到执行DNS查询的恶意行为。
利用代码及验证
利用代码如下,关于详细的代码释义见其中的代码注释。需要特别注意地是,在序列化时,需要通过反射修改hashCode成员变量为非-1,否则在序列化时就会触发DNS请求,这样将会对反序列化漏洞的检测造成误报。
1 | package com.javasec.urldns; |
现在,向一个存在反序列化漏洞的Jboss环境发送如上生成的恶意序列化数据,效果符合我们的预期,见如下图,即Jboss服务器成功向我们指定的域发起DNS查询。
1 | curl -H "Content-Type: application/x-java-serialized-object; class=org.jboss.invocation.MarshalledValue" --data-binary "@urldns.ser" http://192.168.1.128:8080/invoker/readonly |
URLDNS链总结
URLDNS链比较简单,由于它只依赖原生类,没有JDK版本限制,且没有危害,只会触发DNS查询,所以它常被用作来检测反序列化漏洞,但前提是服务器的DNS流量必须出网。
由于该链的简单性,它只有kick-off和sink,之间没有其他的类作为中间调用链。
- kick-off:
java.util.HashMap#readObject
- sink:
java.net.URL#hashCode
完整gadget调用链如下。
1 | HashMap.readObject() |
0x04 参考
- https://www.slideshare.net/frohoff1/appseccali-2015-marshalling-pickles
- https://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/
- https://gosecure.github.io/presentations/2019-04-29_atlseccon/History_of_Deserialization_v2.2.pdf
- https://www.pwntester.com/blog/2013/12/16/cve-2011-2894-deserialization-spring-rce/
- https://owasp.org/www-pdf-archive/Utilizing-Code-Reuse-Or-Return-Oriented-Programming-In-PHP-Application-Exploits.pdf
- https://su18.org/post/ysuserial/
- https://nvd.nist.gov/vuln/detail/CVE-2017-12149
- https://web-sec.gitbook.io/wsa/advanced/deserialization