再谈RMI


0x00 前言

前一篇关于RMI的文章写的稍微粗糙了些,写这一篇再巩固一下RMI的内容并做一些延伸。

0x01 一些细节

网上关于RMI服务端和客户端编写的文章方法很多,我上一篇是分为接口、服务端、客户端三个部分编写的,当时是将rmi registry和rmi server都写到了服务端类中:

package com.Anchor;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer implements Hello {
    public static void main(String[] args) {
        try {
            RMIServer obj = new RMIServer();
            Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 8888);//该语句执行后会运行一个 rmi server,监听本地8888端口等待client的请求
            Registry registry = LocateRegistry.createRegistry(1099); // 该语句执行后会创建并启动 rmi registry,监听在本地 1099 端口
            //exportObject() 方法返回结果为 remote object stub (代理对象,实现了与 Hello 接口同样的方法,包含 rmi server 的 host、port 信息)
            registry.bind("Hello", stub); // 将 stub 注册到 registry,并与 name Hello 绑定

        } catch (Exception e) {
            System.out.println("Server Exception: " + e.toString());
            e.printStackTrace();
        }
    }

    public String sayHello() throws RemoteException {
        return "Hello, World";
    }
}

因为是在本地环境下,为了避免麻烦,就直接写到了一起。但是在低版本的jdk中,注册中心rmi registry与rmi server是可以分离的,甚至可以是运行在不同的主机上。但官方文档中是这样说:

出于安全原因,应用程序只能绑定或取消绑定到在同一主机上运行的注册中心。这样可以防止客户端删除或覆盖服务器的远程注册表中的条目。但是,查找操作是任意主机都可以进行的。

也就是说不建议将rmi registry和rmi server分离到不同主机来运行,我们后文会提到为什么会有这样的安全考虑。

另外该段代码是使用UnicastRemoteObject的exportObject方法来导出远程对象,并监听8888端口运行一个 rmi server。

Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 8888)

接着是通过registry.bind("Hello", stub)将”Hello”与stub的映射关系绑定到rmi registry上,这种绑定方式不需要书写完整的RMI URL,只需写对象名称”Hello”即可。

这是我参考的官网上的做法,但网上的大部分文章更多是利用下面的方式:

package com.Anchor;


import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer2 {
    public class RemoteHello extends UnicastRemoteObject implements Hello{
        protected RemoteHello() throws RemoteException{
            super();
        }

        public String sayHello() throws RemoteException{
            return "Hello, World";
        }
    }
    private void start() throws Exception{
        RemoteHello rh = new RemoteHello();
        LocateRegistry.createRegistry(1099);
        Naming.bind("rmi://127.0.0.1:1099/Hello", rh);
    }

    public static void main(String[] args) throws Exception{
        RMIServer2 rs2 = new RMIServer2();
        rs2.start();
    }
}

这里使用了java.rmi.Naming类,该类又叫做命名服务,提供了一种方便的方式来绑定和查找远程对象。可以看一下它的bind方法的源码,发现它是通过解析URI绑定远程对象,将URI拆分成主机、端口和远程对象名称,最后使用的仍是Registry类的bind来进行操作的:

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

我们在rmi client中也可以使用Naming类的lookup方法来查找远程对象(可以看到client端简洁了许多):

package com.Anchor;

import java.rmi.Naming;

public class RMIClient {

    public static void main(String[] args) throws Exception {
        Hello hello = (Hello)
                Naming.lookup("rmi://192.168.0.107:1099/Hello");
        System.out.println(hello.sayHello());
    }
}

上面说到,相比于直接使用Registry类, Naming类在绑定和查找操作时需要的是一个URL,需要说明的时,很多文章都指出该URL的形式一定要下面这样:

rmi://host:port/objName

