f10@t's blog

Java 反序列化漏洞与JNDI注入(上)

字数统计: 3.5k阅读时长: 16 min
2019/09/03

好久没写博客,,,因为考虑到没有扎实的掌握一门流行的编程语言,所以花了两个月左右的时间补了Java的坑,最终当然也是要回归到安全方面上(ps:打OPPO的OGeek比赛时就碰到了Bind XXE类型,结合了tomcat的一些东西,结果除了扫目录毛线都不会)。这次主要想记录一下学习Java反序列化漏洞的过程,主要涉及Java的RMI(Remote Method Invocation)、RMP(Java Remote Message Protocol,Java 远程消息交换协议)、JDNI的概念。

ps:没有各位师傅的指导我可能会走更多的路,感谢这些师傅们的博客。

http://pupiles.com/java_unserialize1.html

https://kingx.me/Exploit-Java-Deserialization-with-RMI.html

http://sunsec.top/2019/10/16/JavaSpring反序列化

Java的序列化

对象序列化是指将内存中保存的对象以二进制数据流的形式进行处理以便实现对象的传输或储存。比如要发送到服务器上、储存在文件里、存在数据库中等。但并非所有对象都可以被序列化,要序列化一定要实现java.io.Serializable的父接口,该接口没有任何方法,描述的是一种类的能力。

下面是一段简单的代码演示如何进行Java对象的序列化以及反序列化。主要使用类及函数:

  1. ObjectOutputStream (接受OutputStream及其子类的对象)
  2. ObjectInputStream (接受InputStream及其子类的对象)
  3. FileOutputStream (接受File类型,打开一个文件输出流)
  4. FileInputStream (接受File类型,打开一个文件输入流)
  5. 序列化方法:writeObject()
  6. 反序列化方法:readObject()

代码下:

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
59
60
61
62
63
64
65
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

@SuppressWarnings("serial")
class Person implements Serializable { //可以被序列化的Person类
//如果加上transient关键字的话序列化时会忽略这个属性
private String name;
private int age;

/**
* 创建人的对象
* @param name
* @param age
*/
public Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "姓名: " + name + "、年龄: " + age;
}

}

/**
* 所谓的序列化就是二进制数据流的处理
* @ClassName: Baby_Serializable
* @Author: flo@t
*/
public class Baby_Serializable {

private static final File SAVE_FILE = new File(".\\serial.Person");

/**
* @Title: main
* @Description: 序列化和反序列化的操作可由ObjectOutputStream和ObjectInputStream两个类来实现
* ObjectOutputStream: 是OutputStream子类,接受一个OutputStream类对象
* ObjectInputStream: 是InputStream子类,接受一个InputStream类对象
* 序列化方法:writeObject()
* 反序列化方法:readObject()
*/
public static void main(String[] args) throws Exception {
saveObject(new Person("小米", 18)); //序列化
System.out.println(loadObject());
}

public static void saveObject(Object obj) throws Exception {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(SAVE_FILE));
os.writeObject(obj); //序列化
os.close();
}

public static Object loadObject() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SAVE_FILE));
Object obj = ois.readObject(); //反序列化
ois.close();
return obj;
}
}

反序列输出结果: 并且在指定的路径下生成这个对象的序列化文件 在Winhex中看起来是这样的,序列化后的文件头是0xaced0005

除了以上的默认序列化方法外,也可以进行自定义的序列化与反序列化,这也就是问题的所在。   

Java的自定义序列化与反序列化

自定义的序列化与反序列化相比上面只需要在被序列化的类声明中加入loadObject()方法和readObject()方法。如下代码所示:

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**  
* 自定义的序列化与反序列化操作
* @Title Defined_Serializable.java
* @Package io_Opt
* @author flo@t
* @date 2019年9月3日
* @version V1.0
*/
package io_Opt;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

@SuppressWarnings("serial")
class Car implements Serializable {
private String brand;
private int cost;

/**
* @param brand
* @param cost
*/
public Car(String brand, int cost) {
super();
this.brand = brand;
this.cost = cost;
}

@Override
public String toString() {
return "品牌: " + brand + "、售价: " + cost;
}

private void readObject(ObjectInputStream in) throws Exception {
try {
in.defaultReadObject();
System.out.println("Read Object Over");
} catch (Exception e) {
e.printStackTrace();
}
}

private void writeObject(ObjectOutputStream out) throws Exception {
try {
out.defaultWriteObject();
System.out.println("Write Object Over");
} catch (Exception e) {
e.printStackTrace();
}
}
}

