Java 内存马要处理 HTTP 请求,必须进入服务器真实执行链。Servlet、Filter 和 Listener 名字相似,触发方式却完全不同:Servlet 是请求终点,Filter 包围请求处理过程,Listener 等待特定事件发生。
先看懂这条链,才能判断一个运行时对象是否可以接收请求、为什么一直存活,以及清除时应该断开哪段引用。
本文实验只增加响应头和日志,不读取命令参数,不执行系统命令。
一、一次请求经过了什么
以传统 Java Web 应用为例,请求进入 Tomcat 后大致经过:
1 | 客户端 |
三个 Servlet 规范组件的位置不同:
| 组件 | 触发方式 | 能否直接处理请求 | 常见持有位置 |
|---|---|---|---|
| Servlet | URL 映射命中 | 可以 | Web 应用注册表 |
| Filter | URL 或 Servlet Name 映射命中 | 可以拦截、放行或提前返回 | Filter 注册表与过滤链 |
| Listener | Context、Request、Session 等事件发生 | 取决于监听的事件 | Web 应用监听器集合 |
Listener 不是固定处于 Filter 前面或后面。ServletContextListener 只关心应用启动与销毁,ServletRequestListener 才会在请求创建和销毁时回调,HttpSessionListener 处理的是会话事件。
如果应用使用 Spring MVC,请求链还会继续细分:
1 | FilterChain |
因此,Spring Controller、Servlet Filter 和 Tomcat Valve 不是同一种入口。它们位于不同层,由不同对象保存,也需要用不同方式枚举和清除。
二、用最小实验看见 FilterChain
下面使用 Jakarta Servlet API,适用于 Tomcat 10 及之后的版本。Tomcat 9 及更早版本通常使用 javax.servlet.*,替换包名即可,不能在同一个应用中混用两套 API。
先创建一个只增加响应头的 Filter:
1 | package lab; |
再创建请求终点:
1 | package lab; |
部署应用后访问:
1 | curl -i http://127.0.0.1:8080/app/demo |
正常结果同时包含:
1 | HTTP/1.1 200 |
响应头来自 Filter,响应体来自 Servlet,证明一次请求先进入 Filter,再由 chain.doFilter() 交给后续组件。若 Filter 不再调用 chain.doFilter(),后面的 Servlet 就不会执行;内存马也正可能利用这个位置读取请求、修改响应或提前截断业务链。
三、组件为什么能够持续工作
Servlet 组件不是每次请求临时创建。应用启动时,容器完成类发现、实例创建、初始化和映射装配;请求到达后复用这些对象;应用停止时再执行销毁流程。
1 | 发现组件 |
容器长期持有组件,解释了两个关键现象:
- 局部注入代码执行结束后,已注册对象仍然可以继续工作;
- 只删除创建对象时使用的临时文件,不会自动移除容器中的实例和映射。
标准 Servlet API 允许在应用初始化阶段动态注册组件。例如 ServletContainerInitializer.onStartup() 中可以调用:
1 | FilterRegistration.Dynamic registration = |
上下文初始化完成后,标准动态注册窗口通常已经关闭,再调用 addFilter()、addServlet() 等方法会抛出 IllegalStateException。运行期注入之所以依赖 Tomcat、Jetty 或 Undertow 的内部对象,就是为了绕过标准 API 的生命周期限制,直接修改已经运行的注册表或处理链。
一次外部请求也不一定只进入一次 Filter:
| DispatcherType | 场景 | 是否进入 Filter |
|---|---|---|
REQUEST |
客户端首次请求 | 取决于 REQUEST 映射 |
FORWARD |
服务端转发 | 取决于 FORWARD 映射 |
ERROR |
错误页派发 | 取决于 ERROR 映射 |
ASYNC |
异步重新派发 | 取决于 ASYNC 映射及异步配置 |
所以判断触发范围不能只看 URL。映射对象、DispatcherType、组件顺序和异步支持共同决定它何时进入执行链。
四、从请求链定位异常组件
应用内部可以先读取 Servlet 规范公开注册表:
1 | ServletContext context = request.getServletContext(); |
排查时不要只搜索 Runtime.exec() 或可疑类名,应按执行关系核对:
1 | 注册了什么组件 |
公开注册表只能看到 Servlet 规范层,无法覆盖 Tomcat Valve、Jetty Handler、Undertow HttpHandler、Spring 动态路由或 JVM 字节码修改。即使注册名称和类名正常,运行中的类也可能已经被替换或重转换,因此最终仍要结合实例、类来源和字节码基线判断。
至此可以把 Servlet 请求型内存马抽象成一条完整关系:
1 | 请求匹配 |
只看到可疑对象不等于它能够处理请求;只删除映射也不等于实例、植入入口和复活机制已经消失。请求链决定触发面,持有关系决定生命周期,两者必须一起分析。