好久没写博客,,,因为考虑到没有扎实的掌握一门流行的编程语言,所以花了两个月左右的时间补了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对象的序列化以及反序列化。主要使用类及函数:
- ObjectOutputStream (接受OutputStream及其子类的对象)
- ObjectInputStream (接受InputStream及其子类的对象)
- FileOutputStream (接受File类型,打开一个文件输出流)
- FileInputStream (接受File类型,打开一个文件输入流)
- 序列化方法:writeObject()
- 反序列化方法:readObject()
代码下:
1 | import java.io.File; |
反序列输出结果: 并且在指定的路径下生成这个对象的序列化文件 在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;
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;
}
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 | private void readObject(ObjectInputStream in) throws Exception { |
在正常输出读取对象成功后,会运行计算器。
反序列化利用与JNDI注入和RMI
1 | 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. |
简单来说的话,大家应该都清楚RPC(Remote Procedure Call)的作用,RMI相当于就是Java版本的RPC,可以实现远程对一个实例化的对象的使用。
而JNDI的作用提供了一组通用的接口可供应用很方便地去访问不同的后端服务,例如 LDAP、RMI、CORBA等。
在Java中为了能够更方便的管理、访问和调用远程的资源对象,常常会使用 LDAP和RMI等服务来将资源对象或方法绑定在固定的远程服务端,供应用程序来进行访问和调用。下面分别对这两者进行实例与解释。
RMI
如下代码大部分来自《Java核心技术第九版》的RMI部分,我稍加了修改,主要功能是实现远程对仓库商品的一个查询
Warehouse接口定义以及实现
1 | /** |
对应的实现如下: 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
*
*/
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);
}
/**
* 接受用户要查询的商品的名称,返回商品的价格
*/
public double getPrice(String description) throws RemoteException {
Double price = prices.get(description);
return price == null ? 0 : price;
}
}
WarehouseServer实现
1 | /** |
WarehouseClient实现
1 | /** |
运行截图,Server端: Client端: 可以看到获得了服务端所绑定到RMI的对象的信息。下面给出RMI工作的流程图: 我的理解是在服务端对一个实例化的对象(实现了继承Remote接口的那个接口)在RMI注册表中进行绑定之后,客户端会构造一个请求的部分(理解为一个存根,属于Registry类型的一个实例化对象),对服务端进行请求。服务端在对自己的注册表进行查询之后,在存在该服务(对象)的前提下会返回要查询的信息。 对于客户端,核心是-----lookup()方法,该方法参数为你要请求的对象的名字(String name),之后会返回这个对象的引用。客户端所得到的结果实际上是在服务端那里执行完成的,服务端只是将这个结果返回给了请求者,借用Pupile师傅的说明:
1 | 1. rmi服务注册他的名字和IP到RMI注册中心(bind) |
JDNI及注入原理(上)
代码如下,改自Pupile师傅的代码,使用的远程对象类的接口以及实现还是上面的Warehouse模型,这里不再重复。
jdniServer
1 | /** |
jdniClient
1 | /** |
开启JDNI的服务端
开启客户端,并查询Toaster
的价格,可以看到返回了价格。
但是,如果在客户端中设定一个Reference的类,设定敌手服务器地址和其上的一个恶意的类。当JDNI的服务端在它的本地没有找到你请求的对象时,就会对你设定的恶意对象发出请求,并在服务端执行返回结果。这样就达成了(RCE)远程代码执行的效果。具体怎么实现下篇再说。