好久没写博客,,,因为考虑到没有扎实的掌握一门流行的编程语言,所以花了两个月左右的时间补了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 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 { private String name; private int age; public Person (String name, int age) { this .name = name; this .age = age; } @Override public String toString () { return "姓名: " + name + "、年龄: " + age; } } public class Baby_Serializable { private static final File SAVE_FILE = new File (".\\serial.Person" ); 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 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; 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(); } } } public class Defined_Serializable { private static final File SAVE_FILE = new File (".\\serial.Car" ); 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(); 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 package warehouse;import java.rmi.*;public interface Warehouse extends Remote { 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 package warehouse;import java.util.*;import java.rmi.*;import java.rmi.server.*;@SuppressWarnings("serial") public class WarehouseImpl extends UnicastRemoteObject implements Warehouse { private Map<String, Double> prices; public WarehouseImpl () throws RemoteException { 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 package warehouse;import java.rmi.registry.LocateRegistry;import java.rmi.Naming;import java.rmi.RemoteException;import java.net.MalformedURLException;import java.rmi.AlreadyBoundException;public class WarehouseServer { public static void main (String[] args) { try { System.out.print("Constructing server implemention..." ); WarehouseImpl centralWarehouse = new WarehouseImpl (); System.out.println("Ok" ); LocateRegistry.createRegistry(8888 ); System.setProperty("rmi:central_warehouse" , "127.0.0.1" ); System.out.print("Binding server implemention to 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 package warehouse;import java.rmi.NotBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class WarehouseClient { public static void main (String[] args) { try { Registry registry = LocateRegistry.getRegistry("localhost" ,8888 ); 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 package Baby_JDNI;import java.util.Properties;import java.rmi.registry.Registry;import java.rmi.registry.LocateRegistry;import javax.naming.Context;import warehouse.WarehouseImpl;public class jdniServer { public static void main (String args[]) throws Exception { Properties env = new Properties (); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); 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 package Baby_JDNI;import java.util.Properties;import javax.naming.Context;import javax.naming.InitialContext;import warehouse.Warehouse; public class jdniClient { public static void main (String[] args) throws Exception { 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); Warehouse rHello = (Warehouse) ctx.lookup("hello" ); String request = "Toaster" ; System.out.println(rHello.getPrice(request)); } }
开启JDNI的服务端
开启客户端,并查询Toaster
的价格,可以看到返回了价格。
但是 ,如果在客户端中设定一个Reference的类,设定敌手服务器地址和其上的一个恶意的类。当JDNI的服务端在它的本地没有找到你请求的对象时,就会对你设定的恶意对象发出请求,并在服务端执行返回结果。这样就达成了(RCE)远程代码执行的效果。具体怎么实现下篇再说。
v1.5.2