原理分析 | Listener —— Tomcat 内存马 回显问题 反射总结
目录
- 前置:StandardContext 回顾
- ApplicationEventDispatcher
- ServletRequestListener 示例(静态注册)
- web.xml 中注册 Listener 的标准写法
- 动态注入 Payload(无回显版)
- Listener 与 Filter/Servlet 的差异对比
- 回显问题:为什么 Listener 不能直接回显?
- 解决回显:反射硬拿 Response
- 其他常见 Listener 接口简介
- 反射补充:六大核心能力
- 总结
前置:StandardContext 回顾
Listener 型内存马的注入原理,依然依赖于 Tomcat 内部的 StandardContext。
| 对象 | 说明 |
|---|---|
StandardContext |
Tomcat 中代表一个 Web 应用的核心对象,里面存了 Filter、Servlet、Listener 等组件 |
可以用这个类比来理解三种组件的区别:
- Filter:像一个安检闸机,所有请求都过一遍
- Servlet:像一个具体柜台,请求最终被它处理
- Listener:像一个感应门铃,有人进门(请求开始)、出门(请求结束)、新顾客到店(Session 创建)……都会触发
Listener 就存在 StandardContext 的 applicationEventListeners 字段里,执行调度器是 ApplicationEventDispatcher,它负责从这个列表里取出 Listener 并调用。
ApplicationEventDispatcher
ApplicationEventDispatcher 是 Tomcat 运行时自动调用的调度器。
只要把 Listener 实例放进 applicationEventListeners 列表,Tomcat 在处理每个请求时,ApplicationEventDispatcher 就会自动从列表里拿到你的 Listener 并调用它。
和 Filter 对比一下差异就很明显了:
- 动态注册 Filter:需要操作
FilterDef+FilterMap+FilterChain(重新创建),比较繁琐 - 动态注册 Listener:只操作一个列表,没有映射,没有链,简单得多
ServletRequestListener 示例(静态注册)
先看一个静态注册的 ServletRequestListener,理解它的工作机制。
作用是:每次 HTTP 请求进入 Tomcat 时,自动执行 requestInitialized 方法;请求结束时执行 requestDestroyed 方法。
1 | import javax.servlet.ServletRequestEvent; |
启动 Tomcat 服务后,类被加载就会显示:

每次刷新页面就会多一次输出,包含请求开始、IP 地址、请求结束三条日志:

这里 IP 显示的是
0:0:0:0:0:0:0:1(简写::1),这是 IPv6 的本地回环地址,等价于 IPv4 的127.0.0.1。
web.xml 中注册 Listener 的标准写法
静态注册除了用 @WebListener 注解,也可以写在 web.xml 里:
1 |
|
动态注入 Payload(无回显版)
和 Filter、Servlet 一样,先两层反射获取 StandardContext:
1 | ServletContext >> ApplicationContext >> StandardContext |
1 | <%@ page import="java.lang.reflect.Field" %> |
访问 http://localhost:8080/inject_listener_noecho.jsp,显示注册了一个监听器:

再次访问,注册数量会累加(因为每次访问都会重新注入一个):

Listener 与 Filter/Servlet 的差异对比
Listener 和 Filter、Servlet 在多次注入行为上有些不同:
| 类型 | 能否多次注入 | 关键约束 | 是否会覆盖 |
|---|---|---|---|
| Listener | ✅ 能 | 无名称,直接追加 | 不会覆盖,都会执行 |
| Filter | ✅ 能 | Filter 名称必须唯一 | 相同名称会覆盖;不同名称共存 |
| Servlet | ✅ 能 | Servlet 名称唯一;URL 映射唯一 | 相同名称或相同 URL 映射会覆盖 |
Listener 没有名称这个概念,每次注入都是往列表里追加一个对象,所以注册了几个,每次请求就会触发几次。
此时再访问 http://localhost:8080/任意路径?cmd=whoami(只要该路径能被注册的监听器监听到就行,即使显示 404 也能被监听到):

IDEA 的 Tomcat 控制台里可以看到命令执行结果:

