原來Java反序列化遠程執行漏洞這么簡單

2019-05-24 83881人圍觀 ,發現 7 個不明物體 數據安全新手科普

在這里我們對Java中反序列化問題引發的遠程代碼執行漏洞的原理進行介紹。為了簡化說明,在不引入第3方庫的前提下進行操作,希望能起到拋磚引玉的效果。

主要有3個部分組成:

Java的反省機制

Java的序列化處理

Java的遠程代碼執行

Java的反射與代碼執行

我們先看1個簡單的例子,使用Java調用計算器程序:

import java.io.IOException;

import java.lang.Runtime;

public class Test {

    public static void main(String[] args) {
        Runtime env = Runtime.getRuntime();
        String cmd = "calc.exe";
        try {
            env.exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

我們從java.lang包中導入Runtime類,之后調用其getRuntime方法得到1個Runtime對象,該對象可以用于JVM虛擬機運行狀態的處理。接著我們調用其exec方法,傳入1個字符串作為參數。

此時,將啟動本地計算機上的計算器程序。

下面我們通過Java的反省機制對上述的代碼進行重寫。通過Java的反省機制可以動態的調用代碼,而逃過一些服務端黑名單的處理:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test {

    public static void main(String[] args) {
        try {
            Class<?> cls = Class.forName("java.lang.Runtime");
            String cmd = "calc.exe";
            try {
                Method getRuntime = cls.getMethod("getRuntime", new Class[] {});
                Object runtime = getRuntime.invoke(null);
                Method exec = cls.getMethod("exec", String.class);
                exec.invoke(runtime, cmd);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (SecurityException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        } catch (ClassNotFoundException e1) {
            e1.printStackTrace();
        }
    }
}

上述代碼看起來很繁瑣,實際上并不是很難。首先,通過Class.forName傳入1個字符串作為參數,其返回1個Class的實例。而其作用是根據對應的名稱找到對應的類。

接著我們使用Class實例的getMethod方法獲取對應類的getRuntime方法,由于該類沒有參數,因此可以將其設置為null或使用匿名類來處理。

Method getRuntime = cls.getMethod("getRuntime", new Class[] {});

之后通過得到的方法的實例的invoke方法調用對應的類方法,由于沒有參數則傳入null即可。同理,我們再獲取到exec方法。

Java序列化處理

對于Java中的序列化處理,對應的類需要實現Serializable接口,例如:

import java.io.Serializable;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class Reader implements Serializable {
    private static final long serialVersionUID = 10L;

    private void readObject(ObjectInputStream stream) {
        System.out.println("foo...bar...");
    }

    public static byte[] serialize(Object obj) {
        //序列化對象
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream output = null;
        try {
            output = new ObjectOutputStream(out);
            output.writeObject(obj);
            output.flush();
            output.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
        return out.toByteArray();

    }

    public static Object deserialize(byte[] bytes) {
        //反序列化處理
        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
        ObjectInputStream input;
        Object obj = null;
        try {
            input = new ObjectInputStream(in);
            obj = input.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return obj;

    }

    public static void main(String[] args) {
        byte[] data = serialize(new Reader()); //對類自身進行序列化
        Object response = deserialize(data);
        System.out.println(response);
    }
}

在這里我們重寫了該類的readObject方法,用于讀取對象用于測試。其中比較重要的2個函數是serialize和deserialize,分別用于序列化和反序列化處理。

其中,serialize方法需要傳入1個對象作為參數,其輸出結果為1個字節數組。在該類中,其中的對象輸出流ObjectOutputStream主要用于ByteArrayOutputStream進行包裝,之后使用其writeObject方法將對象寫入進去,最后我們通過ByteArrayOutputStream實例的toByteArray方法得到字節數組。

而在deserialize方法中,需要傳入1個字節數組,而返回值為1個Object對象。與之前的序列化serialize函數類似,此時我們使用ByteArrayInputStream接收字節數組,之后使用ObjectInputStream對ByteArrayInputStream進行包裝,接著調用其readObject方法得到1個Object對象,并將其返回。

當我們運行該類時,將得到如下的結果:

foo...bar...

Java遠程通信與傳輸

為了實現Java代碼的遠程傳輸及遠程代碼執行,我們可以借助RMI、RPC等方式。而在這里我們使用Socket進行服務端及客戶端處理。

首先是服務器端,監聽本地的8888端口,其代碼為:

import java.net.Socket;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
public class Server {

    public static void main(String[] args) throws ClassNotFoundException {
        int port = 8888;
        try {
            ServerSocket server = new ServerSocket(port);
            System.out.println("Server is waiting for connect");
            Socket socket = server.accept();
            InputStream input = socket.getInputStream();
            byte[] bytes = new byte[1024];
            int length = 0;
            while((length=input.read(bytes))!=-1) {
                String out = new String(bytes, 0, length, "UTF-8");
                System.out.println(out);
            }
            input.close();
            socket.close();
            server.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

我們通過傳入1個端口來實例化ServerSocket類,此時得到1個服務器的socket,之后調用其accept方法接收客戶端的請求。此時,得到了1個socket對象,而通過socket對象的getInputStream方法獲取輸入流,并指定1個長度為1024的字節數組。接著調用socket的read方法讀取那么指定長度的字節序列,之后通過String構造器將字節數組轉換為字符串并輸出。這樣我們就得到了客戶端傳輸的內容。

而對于客戶端器,其代碼類似如下:

import java.io.IOException;
import java.net.Socket;
import java.io.OutputStream;

public class Client {

    public static void main(String[] args) {
        String host = "192.168.1.108";
        int port = 8888;
        try {
            Socket socket = new Socket(host, port);
            OutputStream output = socket.getOutputStream();
            String message = "Hello,Java Socket Server";
            output.write(message.getBytes("UTF-8"));
            output.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在客戶端,我們通過Socket對象傳遞要連接的IP地址和端口,之后通過socket對象的getOutputStream方法獲取到輸出流,用于往服務器端發送輸出。由于這里只是演示,使用的是本地的主機IP。而在實際應用中,如果我們知道某個外網主機的IP及開放的端口,如果當前主機存在對應的漏洞,也是可以利用類似的方式來實現的。

這里我們設置要傳輸的內容為UTF-8編碼的字符串,俄日在輸出流的write方法中通過字符串的getBytes指定其編碼,從而將其轉換為對應的字節數組進行發送。

正常情況下,我們運行服務器后再運行客戶端,在服務器端可以得到如下輸出:

Server is waiting for connect
Hello,Java Socket Server

Java反序列化與遠程代碼執行

下面我們通過Java反序列化的問題來實現遠程代碼執行,為了實現遠程代碼執行,我們首先在Reader類中添加1個malicious方法,其代碼為:

    ...
    public Object malicious() throws IOException {
        Runtime.getRuntime().exec("calc.exe");
        System.out.println("Hacked the Server...");
        return this;
    }

在該方法中我們使用之前的介紹調用宿主機器上的計算器程序,然后輸出1個相關信息,最后返回當前類。

之后是對服務器端的代碼進行如下的修改:

...

while((length=input.read(bytes))!=-1) {
    Reader obj = (Reader) Reader.deserialize(bytes);
    obj.malicious();
}

我們在接收到客戶端對應的字符串后對其進行反序列處理,之后調用某個指定的函數,從而實現遠程代碼的執行。

而在客戶端,我們需要對其進行序列化處理:

Reader reader = new Reader();
byte[] bytes = Reader.serialize(reader);
String message = new String(bytes);
output.write(message.getBytes());

下面我們在宿主機器上運行服務器端程序,之后在本地機器上運行客戶端程序,當客戶端程序執行時,可以看到類似如下的結果:

圖片.png

可以看到,我們成功的在宿主機器上執行了對應的命令執行。

總結

為了實現通過Java的反序列問題來實現遠程代碼執行的漏洞,我們需要編寫1個有惡意代碼注入的序列化類。之后在客戶端將惡意代碼序列化后發送給服務器端,而服務器端需要調用我們期望的方法,從而觸發遠程代碼執行。

為了避免服務器端進行一些安全處理,我們可以采用反射的方式來逃逸其處理。

這里只是1個簡化的過程,更加實用的過程可以參考Apache Common Collections的問題導致的Weblogic漏洞CVE-2015-4852及Jboss的漏洞CVE-2015-7501。

*本文作者:yafeile,轉載請注明來自FreeBuf.COM

發表評論

已有 7 條評論

取消
Loading...
yafeile

這家伙太懶,還未填寫個人描述!

1 文章數 2 評論數

特別推薦

推薦關注

填寫個人信息

姓名
電話
郵箱
公司
行業
職位
css.php jizzz