但其实并不是。首先,其中的rmi:是可以省略的,也就是可以写成这样://host:port/objName(但不要少了前面的双斜杠,否则会报错);其次,如果服务端或者客户端和RMI registry在同一主机上运行,那么hostport也可以省略,也就是直接远程对象名objName

另外,对于绑定方法,在Naming类和Registry类,除了bind还要rebind,两者区别如下:

rebind是指“重绑定”,如果“重绑定”时rmi registry已经有了这个服务name的存在,则之前所绑定的Remote Object将会被替换;而bind在执行时如果“绑定”时rmi registry已经有这个服务name的存在,则系统会抛出错误。

所以除非有特别的业务要求,一般建议使用rebind方法进行Remote Object绑定。

0x02 RMI利用

讲了这么多,只是涉及了rmi具体通信原理以及如何实现,下面来从利用的层面讲讲。

关于利用的方法,可以分别从攻击rmi registry、攻击rmi server、攻击rmi client角度来实现,由于篇幅和本人水平有限(对java反序列化的知识掌握不深),本文就略带介绍一下利用方式,具体实现在以后的文章中进行。

攻击rmi registry

也许你也注意到上文中提到的,rmi server可以通过bind、rebind等方法申请在rmi registry上进行绑定注册,那么想想是否可以伪造成一个远程的rmi server注册一个恶意远程服务上去呢?

前文也说过,在低版本的jdk本中(8u121),rmi registry和rmi server可以不在一台服务器上,此时是可以利用这种方法攻击注册中心的。但是通常情况下,我们遇到的场景都是高版本的jdk,此时这种攻击注册中心的方式就不太适用。

由于我没能下载到低版本的jdk,部分源码片段我就截取了网上的文章进行说明。

我们先来看前面贴上的java.rmi.Naming#bind方法的源码,其中它会将URL拆解后的结果作为参数放到getRegistry方法中从而得到一个registry:

Registry registry = getRegistry(parsed);

那么查看这个getRegistry方法源码:

private static Registry getRegistry(ParsedNamingURL parsed)
    throws RemoteException
{
    return LocateRegistry.getRegistry(parsed.host, parsed.port);
}

可以看到这里归根到底调用的还是LocateRegistry类的getRegistry方法,继续跟踪进去:

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) {
            // If host is blank (as returned by "file:" URL in 1.0.2 used in
            // java.rmi.Naming), try to convert to real local host name so
            // that the RegistryImpl's checkAccess will not fail.
            try {
                host = java.net.InetAddress.getLocalHost().getHostAddress();
            } catch (Exception e) {
                // If that failed, at least try "" (localhost) anyway...
                host = "";
            }
        }

        /*
         * Create a proxy for the registry with the given host, port, and
         * client socket factory.  If the supplied client socket factory is
         * null, then the ref type is a UnicastRef, otherwise the ref type
         * is a UnicastRef2.  If the property
         * java.rmi.server.ignoreStubClasses is true, then the proxy
         * returned is an instance of a dynamic proxy class that implements
         * the Registry interface; otherwise the proxy returned is an
         * instance of the pregenerated stub class for RegistryImpl.
         **/
        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);
    }

这里getRegistry方法有好几种重载方式,前面所说的传入不同形式的URL,正好对应了这几种重载方式,我这里粘贴的是最终调用的这个核心的getRegistry方法。前面的if判断是处理port和host的,然后分别实例化了liveRef和RemoteRef对象,这一段过程应该是用过来处理网络通信相关的,不用管,直接定位最后的返回语句,调用了createProxy方法。关于这个方法,要涉及到动态代理的内容(给自己挖个坑,以后研究),它会根据传入的class对象,返回一个代理,也就是说,这里传入RegistryImpl.class后,得到的其实是一个RegistryImpl_Stub对象,即远程的Registry接口在本地的代理。

我们再回到Naming类的bind方法中,接下来就是执行registry.bind(parsed.name, obj);语句,这里调用了registrybind方法,其实是调用的RegistryImpl_Stub类中的bind方法,我们定位到该类的bind方法:

