Ywc's blog

从0开始学Java反序列化漏洞

Word count: 4kReading time: 16 min
2020/02/08

一、Java序列化与反序列化简介

序列化是让Java对象脱离Java运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。

Java序列化是指把Java对象转换为字节序列的过程(便于保存在内存、文件、数据库中),ObjectOutputStream类的writeObject()方法可以实现序列化。

Java反序列化是指把字节序列恢复为Java对象的过程,ObjectInputStream类的readObject()方法用于反序列化。

实现java.io.Serializable接口才可被反序列化,而且所有属性必须是可序列化的(用transient关键字修饰的属性除外,不参与序列化过程)

在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以通过ObjectInputStream与ObejctOutputStream序列化.

一个类的对象要想序列化成功,必须满足两个条件:

  • 1.该类必须实现 java.io.Serializable 接口。

  • 2.该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。

如果想知道一个 Java 标准类是否是可序列化的,可以通过查看该类的文档,查看该类有没有实现 java.io.Serializable接口。

二、漏洞成因

序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

漏洞代码示例:

1
2
3
4
5
6
7
......
//读取输入流,并转换对象
InputStream in=request.getInputStream();
ObjectInputStream ois = new ObjectInputStream(in);
//恢复对象
ois.readObject();
ois.close();

暴露或间接暴露反序列化API,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码

两个或多个看似安全的模块在同一运行环境下,共同产生的安全问题。

触发场景

  • 1.HTTP请求中的参数
  • 2.RMI,即Java远程方法调用,在RMI中传输的数据皆为序列化
  • 3.JMX,一个为应用程序植入管理功能的框架
  • 4.自定义协议,用来接收与发送原始的java对象

相关工具: https://github.com/frohoff/ysoserial/

三、漏洞基本原理

JAVA序列化数据流特征

  • 加密便于传输,不加密不容易阅读传输

  • 数据流以

    • rO0AB开端是JAVA序列化的base64加密
    • aced开端是JAVA序列化的16禁止加密
  • 涉及到以下函数,则考虑JAVA反序列化:

    1
    2
    3
    4
    5
    6
    7
    ObjectInputStream.readobject
    ObjectInputStream.readUnshared
    XMLDecoder.readObject
    XStream.fromXML
    ObjectMapper.readValue
    JSON.parseObject
    .....

序列化

通过查看序列化后的数据,可以看到反序列化数据开头包含两字节的魔术数字,这两个字节始终为十六进制的0xAC ED。接下来是两字节的版本号0x00 05的数据。此外还包含了类名、成员变量的类型和个数等。

例子:SerialObject 示例

对象所属类:

1
2
3
4
5
6
7
8
9
10
11
12
public class SerialObject implements Serializable{
private static final long serialVersionUID = 5754104541168322017L;

private int id;
public String name;

public SerialObject(int id,String name){
this.id=id;
this.name=name;
}
...
}

序列化SerialObject实例后以二进制格式查看:

1
2
3
4
5
6
7
00000000: aced 0005 7372 0024 636f 6d2e 7878 7878  ....sr.$com.xxxx
00000010: 7878 2e73 6563 2e77 6562 2e68 6f6d 652e xx.sec.web.home.
00000020: 5365 7269 616c 4f62 6a65 6374 4fda af97 SerialObjectO...
00000030: f8cc c5e1 0200 0249 0002 6964 4c00 046e .......I..idL..n
00000040: 616d 6574 0012 4c6a 6176 612f 6c61 6e67 amet..Ljava/lang
00000050: 2f53 7472 696e 673b 7870 0000 07e1 7400 /String;xp....t.
00000060: 0563 7279 696e 0a .cryin.

序列化的数据流以魔术数字和版本号开头,这个值是在调用ObjectOutputStream序列化时,由writeStreamHeader方法写入:

1
2
3
4
protected void writeStreamHeader() throws IOException {
bout.writeShort(STREAM_MAGIC);//STREAM_MAGIC (2 bytes) 0xACED
bout.writeShort(STREAM_VERSION);//STREAM_VERSION (2 bytes) 5
}

反序列化

Java程序中类ObjectInputStreamreadObject方法被用来将数据流反序列化为对象,如果流中的对象是class,则它的ObjectStreamClass描述符会被读取,并返回相应的class对象,ObjectStreamClass包含了类的名称及serialVersionUID

如果类描述符是动态代理类,则调用resolveProxyClass方法来获取本地类。如果不是动态代理类则调用resolveClass方法来获取本地类。如果无法解析该类,则抛出ClassNotFoundException异常。

