问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
JNI、JNA、JNR的浅入浅出
如何不依赖java原生命令执行类去实现命令执行
JNI === JNI介绍 ----- JNI指的是Java Native Interface,是Java提供的一种机制,用于在Java虚拟机(JVM)上运行的应用程序与本地操作系统或其他本地库进行交互。 通过JNI,Java应用程序可以调用本地代码,实现了Java语言与本地代码的无缝集成。使用JNI,开发人员可以编写C或C++代码,将其编译为本地共享库,并从Java程序中动态加载这些库并调用其中的函数。通常情况下,JNI被用来访问操作系统提供的底层接口,比如文件系统、网络等功能。同时,JNI也可以让Java程序直接调用本地语言编写的库,比如图形界面库、数学计算库等。 JNI实现过程 ------- 具体步骤如下: 1、.java文件声明一个native方法 2、对native方法的.java文件使用javah进行编译,生成.h头文件 3、引用.h头文件,编写对应的c代码 4、根据目标系统不同,通过gcc/g++编译成.so或.dll链接库文件 5、编写一个新的Java类使用`System.loadLibrary`或`System.load`方法,加载链接库文件并调用文件中的方法 JNI具体实现 ------- 根据以上步骤进行详细的实现 1、.java文件声明一个native方法 ```java package com.company; public class Command { public native String exec(String cmd); } ``` 2、对native方法的.java文件使用javah进行编译,生成.h头文件 来到.java文件路径下,使用命令`javah -cp . com.company.Command`生成出对应的类头文件,头文件内容如下: ```java /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_company_Command */ #ifndef _Included_com_company_Command #define _Included_com_company_Command #ifdef __cplusplus extern "C" { #endif /* * Class: com_company_Command * Method: exec * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_company_Command_exec (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif ``` 这里可以看到有个`Java_com_company_Command_exec`的字符,前面的Java是固定的前缀,`CmdExec`是类名,最后面的`exec`是类中定义的方法名,该方法接受一个`jstring`类型的参数,并返回一个`jstring`类型的值。由于该方法被声明为静态方法,因此可以通过类名直接调用 而括号中的三个参数分别代表:`JNIEnv*`表示指向JNI环境的指针,`jclass`表示对象的类,`jstring`表示Java字符串类型。 以上关于定义的类型转换,可参考:[](https://blog.csdn.net/qq_25722767/article/details/52557235)[https://blog.csdn.net/qq\_25722767/article/details/52557235](https://blog.csdn.net/qq_25722767/article/details/52557235) 3、引用.h头文件,编写对应的c代码 以下c中实现了调用`popen`去执行系统命令,并将执行结果返回 ```cpp #include <jni.h> #include <stdlib.h> #include <stdio.h> #include <string> #include <iostream> #include "com_company_Command.h" using namespace std; JNIEXPORT jstring JNICALL Java_com_company_Command_exec(JNIEnv *env, jobject obj, jstring cmd) { const char *cmdStr = env->GetStringUTFChars(cmd, NULL); //将Java中的字符串转换为C风格的字符串 FILE *fp; char buffer[256]; fp = popen(cmdStr, "r"); // 执行系统命令 std::string result = ""; while (fgets(buffer, sizeof(buffer), fp)) { result += buffer; // 将命令输出添加到结果字符串中 } pclose(fp); env->ReleaseStringUTFChars(cmd, cmdStr); //释放字符串资源 return env->NewStringUTF(result.c_str()); // 将结果字符串转换为Java字符串 } ``` 4、根据目标系统不同,通过gcc编译成.so或.dll链接库文件 > 注意x64位系统对应x64位gcc 来到c目录下,使用g++命令编译成动态链接库,前提是需要提前装好编译环境如:gcc/g++ 需要指定jdk的include和win32文件 `g++ -I "%JAVA_HOME%\\include" -I"%JAVA_HOME%\\include\\win32" -shared -o cmd.dll Command.c` linux编译: `g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o cmd.so Command.cpp` 编译完成我们就可以使用这个动态链接库了 5、编写一个新的Java类使用`System.loadLibrary`或`System.load`方法,加载链接库文件并调用文件中的方法 ```cpp package com.company; public class Main { public static void main(String[] args) { System.loadLibrary("cmd"); Command command = new Command(); String ipconfig = command.exec("ipconfig"); System.out.println(ipconfig); } } ``` > 如果出现找不到链接库等报错,建议重新打开IDEA ![Untitled.png](https://shs3.b.qianxin.com/attack_forum/2023/04/attach-b9617a23d759ec92c2c62f7a3936b70b2a2a276a.png) 命令执行成功,这里不是调用java原生命令执行的方法,而是通过底层调用自写dll文件中封装的方法。 ### 加载链接库的经过 首先通过`System.loadLibrary`跟进到`Runtime.*getRuntime*().loadLibrary0(Reflection.*getCallerClass*(), libname)`,其中`Reflection.getCallerClass()`方法会返回当前调用该方法的类对象,`libname`则是要加载的cmd.dll本地库文件。这个方法会在运行时将cmd.dll加载到进程中,从而使得 Java 程序可以调用其中的函数。 ![Untitled (1).png](https://shs3.b.qianxin.com/attack_forum/2023/04/attach-1b82bac5dc7edd7de8fc71173ee97e734d437d96.png) `loadLibrary0`中将调用`ClassLoader.loadLibrary(fromClass, libname, false);`也是用来加载本地库文件的,其中的`fasle`表示并不会使用`System.loadLibrary()`方法进行本地库文件加载,该方法中将根据指定的类加载器`sun.misc.Launcher$AppClassLoader`从特定位置加载本地库文件cmd.dll,避免了对系统全局状态的依赖。 ![Untitled (2).png](https://shs3.b.qianxin.com/attack_forum/2023/04/attach-50381657bafb3f25cead9fb533b2ab584812e159.png) JNI利用场景 ------- 1、将自写dll和恶意war包绑定,通过tomcat后台war包上传等任意文件上传漏洞部署应用后,jsp或者class调用自写dll文件,即可实现命令执行 2、上传jsp或注入class,将dll文件十六进制编码释放并调用,即可实现命令执行 JNA === JNA介绍 ----- "JNA" 是 Java Native Access 的缩写,也是Java调用本地库的一种方式,它允许Java应用程序通过本地方法调用(Native Method Invocation,NMI)来访问本地共享库(DLL、SO等)。但是相对JNI,JNA提供了一个易于使用的Java API,简化了手动编写映射代码的过程,可以自动将Java方法映射到本地库函数,允许Java应用程序直接访问本地库。 JNA实现过程 ------- 具体步骤如下: 1、引用`com.sun.jna.Native`的jar包 2、编写一个新的Java类使用`Native.load()`方法,即可在IDEA或应用上调用Native方法 JNA具体实现 ------- 根据以上步骤进行详细实现 ```java import com.sun.jna.Library; import com.sun.jna.Native; public class CommandExecutor { public interface CLibrary extends Library { // windows下的共享库 CLibrary INSTANCE = (CLibrary) Native.loadLibrary("kernel32", CLibrary.class); int WinExec(String command); // linux下的共享库 // CLibrary INSTANCE = (CLibrary) Native.load("libc.so.6", CLibrary.class); // int system(String command); } public static void main(String[] args) { // 执行命令并获取返回值 String command = "calc"; int returnValue = CLibrary.INSTANCE.WinExec(command); // int returnValue = CLibrary.INSTANCE.system(command); // 打印返回值 System.out.println("Return Value: " + returnValue); } } ``` > 在本例中,"kernel32"代表C共享库,由于是windows操作系统提供的共享库,linux则使用"libc.so.6"共享库 1、在类中定义原生函数接口 `public interface CLibrary extends Library`语句定义了一个代理接口`CLibrary`,它继承自`Library`类。接口中声明了一个名为`INSTANCE`的常量,其类型为`CLibrary`,并使用`Native.loadLibrary("kernel32", CLibrary.class)`方法加载了名为"kernel32"的本地共享库,并将结果赋值给`INSTANCE`常量。 2、同时声明需要调用的函数 在`CLibrary`接口中声明了一个`WinExec`函数,该函数与C共享库中的`WinExec`函数具有相同的参数类型(即输入参数为字符串类型,返回值为整型),表示将要执行的系统命令`command`作为输入参数,函数将返回执行此命令后的状态码。 3、调用共享库函数 在`main`函数中,先定义了一个变量`command`用于存储将要执行的命令。使用`CLibrary.INSTANCE.system(command)`方法调用了C共享库中的`WinExec`函数,并将`command`作为输入参数传递给该函数。最后输出、打印执行命令后的返回值即状态码`returnValue`。 综上所述,这段代码的主要功能是使用JNA库直接调用`kernel32`共享库中的`WinExec`函数,实现执行操作系统命令并获取返回值 ![Untitled (5).png](https://shs3.b.qianxin.com/attack_forum/2023/04/attach-a92df3cf3276213bdbb72ae35d42a24a4a6caa3f.png) ### 加载链接库的经过 直接跟进到`com.sun.jna.Native.loadLibrary`,该方法用于加载JNA库的指定接口类的实现,并返回该实现的代理对象。它会创建一个名为”kernel32”的库处理程序`handler`,并使用该处理程序创建一个动态代理对象`proxy`,该代理对象实现了`CLibrary`接口中定义的所有方法。 ![Untitled (4).png](https://shs3.b.qianxin.com/attack_forum/2023/04/attach-fefdd650297efb9d5591c5ed4ad13f357a9c2802.png) 其中调用了`cacheOptions`方法实现了创建并缓存`Native`库的选项,其中将`(cls, proxy)`添加到`libraries Map`中,其中`cls`为传入的类`CommandExecutor$CLibrary`,`proxy`为其代理对象 ![Untitled (5).png](https://shs3.b.qianxin.com/attack_forum/2023/04/attach-343cfc4f2a6ae49f8931f37297da20b6c79eedea.png) 回到`CLibrary.INSTANCE.WinExec(command);`,该方法中通过`CommandExecutor$CLibrary.WinExec(java.lang.String)`反射调用相应Native库的`WinExec`方法并传入参数,实现native方法的命令执行 ![Untitled (6).png](https://shs3.b.qianxin.com/attack_forum/2023/04/attach-646cbc182e28d57e374f8c99a27b0c9488f4b23a.png) ![Untitled (7).png](https://shs3.b.qianxin.com/attack_forum/2023/04/attach-957d78f7ae98562884f33a562102df8661ebf03b.png) 基于JNA实现的pty4j ------------- ### pty4j介绍 pty4j是一个Java库,其提供了一个简单的API,可以轻松地启动一个子进程,并通过标准输入/输出流与其进行交互,就像在终端中一样。它还支持设置超时时间、绑定输出处理器以及获取子进程的退出状态等功能。 ### pty4j命令执行 本身pty4j主要用于远程连接,做远程终端管理的工具。但是意外发现它具备了基于JNA实现本地命令执行或处理文件系统的能力,并且不依赖jdk原生的runtime等命令执行类。 其利用环境比较苛刻,我下载的pty4j版本由于其本身采用了JDK11进行编译,那我就将Java运行版本也切换成JDK11,并且还需要结合其它的第三方库才能正常实现命令执行(根据报错,缺什么装什么)。 ```java import com.pty4j.PtyProcess; import com.pty4j.PtyProcessBuilder; import java.io.*; public class ptyTest { public static void main(String[] args) throws IOException { PtyProcessBuilder builder = new PtyProcessBuilder(new String[]{"cmd.exe","/c","calc.exe"}); //PtyProcessBuilder builder = new PtyProcessBuilder(new String[]{"/bin/bash","-c","ifconfig"}); PtyProcess process = builder.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while((line = reader.readLine()) != null){ System.out.println(line); } try { process.waitFor(); process.destroy(); } catch (InterruptedException e) { e.printStackTrace(); } process.destroy(); } } ``` 效果如下 ![Untitled (8).png](https://shs3.b.qianxin.com/attack_forum/2023/04/attach-c7a9052ac2a3f79e9349cb3333dc6e6e490498ac.png) ### 具体调用过程 首先创建一个传入命令执行的参数的`PtyProcessBuilder`对象,跟进到`start()`方法,将看到创建新的PTY进程时需要指定的选项 ![Untitled (9).png](https://shs3.b.qianxin.com/attack_forum/2023/04/attach-5f7f53dcdf064fe00cb88f66b6d2e9cfdf206087.png) 接着来到私有方法`WinPtyProcess`中,提取以上的配置选项,做下一步PTY执行的内容。在`com.pty4j.windows.WinPty`类中可以看到加载了pty4j在临时文件夹产生的winpty.dll,调用`INSTANCE.winpty_spawn`方法,后续的过程跟之前说的直接利用JNA一样,同样利用JNA反射调用了相应Native库的`winpty_spawn`方法实现了命令执行 ![Untitled (10).png](https://shs3.b.qianxin.com/attack_forum/2023/04/attach-f13b7b639d869ebcd6cfd7788defd3493d06f3b5.png) ![Untitled (11).png](https://shs3.b.qianxin.com/attack_forum/2023/04/attach-8d5bf9a744d419286f6dd18bfedbcd5029402626.png) 题外话 === 还有一种Native执行命令的技术叫`JNR`,它其实是一个开源项目,也允许Java程序调用本地库,但与JNA不同的是,JNR采用了一种更快、更轻量级的实现方式。JNR使用Java字节码生成技术,在运行时动态生成本地方法的映射代码,避免了JNI调用过程中的不必要开销。 实则JNR的不同就是作者重写了`jnr.ffi.Library.loadLibrary`方法,利用该方法去加载Windows的`kernel32`库,接着利用`kernel32`库的`CreateProcess`函数创建命令管道和进程,从而达到命令执行的效果。(有兴趣的话可以自己研究一下) 小结 == 以上内容主要出于”不依赖java原生命令执行类去实现命令执行“的想法出发,最为主流的技术非JNI莫属,同时发掘其它新的调用Native技术。不难发现,JNA和JNR都是为了简化Java与本地代码之间的交互而创建的工具库,它们都是基于JNI的思想进行扩展的。
发表于 2023-05-11 10:09:25
阅读 ( 5681 )
分类:
安全工具
2 推荐
收藏
0 条评论
请先
登录
后评论
w1nk1
12 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!