这里因为注册了四个监听器,访问了一次网站,被监听到了四次,所以输出了四条结果。
Listener 拦截”任意 URL”的优势:
| 内存马类型 | 能否拦截任意 URL(包括不存在的路径、404) | 条件 |
|---|---|---|
| Listener(ServletRequestListener) | ✅ 能 | 只要注册到 StandardContext,无需任何映射,每个请求都会触发 |
| Filter | ✅ 能(但需要配置) | 必须添加 /* 的 URL 映射,才能拦截所有请求 |
| Servlet | ❌ 一般不能 | 必须匹配具体的 URL 模式(如 /shell、/admin/*),无法覆盖所有路径 |
回显问题:为什么 Listener 不能直接回显?
上面的写法命令结果只输出在 IDEA 的 Tomcat 控制台里,没有回显到浏览器。那 Listener 可以像 Filter 和 Servlet 一样直接把命令结果回显到浏览器吗?
不可以。 原因很简单:
- Filter:
doFilter(ServletRequest request, ServletResponse response, FilterChain chain)— 方法签名里直接有response - Servlet:
service(HttpServletRequest request, HttpServletResponse response)— 同上,直接有response - Listener:
requestInitialized(ServletRequestEvent sre)— 只有ServletRequestEvent,没有ServletResponse
| 组件 | 方法签名 | 能否直接回显 |
|---|---|---|
| Filter | doFilter(request, response, chain) |
✅ 直接写 response |
| Servlet | service(request, response) |
✅ 直接写 response |
| Listener | requestInitialized(ServletRequestEvent sre) |
❌ 没有 response,需要反射 |
这也是为什么 Filter 型内存马和 Servlet 型内存马在回显上最方便,而 Listener 更适合做”无回显的潜伏监控”或者配合其他组件使用。
解决思路大概有两种:
- Listener + Filter 组合:先用 Listener 监听,再由 Filter 负责回显。但这样还不如直接用 Filter,可能的优势是 Listener 触发时机比 Filter 略早一点点,实际意义不大。
- 反射硬拿 Response(推荐,更干净)
解决回显:反射硬拿 Response
ServletRequestEvent 只提供了 getServletRequest(),没有 getServletResponse()。但 Tomcat 在处理请求时,实际上同时创建了 Request 和 Response 对象,只是没暴露给 Listener 接口。
调用链大概长这样:
1 | javax.servlet.ServletRequestEvent 的 request 字段(实际类型是 RequestFacade) |
所以”找 response”的本质是:从 ServletRequestEvent 出发,穿透两层包装,拿到真正的 Request,再调用其 getResponse() 方法。
1 | <%@ page import="java.lang.reflect.Field" %> |
访问 http://localhost:8080/inject_listener_echo_reflect.jsp,注入成功:

然后访问 http://localhost:8080/任何.jsp?cmd=whoami,命令结果直接回显到浏览器:

其他常见 Listener 接口简介
Tomcat 里不只有 ServletRequestListener 一种,还有其他几种,简单了解一下:
| 接口 | 触发时机 | 能否拿到 Request/Response | 内存马适用度 |
|---|---|---|---|
ServletRequestListener |
请求刚进入、请求结束时 | 有 ServletRequest,无 ServletResponse | ⭐⭐⭐⭐⭐(最常用,触发频率高) |
HttpSessionListener |
Session 创建、销毁 | 无 Request/Response | ⭐⭐(需要先有 Session 才触发) |
ServletContextListener |
Web 应用启动、销毁时 | 无 Request/Response | ⭐(只触发一两次,不适合做后门) |
ServletRequestAttributeListener |
请求域属性增删改时 | 有 ServletRequest | ⭐⭐(需要代码主动触发属性变化) |
HttpSessionAttributeListener |
Session 域属性变化时 | 无 Request/Response | ⭐(依赖 Session 操作) |
AsyncListener |
异步请求开始/完成/超时/出错 | 有 ServletRequest/ServletResponse | ⭐⭐(需要异步支持) |
做内存马首选 ServletRequestListener,触发频率最高,几乎每个请求都能命中。
反射补充:六大核心能力
顺带把反射的六大核心能力梳理一下,目前代码审计碰到过前三种:
| 能力 | 关键类 | 核心方法 | 用途示例 |
|---|---|---|---|
| 1. 对象创建(构造器反射) | Constructor |
getDeclaredConstructor() / newInstance() |
创建类的实例,包括私有构造器 |
| 2. 字段反射 | Field |
getDeclaredField() / set() / get() |
读取或修改成员变量的值(包括私有字段) |
| 3. 方法反射 | Method |
getDeclaredMethod() / invoke() |
调用对象的方法(包括私有方法) |
| 4. 访问私有内部类 | Class |
getDeclaredClasses() / getDeclaredConstructors() |
实例化或操作私有内部类、匿名类 |
| 5. 数组反射 | Array |
Array.newInstance() / Array.get() / set() |
动态创建和操作数组 |
| 6. 泛型反射 | ParameterizedType 等 |
getGenericSuperclass() / getGenericParameterTypes() |
获取运行时泛型类型(比如 List<String> 中的 String) |
几种常用写法对比:
构造器反射(创建对象):
1 | Constructor<MyClass> ctor = MyClass.class.getDeclaredConstructor(String.class); |
字段反射(读写私有字段):
1 | Field field = obj.getClass().getDeclaredField("secret"); |
方法反射(调用私有方法):
1 | Method method = obj.getClass().getDeclaredMethod("privateMethod", String.class); |
访问非公开类(以 AnnotationInvocationHandler 为例):
注:这个不是内部类,当时记混了,但写法和构造器反射基本一样。
1 | // 1. 获取类(全限定名) |
这几种写法结构高度相似,核心模式都是:获取 Class → 获取成员(字段/方法/构造器)→ setAccessible(true) → 操作。
总结
Listener 型内存马的整体思路比 Filter 和 Servlet 都简单,核心就一步:把恶意 Listener 对象塞进 StandardContext 的 applicationEventListeners 列表,Tomcat 的 ApplicationEventDispatcher 就会帮你在每次请求时自动调用它。
几个关键点:
- 注入方式:推荐用
standardContext.addApplicationEventListener(),比反射操作字段更稳定 - 触发条件:无需 URL 映射,任意请求(包括 404)都能触发,隐蔽性强
- 回显问题:Listener 接口没有
response参数,要回显就得两层反射穿透拿到org.apache.catalina.connector.Response - 多次注入:Listener 没有名称约束,每次访问注入 JSP 都会追加一个新的,调试时注意别重复注册太多
- 最优选择:做内存马首选
ServletRequestListener,触发频率最高,其他类型的 Listener 触发条件苛刻,实战价值有限