如果反序列化对象不是String、array、enum类型,ObjectStreamClass包含的类会在本地被检索,如果这个本地类没有实现java.io.Serializable或者externalizable接口,则抛出InvalidClassException异常。因为只有实现了SerializableExternalizable接口的类的对象才能被序列化。

readObject()方法在反序列化漏洞中它起到了关键作用,readObject()方法被重写的的话,反序列化该类时调用便是重写后的readObject()方法。如果该方法书写不当的话就有可能引发恶意代码的执行.

四、检测Java反序列化漏洞

代码审计

重点关注一些反序列化操作函数并判断输入是否可控
基本思路:

1、通过检索源码中对反序列化函数的调用来静态寻找反序列化的输入点
可以搜索以下函数:

1
2
3
4
5
6
7
ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMapper.readValue
JSON.parseObject

小数点前面是类名,后面是方法名

2、确定了反序列化输入点后,再考察应用的Class Path中是否包含Apache Commons Collections等危险库(ysoserial所支持的其他库亦可)

3、若不包含危险库,则查看一些涉及命令、代码执行的代码区域,防止程序员代码不严谨,导致bug

4、若包含危险库,则使用ysoserial进行攻击复现

https://github.com/frohoff/ysoserial/

白盒审计

大型企业的应用很多,每个都人工去审计不现实,往往都有相应的自动化静态代码审计工具,这里以ObjectInputStream.readObject()为例,其它原理也相似。在自动化检测时,可通过实现解析java源代码,检测readObject()方法调用时判断其对象是否为java.io.ObjectOutputStream。如果此时ObjectInputStream对象的初始化参数来自外部请求输入参数则基本可以确定存在反序列化漏洞了。这是只需确认是否存在相应的安全修复即可。 检测方式可参考lgtm.com对于Deserialization of user-controlled data的实现

黑盒审计

调用ysoserial并依次生成各个第三方库的利用payload(也可以先分析依赖第三方包量,调用最多的几个库的paylaod即可),该payload构造为访问特定url链接的payload,根据http访问请求记录判断反序列化漏洞是否利用成功。如:

1
java -jar ysoserial.jar CommonsCollections1 'curl " + URL + " '

也可通过DNS解析记录确定漏洞是否存在。现成的轮子很多,推荐NickstaDB写的SerialBrute,还有一个针对RMI的测试工具BaRMIe。

攻击检测

通过查看反序列化后的数据,可以看到反序列化数据开头包含两字节的魔术数字,这两个字节始终为十六进制的0xAC ED。接下来是两字节的版本号。我只见到过版本号为5(0x00 05)的数据。考虑到zip、base64各种编码,在攻击检测时可针对该特征进行匹配请求post中是否包含反序列化数据,判断是否为反序列化漏洞攻击。

1
2
3
4
5
xxxdeMacBook-Pro:demo xxx$ xxd objectexp 
00000000: aced 0005 7372 0032 7375 6e2e 7265 666c ....sr.2sun.refl
00000010: 6563 742e 616e 6e6f 7461 7469 6f6e 2e41 ect.annotation.A
00000020: 6e6e 6f74 6174 696f 6e49 6e76 6f63 6174 nnotationInvocat
00000030: 696f 6e48 616e 646c 6572 55ca f50f 15cb ionHandlerU.....

但仅从特征匹配只能确定有攻击尝试请求,还不能确定就存在反序列化漏洞,还要结合请求响应、返回内容等综合判断是否确实存在漏洞。

RASP检测

Java程序中类ObjectInputStreamreadObject方法被用来将数据流反序列化为对象,如果流中的对象是class,则它的ObjectStreamClass描述符会被读取,并返回相应的class对象,ObjectStreamClass包含了类的名称及serialVersionUID。

类的名称及serialVersionUID的ObjectStreamClass描述符在序列化对象流的前面位置,且在readObject反序列化时首先会调用resolveClass读取反序列化的类名,所以RASP检测反序列化漏洞时可通过重写ObjectInputStream对象的resolveClass方法获取反序列化的类即可实现对反序列化类的黑名单校验。

其他

1.从流量中发现序列化的痕迹,关键字:ac ed 00 05,rO0AB
2.Java RMI的传输100%基于反序列化,Java RMI的默认端口是1099端口
3.从源码入手,可以被序列化的类一定实现了Serializable接口
4.观察反序列化时的readObject()方法是否重写,重写中是否有设计不合理,可以被利用之处

从可控数据的反序列化或间接的反序列化接口入手,再在此基础上尝试构造序列化的对象。

ysoserial是一款非常好用的Java反序列化漏洞检测工具,该工具通过多种机制构造PoC,并灵活的运用了反射机制和动态代理机制,值得学习和研究。

