前言 实际解一道题,找到链子也只是基础,具体的攻击手段也许才是核心
RMI 认识 对于这个概念,我们需要建立基本的认识 RMI 全称 Remote Method Invocation(远程方法调用),即在一个 JVM 中 Java 程序调用在另一个远程 JVM 中运行的 Java 程序,这个远程 JVM 既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。 具体到漏洞利用阶段,我们可以远程开一个vps,准备一个恶意类 然后利用靶机进行远程调用,实现RCE RMI整个交互过程依赖于什么协议呢? JRMP(Java Remote Message Protocol,Java 远程消息交换协议),该协议为 Java 定制,要求服务端与客户端都为 Java 编写。
1 2 3 4 Server ———— 服务端:服务端通过绑定远程对象,这个对象可以封装很多网络操作,也就是 Socket Client ———— 客户端:客户端调用服务端的方法 因为有了 C/S 的交互,而且 Socket 是对应端口的,这个端口是动态的,所以这里引进了第三个 RMI 的部分 ———— Registry 部分。 Registry ———— 注册端;提供服务注册与服务获取。即 Server 端向 Registry 注册服务,比如地址、端口等一些信息,Client 端从 Registry 获取远程对象的一些信息,如地址、端口等,然后进行远程调用。
这么理解即可,接下来,我们本地搭一下全过程
本地搭建 服务端概要 明确三个部分
一个继承了java.rmi.Remote 的接⼝,其中定义我们要远程调⽤用的函数,⽐如这里的 hello()
一个实现接口的类
一个主类,用于创建Rigistry,并将上述类实例化绑定到一个地址中
服务端代码 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 package org.vulhub;import java.rmi.Naming;import java.rmi.Remote;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.server.UnicastRemoteObject;public class RMIServer { public interface IRemoteHelloWorld extends Remote { public String hello () throws RemoteException; } public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld { protected RemoteHelloWorld () throws RemoteException { super (); } public String hello () throws RemoteException { System.out.println("call from" ); return "Hello world" ; } } private void start () throws Exception { RemoteHelloWorld h = new RemoteHelloWorld (); LocateRegistry.createRegistry(1099 ); Naming.rebind("rmi://127.0.0.1:1099/Hello" , h); } public static void main (String[] args) throws Exception { new RMIServer ().start(); } }
客户端代码 1 2 3 4 5 6 7 8 9 10 11 12 package org.vulhub;import java.rmi.Naming;public class Client { public static void main (String[] args) throws Exception { RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming.lookup("rmi://127.0.0.1:1099/Hello" ); String ret = hello.hello(); System.out.println( ret); } }
运行 成功调用
分析 wireshark 抓流量包 可以看到流量的移动 总结如下
1 2 3 实际建⽴了两次 TCP 连接,第一次是去连 1099 端口的;第二次是由服务端发送给客户端的。 在第一次连接当中,是客户端连 Registry 的,在其中寻找 Name 为 hello 的对象,这个对应数据流中的 Call 消息;然后 Registry 返回⼀个序列化的数据,这个就是找到的 Name=Hello 的对象,这个对应数据流中的ReturnData消息。 到了第二次连接,服务端发送给客户端 Call 的消息。客户端反序列化该对象,发现该对象是⼀个远程对象,地址在 172.17.88.209:24429,于是再与这个地址建⽴ TCP 连接;在这个新的连接中,才执⾏真正远程⽅法调⽤,也就是 sayHello()
就这些了() 看了看流量包是这么个意思
调试跟进 学rmi总得跟一个吧 关注三块
打断点、分析流量理解三者间的逻辑即可,便于我们后续针对细节的攻击
创建 开始 我们看到调试到UnicastRemoteObject的构造函数 port被传入了0 exportObject() 是一个静态函数,它就是主要负责将远程服务发布到网络上 我们利用继承,即不用手动导入了 继续跟进构造方法 调用了UnicastServerRef构造方法 继续调用
1 2 3 public LiveRef (ObjID objID, int port) { this (objID, TCPEndpoint.getLocalEndpoint(port), true ); }
第一个参数 ID,第三个参数为 true,所以我们重点关注一下第二个参数。 跟进看一下这个方法实现什么功能
1 2 3 public TCPEndpoint (String host, int port) { this (host, port, null , null ); }
一个发请求的类,继续跟进this 查看具体创建过程 进入LiveRef 创建的过程 完毕之后跟进exportObject()方法 直到stub的出现
1 stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
而这个stub RMI 先在 Service 的地方,也就是服务端创建一个 Stub,再把 Stub 传到 RMI Registry 中,最后让 RMI Client 去获取 Stub。 可想而知它的重要性,我们跟进它的创建过程
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 public static Remote createProxy (Class<?> implClass, RemoteRef clientRef, boolean forceStubUse) throws StubNotFoundException { Class<?> remoteClass; try { remoteClass = getRemoteClass(implClass); } catch (ClassNotFoundException ex ) { throw new StubNotFoundException ( "object does not implement a remote interface: " + implClass.getName()); } if (forceStubUse || !(ignoreStubClasses || !stubClassExists(remoteClass))) { return createStub(remoteClass, clientRef); } final ClassLoader loader = implClass.getClassLoader(); final Class<?>[] interfaces = getRemoteInterfaces(implClass); final InvocationHandler handler = new RemoteObjectInvocationHandler (clientRef); try { return AccessController.doPrivileged(new PrivilegedAction <Remote>() { public Remote run () { return (Remote) Proxy.newProxyInstance(loader, interfaces, handler); }}); } catch (IllegalArgumentException e) { throw new StubNotFoundException ("unable to create proxy" , e); } }
这里在创建动态代理 继续打断点,到了
1 2 3 4 5 Target target = new Target (impl, this , stub, ref.getObjID(), permanent); ref.exportObject(target); hashToMethod_Map = hashToMethod_Maps.get(implClass); return stub;
这里在进行一个封装 在这段过程里打断点仔细分析一下 这里就不讲发生啥了,感兴趣自己调一下 跟进到TCPTransport.java#exportObject 这里关键调用了一个listen方法,只讲结果 多出了一个随机化的端口 如 大佬是这么说的 发布之后的记录就没跟进了()
1 2 3 用 exportObject() 指定到发布的 IP 与端口,端口的话是一个随机值。至始至终复杂的地方其实都是在赋值,创建类,进行各种各样的封装,实际上并不复杂。 还有一个过程就是发布完成之后的记录,理解的话,类似于日志就可以了,这些记录是保存到静态的 HashMap 当中。 这一块是服务端自己创建远程服务的这么一个操作,所以这一块是不存在漏洞的。
我们抓个流量包看看 是正确的
注册 我们把这个类放到这个端口上之后,就要开始注册操作,跟进 细节就不说了,总之就是不断跟进,主体还是和上面的分析是一致的,不过只是几点细微的差距 比如 创建注册中心是走进到 createStub(remoteClass, clientRef); 进去的,而发布远程对象则是直接创建动态代理的,等
1 2 总结一下比较简单,注册中心这里其实和发布远程对象很类似,不过多了一个持久的对象,这个持久的对象就成为了注册中心。 绑定的话就更简单了,一句话形容一下就是 hashTable.put(IP, port)
客户端调用 这一块好好跟一下,看看,漏洞为什么会产生,这也是本次学习的核心
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 public static Registry getRegistry (String host, int port, RMIClientSocketFactory csf) throws RemoteException { Registry registry = null ; if (port <= 0 ) port = Registry.REGISTRY_PORT; if (host == null || host.length() == 0 ) { try { host = java.net.InetAddress.getLocalHost().getHostAddress(); } catch (Exception e) { host = "" ; } } LiveRef liveRef = new LiveRef (new ObjID (ObjID.REGISTRY_ID), new TCPEndpoint (host, port, csf, null ), false ); RemoteRef ref = (csf == null ) ? new UnicastRef (liveRef) : new UnicastRef2 (liveRef); return (Registry) Util.createProxy(RegistryImpl.class, ref, false ); }
这里同样ref,封装的是 127.0.01:1099 继续跟进,总之会进入 UnicastRef#invoke方法里面 再跟进call.executeCall()方法 再跟进getDGCAckHandler()方法 在这里存在一个反序列化
1 in 就是数据流里面的东西。这里获取异常的本意应该是在报错的时候把一整个信息都拿出来,这样会更清晰一点,但是这里就出问题了 ———— 如果一个注册中心返回一个恶意的对象,客户端进行反序列化,这就会导致漏洞。这里的漏洞相比于其他漏洞更为隐蔽。
当然这里还只是进行连接,到执行方法那一步呢? 先在 RemoteObjectInvocationHandler 类下的 invoke() 方法的 if 判断里面打个断点,这样才能走进去,否则是debug不进去的 跟进invokeRemoteMethod方法
1 2 return ref.invoke((Remote) proxy, method, args, getMethodHash(method));
再跟进 UnicastRef#invoke方法(之前调过) rmi处理网络请求的漏洞就藏在里面 跟进 marshalValue() 这里out.writeObject(value);
然后再走 unmarshalValueSee方法,因为现在我们传进去的类型是 String,不符合上面的一系列类型,这里会进行反序列化的操作,把这个数据读回来,这里是存在入口类的攻击点的 也就是在执行方法的时候,走了两种反序列化入口 流程
1 分为三步走,先获取注册中心,再查找远程对象,查找远程对象这里获取到了一个 ref,最后客户端发出请求,与服务端建立连接,进行通信。
注册中心干什么 这里牵扯到客户端打注册中心的打法 这里选择打Transport.java,打在Target上 跟进dispatch方法 跟进ldDispatch方法 跟进RegistryImpl_Skel#dispatch方法
1 2 3 4 5 6 7 8 9 10 11 12 13 我们与注册中心进行交互可以使用如下几种方式: list bind rebind unbind lookup 这几种方法位于 RegistryImpl_Skel#dispatch 中,也就是我们现在 dispatch 这个方法的地方。 如果存在对传入的对象调用 readObject 方法,则可以利用,dispatch 里面对应关系如下: 0->bind 1->list 2->lookup 3->rebind 4->unbind
看一下哪些会反序列化即可 除了 list 都可 注册中心就是处理 Target,进行 Skel 的生成与处理 漏洞点是在 dispatch 这里,存在反序列化的入口类。这里可以结合 CC 链子打的
服务端做了什么呢? 这里也藏着不少漏洞 打两个断点 和之前差不多,也是进入循环当中的 unmarshalValue() 方法,这里和我们之前说的一样,是存在漏洞的。 这里传入hello,序列化进去,反序列化出来
DGC的过程 DGC:是自动创建的一个过程,用于清理内存。
这个就不跟进了,总的反序列化漏洞和之前很类似
小结 整体慢调了下,虽然有点粗糙() 大体了解了下整个运作
作者还讲了一篇关于的攻击手段,暂且搁浅了,内容比较多,实际做题时再回头看看
参考 https://drun1baby.top/2022/07/19/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BRMI%E4%B8%93%E9%A2%9801-RMI%E5%9F%BA%E7%A1%80/#%E5%B0%8F%E7%BB%93%E4%B8%80%E4%B8%8B-DGC-%E7%9A%84%E8%BF%87%E7%A8%8B