public void bind(String var1, Remote var2) throws AccessException, AlreadyBoundException, RemoteException {
    try {
        StreamRemoteCall var3 = (StreamRemoteCall)this.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);
        }

        this.ref.invoke(var3);
        this.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);
    }
}

前面处理一些约定好的数据,比如host、port以及要绑定的对象和对象名称,我们直接定位到invoke那一句,invoke这里会把请求发出去,这里会调用Skeleton代理即sun.rmi.registry.RegistryImpl_Skel类中的dispatch方法来处理请求,需要注意的是,在这里之后是无法通过断点进入到sun.rmi.registry.RegistryImpl_Skel#dispatch方法中的,看一篇文章说是在bind函数执行处sun.rmi.registry.RegistryImpl#bind下一个断点,直接运行就可以得到调用栈,再回去找就行了。

我们定位查看重要逻辑代码:

switch(var3) {
            case 0:
                try { //bind方法
                    var11 = var2.getInputStream();
                    // readObject反序列化触发
                    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: //list()方法
                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 {  // lookup()方法
                    var10 = var2.getInputStream();
                    // readObject反序列化触发
                    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 { // rebind()方法
                    var11 = var2.getInputStream();
                    //readObject反序列化触发
                    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 { //unbind()方法
                    var10 = var2.getInputStream();
                    //readObject反序列化触发
                    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");
            }

这里var3类型是int型,取值从0到4,依次对应bind/rebind/unbind/lookup/list等请求。从这个switch语句我们可以得知,Registry注册中心能够接收bind/rebind/unbind/lookup/list等请求,而在接收五类请求方法的时候,只有我们bind,rebind,unbind和lookup方法进行了反序列化数据调用readObject函数(再给自己挖个坑,后面补个java反序列化的内容),可能导致直接触发了反序列化漏洞产生。

需要说明的是lookup是从rmi client角度攻击rmi registry, 而bind/rebind/unbind是从rmi server角度攻击rmi registry

由于无法下载到对应的jdk版本,这里我无法演示,可以具体看看这个文章是如何利用该处反序列化进行实际操作的。

高版本的jdk中,进行了如下修复:

在8u121之后,在bind方法里面增加了一个checkAccess方法,该方法会检查是否为localhost,不是则会抛出下面这个异常:

catch (PrivilegedActionException var4) {
    throw new AccessException(var0 + " disallowed; origin " + var2 + " is non-local host");
}

不过我们看源码就知道反序列化语句readObject其实在bind调用之前就执行了,并没有什么用。

然后在8u141修改为在RegistryImpl_Skel中执行readObject之前就执行了checkAccess方法,这样bind,rebind,unbind就没失效了。

以我现在手上的java为例(java版本为1.8.0_391),sun.rmi.registry.RegistryImpl_Skel类的dispatch方法中的switch语句对应处理bind请求的片段如下:

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

                try {
                    var10 = (ObjectInputStream)var7.getInputStream();
                    var8 = SharedSecrets.getJavaObjectInputStreamReadString().readString(var10);
                    var81 = (Remote)var10.readObject();
                } catch (IOException | ClassNotFoundException | ClassCastException var78) {
                    var7.discardPendingRefs();
                    throw new UnmarshalException("error unmarshalling arguments", var78);
                } finally {
                    var7.releaseInputStream();
                }

                var6.bind(var8, var81);

                try {
                    var7.getResultStream(true);
                    break;
                } catch (IOException var77) {
                    throw new MarshalException("error marshalling return", var77);
                }

可以看到第一句就调用了checkAccess进行了限制。

0x03 小结

写这篇时我结合了网上的文章再看的源码,但是还是感觉其中的过程有些绕,对于rmi利用这块,有很多有待去学习。

参考文章:

《java安全漫谈》

RMI机制


文章作者: anch0r
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 anch0r !
  目录