/**
* 进行自定义的Java序列化与反序列化
* @ClassName: Baby_Serializable
* @Author: flo@t
*/
public class Defined_Serializable {

private static final File SAVE_FILE = new File(".\\serial.Car");

/**
*
* @Title: main
*/
public static void main(String[] args) throws Exception {
Car car = new Car("奔驰", 200000);

System.out.println(car.toString());

saveObject(car);

Car myCar = (Car) loadObject();
System.out.println(myCar.toString());
}

public static void saveObject(Object obj) throws Exception {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(SAVE_FILE));
os.writeObject(obj); //序列化
os.close();
}

public static Object loadObject() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SAVE_FILE));
Object obj = ois.readObject(); //反序列化
ois.close();
return obj;
}
}

输出结果: 可以看到除了两次对对象的读取以外,也输出了我们的两条提示性的测试语句。    问题就出在这里了,如果我在这两条测试性的语句的位置写的不是sysout,而是Runtime中的静态方法getRuntime()来调用其exec()方法,就可以任意执行代码或程序了,比如我改成下面这个样子。   

1
2
3
4
5
6
7
8
9
private void readObject(ObjectInputStream in) throws Exception {
try {
in.defaultReadObject();
//System.out.println("Read Object Over");
Runtime.getRuntime().exec("C:\\Windows\\System32\\calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}

在正常输出读取对象成功后,会运行计算器。

反序列化利用与JNDI注入和RMI

1
2
3
1. RMI (Remote Method Invocation) wiki定义:In computing, the Java Remote Method Invocation (Java RMI) is a Java API that performs remote method invocation, the object-oriented equivalent of remote procedure calls (RPC), with support for direct transfer of serialized Java classes and distributed garbage-collection.

2. JNDI(Java Naming and Directory Interface) wiki定义:The Java Naming and Directory Interface (JNDI) is a Java API for a directory service that allows Java software clients to discover and look up data and resources (in the form of Java objects) via a name. Like all Java APIs that interface with host systems, JNDI is independent of the underlying implementation. Additionally, it specifies a service provider interface (SPI) that allows directory service implementations to be plugged into the framework.[1] The information looked up via JNDI may be supplied by a server, a flat file, or a database; the choice is up to the implementation used.

简单来说的话,大家应该都清楚RPC(Remote Procedure Call)的作用,RMI相当于就是Java版本的RPC,可以实现远程对一个实例化的对象的使用。

而JNDI的作用提供了一组通用的接口可供应用很方便地去访问不同的后端服务,例如 LDAP、RMI、CORBA等。

在Java中为了能够更方便的管理、访问和调用远程的资源对象,常常会使用 LDAP和RMI等服务来将资源对象或方法绑定在固定的远程服务端,供应用程序来进行访问和调用。下面分别对这两者进行实例与解释。

RMI

如下代码大部分来自《Java核心技术第九版》的RMI部分,我稍加了修改,主要功能是实现远程对仓库商品的一个查询

Warehouse接口定义以及实现

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
/**  
* @Title: Warehouse.java
* @Package warehouse
* @Description: 仓库的远程接口。
* @author flo@t
* @date 2019年8月25日
* @version V1.0
*/
package warehouse;

import java.rmi.*;

/**
* @ClassName: Warehouse
* @author: flo@t
* @Description: 远程服务的接口定义中必须要继承Remote接口。这个接口中啥方法都没定义,应该是表示一种能力
* 官方描述:
* ......Any object that is a remote object must directly or indirectly implement this interface.....
*
*/
public interface Warehouse extends Remote {

/**
* 用于查询指定商品的价格
* @Title: getPrice
* @author: flo@t
* @Description:
* @param description
* @return
* @throws RemoteException
*/
double getPrice(String description) throws RemoteException;
}

对应的实现如下:

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
59
/**  
* @Title: WarehouseImpl.java
* @Package warehouse
* @Description: 服务器端来实现在远程接口中声明的工作。该类是远程方法调用的目标,因为扩展了UnicastRemoteObject类。
* @author flo@t
* @date 2019年8月25日
* @version V1.0
*/
package warehouse;

import java.util.*;
import java.rmi.*;
import java.rmi.server.*;

/**
* 实现 remote Warehouse的接口。
* @ClassName: WarehouseImpl
* @author: flo@t
*
* 补充:RMI的一个特点是服务的接口定义和其实现是分开的
*
* UnicastRemoteObject类的定义:
* public class UnicastRemoteObject extends RemoteServer
*
* public abstract class RemoteServer extends RemoteObject
*
* public abstract class RemoteObject implements Remote, java.io.Serializable
*
*/

@SuppressWarnings("serial")
public class WarehouseImpl extends UnicastRemoteObject implements Warehouse {

/**
* 定义一个键值对集合用于存放商品价格及其他信息
*/
private Map<String, Double> prices;

/**
* 构造方法
* @author flo@t
*/
public WarehouseImpl() throws RemoteException {
//创建一个Map对象并存入存入数据
prices = new HashMap<>();
prices.put("Toaster", 24.5);
prices.put("water", 2.0);

}

/**
* 接受用户要查询的商品的名称,返回商品的价格
*/
@Override
public double getPrice(String description) throws RemoteException {
Double price = prices.get(description);
return price == null ? 0 : price;
}
}

WarehouseServer实现

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**  
* @Title: WarehouseServer.java
* @Package warehouse
* @Description: 直接构造并且注册一个WarehouseImpl对象
* @author flo@t
* @date 2019年8月25日
* @version V1.0
*/
package warehouse;


import java.rmi.registry.LocateRegistry;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;


/**
* @ClassName: WarehouseServer
* @Description: 实例化WarehouseImpl对象,并用naming service来将它注册,并等待客户端来调用
* @author: flo@t
*/
public class WarehouseServer {

/**
* 使用LocateRegistry和Naming两个类来完成一个简单的RMI Demo
* @Title: main
* @Description:
* @author flo@t
* @param args
* @throws RemoteException
* @throws NamingException
*/
public static void main(String[] args) {
try {
System.out.print("Constructing server implemention...");
WarehouseImpl centralWarehouse = new WarehouseImpl();
System.out.println("Ok");

/*
* LocateRegistry is used to obtain a reference to a bootstrap
* remote object registry on a particular host (including the local host), or
* to create a remote object registry that accepts calls on a specific port.
*/

//创建远程对象的注册表,指明其服务的端口。
LocateRegistry.createRegistry(8888);

//Sets the system property indicated by the specified key.设置系统属性
//这里前面的键值对的键名可以去掉"rmi"(实测),为了区别还是加上吧
System.setProperty("rmi:central_warehouse", "127.0.0.1");

//绑定远程对象到注册表
System.out.print("Binding server implemention to registry...");

// 这里的"rmi://"必须要加上,提供一个URL来访问你的远程对象
//The Naming class provides methods for storing and obtaining references to remote objects in a remote object registry.
Naming.bind("rmi://localhost:8888/central_warehouse", centralWarehouse);
System.out.println("Ok");

//等待客户端的连接
System.out.println("waiting for invocations from clients");

} catch (RemoteException e) {
System.out.println("创建远程对象发生异常");
e.printStackTrace();
} catch (AlreadyBoundException e) {
System.out.println("发生重复绑定对象异常");
e.printStackTrace();
} catch (MalformedURLException e) {
System.out.println("发生URL畸形异常");
e.printStackTrace();
}
}
}

WarehouseClient实现

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
/**  
* @Title: WarehouseClient.java
* @Package warehouse
* @Description: 客户端获取指定服务器和远程对象的名字
* @author flo@t
* @date 2019年8月25日
* @version V1.0
*/
package warehouse;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

/**
* @ClassName: WarehouseClient
* @author: flo@t
* @Description: 客户端实现远程调用方法
*
*/
public class WarehouseClient {

/**
* @Title: main
* @Description: TODO
* @author: flo@t
* @param args
* @throws NamingException
* @throws RemoteException
*/
public static void main(String[] args) {
try {
//获得指定主机和指定端口上的注册表对象
Registry registry = LocateRegistry.getRegistry("localhost",8888);
//Returns a reference to the remote object Registry on the specified host and port.

//返回的是Remote接口类型,这里进行一个向下的转型
Warehouse centralWarehouse = (Warehouse) registry.lookup("central_warehouse");
String descr = "Toaster";
double price = centralWarehouse.getPrice(descr);
System.out.println(descr + ": " + price);
} catch (NotBoundException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}

}
}

运行截图,Server端:   Client端:    可以看到获得了服务端所绑定到RMI的对象的信息。下面给出RMI工作的流程图:          我的理解是在服务端对一个实例化的对象(实现了继承Remote接口的那个接口)在RMI注册表中进行绑定之后,客户端会构造一个请求的部分(理解为一个存根,属于Registry类型的一个实例化对象),对服务端进行请求。服务端在对自己的注册表进行查询之后,在存在该服务(对象)的前提下会返回要查询的信息。      对于客户端,核心是-----lookup()方法,该方法参数为你要请求的对象的名字(String name),之后会返回这个对象的引用。客户端所得到的结果实际上是在服务端那里执行完成的,服务端只是将这个结果返回给了请求者,借用Pupile师傅的说明:

1
2
3
4
5
1. rmi服务注册他的名字和IP到RMI注册中心(bind)
2. rmi客户端通过IP和名字去RMI注册中心找相应的服务(lookup)
3. rmi Stub序列化调用的方法和参数编组后传给rmi Skeleton(call)
4. rmi skeleton执行stub的逆过程,调用真实的server类执行该方法(invocation)
5. rmi skeleton将调用函数的结果返回给stub(return)

JDNI及注入原理(上)

代码如下,改自Pupile师傅的代码,使用的远程对象类的接口以及实现还是上面的Warehouse模型,这里不再重复。

jdniServer

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
/**  
*
* @Title jdniServer.java
* @Package Baby_JDNI
* @author flo@t
* @date 2019年9月6日
* @version V1.0
*/
package Baby_JDNI;

import java.util.Properties;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import javax.naming.Context;

import warehouse.WarehouseImpl;

/**
* JDNI服务端
* @ClassName jdniServer
* @author flo@t
*/
public class jdniServer {

/**
*
* @Title main
* @author flo@t
* @param args
*/
public static void main(String args[]) throws Exception {
// 配置 JNDI 默认设置
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://localhost:1099");

// 本地开启 1099 端口作为 RMI 服务,并以标识 "hello" 绑定方法对象
Registry registry = LocateRegistry.createRegistry(1099);
WarehouseImpl service2 = new WarehouseImpl();
registry.bind("hello", service2);
System.out.println("jdni server start...");
}
}

jdniClient

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
/**  
* JDNI客户端
* @Title jdniClient.java
* @Package Baby_JDNI
* @author flo@t
* @date 2019年9月6日
* @version V1.0
*/
package Baby_JDNI;

import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;

import warehouse.Warehouse;

/**
*
* @ClassName jdniClient
* @author flo@t
*/
public class jdniClient {

/**
*
* @Title main
* @author flo@t
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
// 配置 JNDI 默认设置
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://localhost:1099");
Context ctx = new InitialContext(env);
// JNDI 获取 RMI 上的方法对象并进行调用
Warehouse rHello = (Warehouse) ctx.lookup("hello");
String request = "Toaster";
System.out.println(rHello.getPrice(request));

}
}

   开启JDNI的服务端       开启客户端,并查询Toaster的价格,可以看到返回了价格。

但是,如果在客户端中设定一个Reference的类,设定敌手服务器地址和其上的一个恶意的类。当JDNI的服务端在它的本地没有找到你请求的对象时,就会对你设定的恶意对象发出请求,并在服务端执行返回结果。这样就达成了(RCE)远程代码执行的效果。具体怎么实现下篇再说。

CATALOG
  1. 1. Java的序列化
  2. 2. Java的自定义序列化与反序列化
  3. 3. 反序列化利用与JNDI注入和RMI
    1. 3.1. RMI
      1. 3.1.1. Warehouse接口定义以及实现
      2. 3.1.2. WarehouseServer实现
      3. 3.1.3. WarehouseClient实现
  4. 4. JDNI及注入原理(上)
    1. 4.1. jdniServer
    2. 4.2. jdniClient