五、防御Java反序列化漏洞

1.类白名单校验

ObjectInputStream中resolveClass 里只是进行了class 是否能被load,自定义ObjectInputStream, 重载resolveClass的方法,对className 进行白名单校验

1
2
3
4
5
6
7
8
9
10
11
public final class test extends ObjectInputStream{
...
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException{
if(!desc.getName().equals("className")){
throw new ClassNotFoundException(desc.getName()+" forbidden!");
}
returnsuper.resolveClass(desc);
}
...
}

2.禁止JVM执行外部命令Runtime.exec

通过扩展SecurityManager可以实现:

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
SecurityManager originalSecurityManager = System.getSecurityManager();
if (originalSecurityManager == null) {
// 创建自己的SecurityManager
SecurityManager sm = new SecurityManager() {
private void check(Permission perm) {
// 禁止exec
if (perm instanceof java.io.FilePermission) {
String actions = perm.getActions();
if (actions != null && actions.contains("execute")) {
throw new SecurityException("execute denied!");
}
}
// 禁止设置新的SecurityManager,保护自己
if (perm instanceof java.lang.RuntimePermission) {
String name = perm.getName();
if (name != null && name.contains("setSecurityManager")) {
throw new SecurityException("System.setSecurityManager denied!");
}
}
}

@Override
public void checkPermission(Permission perm) {
check(perm);
}

@Override
public void checkPermission(Permission perm, Object context) {
check(perm);
}
};

System.setSecurityManager(sm);
}

Java反序列化大多存在复杂系统间相互调用,控制,或较为底层的服务应用间交互等应用场景上,因此接口本身可能就存在一定的安全隐患。Java反序列化本身没有错,而是面对不安全的数据时,缺乏相应的防范,导致了一些安全问题。并且不容忽视的是,也许某些Java服务没有直接使用存在漏洞的Java库,但只要Lib中存在存在漏洞的Java库,依然可能会受到威胁。

六、修复Java反序列化漏洞

通过Hook resolveClass来校验反序列化的类

在使用readObject()反序列化时首先会调用resolveClass方法读取反序列化的类名,所以这里通过重写ObjectInputStream对象的resolveClass方法即可实现对反序列化类的校验。

具体实现代码Demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class AntObjectInputStream extends ObjectInputStream{
public AntObjectInputStream(InputStream inputStream)
throws IOException {
super(inputStream);
}

/**
* 只允许反序列化SerialObject class
*/
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
ClassNotFoundException {
if (!desc.getName().equals(SerialObject.class.getName())) {
throw new InvalidClassException(
"Unauthorized deserialization attempt",
desc.getName());
}
return super.resolveClass(desc);
}
}

通过此方法,可灵活的设置允许反序列化类的白名单,也可设置不允许反序列化类的黑名单。但反序列化漏洞利用方法一直在不断的被发现,黑名单需要一直更新维护,且未公开的利用方法无法覆盖。

1
2
3
4
5
6
7
8
9
10
org.apache.commons.collections.functors.InvokerTransformer
org.apache.commons.collections.functors.InstantiateTransformer
org.apache.commons.collections4.functors.InvokerTransformer
org.apache.commons.collections4.functors.InstantiateTransformer
org.codehaus.groovy.runtime.ConvertedClosure
org.codehaus.groovy.runtime.MethodClosure
org.springframework.beans.factory.ObjectFactory
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
org.apache.commons.fileupload
org.apache.commons.beanutils

根据以上方法,有大牛实现了线程的SerialKiller包可供使用。

使用ValidatingObjectInputStream来校验反序列化的类

使用Apache Commons IO Serialization包中的ValidatingObjectInputStream类的accept方法来实现反序列化类白/黑名单控制,具体可参考ValidatingObjectInputStream介绍;示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
private static Object deserialize(byte[] buffer) throws IOException,
ClassNotFoundException , ConfigurationException {
Object obj;
ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
// Use ValidatingObjectInputStream instead of InputStream
ValidatingObjectInputStream ois = new ValidatingObjectInputStream(bais);

//只允许反序列化SerialObject class
ois.accept(SerialObject.class);
obj = ois.readObject();
return obj;
}

使用contrast-rO0防御反序列化攻击

contrast-rO0是一个轻量级的agent程序,通过通过重写ObjectInputStream来防御反序列化漏洞攻击。使用其中的SafeObjectInputStream类来实现反序列化类白/黑名单控制,示例代码如下:

1
2
3
4
SafeObjectInputStream in = new SafeObjectInputStream(inputStream, true);
in.addToWhitelist(SerialObject.class);

in.readObject();

使用ObjectInputFilter来校验反序列化的类

Java 9包含了支持序列化数据过滤的新特性,开发人员也可以继承java.io.ObjectInputFilter类重写checkInput方法实现自定义的过滤器,,并使用ObjectInputStream对象的setObjectInputFilter设置过滤器来实现反序列化类白/黑名单控制。示例代码如下:

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
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.io.ObjectInputFilter;
class BikeFilter implements ObjectInputFilter {
private long maxStreamBytes = 78; // Maximum allowed bytes in the stream.
private long maxDepth = 1; // Maximum depth of the graph allowed.
private long maxReferences = 1; // Maximum number of references in a graph.
@Override
public Status checkInput(FilterInfo filterInfo) {
if (filterInfo.references() < 0 || filterInfo.depth() < 0 || filterInfo.streamBytes() < 0 || filterInfo.references() > maxReferences || filterInfo.depth() > maxDepth|| filterInfo.streamBytes() > maxStreamBytes) {
return Status.REJECTED;
}
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
if (SerialObject.class == filterInfo.serialClass()) {
return Status.ALLOWED;
}
else {
return Status.REJECTED;
}
}
return Status.UNDECIDED;
} // end checkInput
} // end class BikeFilter

上述示例代码,仅允许反序列化SerialObject类对象。

禁止JVM执行外部命令Runtime.exec

通过扩展SecurityManager

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
SecurityManager originalSecurityManager = System.getSecurityManager();
if (originalSecurityManager == null) {
// 创建自己的SecurityManager
SecurityManager sm = new SecurityManager() {
private void check(Permission perm) {
// 禁止exec
if (perm instanceof java.io.FilePermission) {
String actions = perm.getActions();
if (actions != null && actions.contains("execute")) {
throw new SecurityException("execute denied!");
}
}
// 禁止设置新的SecurityManager,保护自己
if (perm instanceof java.lang.RuntimePermission) {
String name = perm.getName();
if (name != null && name.contains("setSecurityManager")) {
throw new SecurityException("System.setSecurityManager denied!");
}
}
}

@Override
public void checkPermission(Permission perm) {
check(perm);
}

@Override
public void checkPermission(Permission perm, Object context) {
check(perm);
}
};

System.setSecurityManager(sm);
}

不建议使用的黑名单

在反序列化时设置类的黑名单来防御反序列化漏洞利用及攻击,这个做法在源代码修复的时候并不是推荐的方法,因为你不能保证能覆盖所有可能的类,而且有新的利用payload出来时也需要随之更新黑名单,但有一种场景下可能黑名单是一个不错的选择。写代码的时候总会把一些经常用到的方法封装到公共类,这样其它工程中用到只需要导入jar包即可,此前已经见到很多提供反序列化操作的公共接口,使用第三方库反序列化接口就不好用白名单的方式来修复了。这个时候作为第三方库也不知道谁会调用接口,会反序列化什么类,所以这个时候可以使用黑名单的方式来禁止一些已知危险的类被反序列化,具体的黑名单类可参考contrast-rO0、ysoserial中paylaod包含的类。

Reference

http://www.bjpowernode.com/javazixun/9595.html

https://www.vulbox.com/knowledge/detail/?id=11

https://xz.aliyun.com/t/2043#toc-2

https://xz.aliyun.com/t/2041#toc-4

https://xz.aliyun.com/t/6787#toc-15

https://mp.weixin.qq.com/s?__biz=MzIzMzgxOTQ5NA==&mid=2247484200&idx=1&sn=8f3201f44e6374d65589d00d91f7148e

CATALOG
  1. 1. 一、Java序列化与反序列化简介
  2. 2. 二、漏洞成因
  3. 3. 三、漏洞基本原理
    1. 3.1. JAVA序列化数据流特征
    2. 3.2. 序列化
    3. 3.3. 反序列化
  4. 4. 四、检测Java反序列化漏洞
    1. 4.1. 代码审计
    2. 4.2. 白盒审计
    3. 4.3. 黑盒审计
    4. 4.4. 攻击检测
    5. 4.5. RASP检测
    6. 4.6. 其他
  5. 5. 五、防御Java反序列化漏洞
  6. 6. 六、修复Java反序列化漏洞
    1. 6.1. 通过Hook resolveClass来校验反序列化的类
    2. 6.2. 使用ValidatingObjectInputStream来校验反序列化的类
    3. 6.3. 使用contrast-rO0防御反序列化攻击
    4. 6.4. 使用ObjectInputFilter来校验反序列化的类
    5. 6.5. 禁止JVM执行外部命令Runtime.exec
    6. 6.6. 不建议使用的黑名单
  7. 7. Reference