news 2026/10/3 15:21:07

Spring-Interceptor内存马:原理、检测与防御实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring-Interceptor内存马:原理、检测与防御实践

1. 从“内存马系列”到Spring-Interceptor:为什么这个组件成了攻防焦点

做Java安全这行的朋友,近两年应该都有一个明显感受:内存马已经从“小众炫技”变成了“必修课”。从Servlet API的Filter型、Tomcat的Valve型,到Spring容器的Controller型、Bean型,每一次Spring生态的普及,都会带动一批新的注入思路被曝光、被修复、再被绕过,循环往复。而我这次想聊的,是Spring MVC体系里一个经常被忽略、但攻击价值极高的组件——Interceptor(拦截器)。

为什么说它攻击价值高?因为拦截器的执行时机天然“贴近业务”。它不像Filter那样位于Servlet容器层,而是已经进入了Spring MVC的调用链,对Handler的包裹更精确、更隐蔽。如果攻击者能在运行期向Spring容器注册一个恶意Interceptor,就等于在每一次HTTP请求进入Controller之前,插入了一段完全受控的逻辑——可以记录参数、篡改响应,甚至直接短路请求返回伪造结果。这些行为落在框架层,常规的RASP和Webshell查杀工具未必看得见。

这篇内容,我打算完全站在防御与检测的角度来拆解Spring-Interceptor内存马。我不会教你怎么打进去,而是把原理讲透:拦截器在Spring MVC里的生命周期是什么、内存马注入的本质是什么、为什么它比其他内存马更难清、以及我们到底能从哪里下手去发现它、处置它。

适合谁来读?一类是负责Java应用安全巡检的运维和蓝队同学,你们需要知道除了Filter和Controller之外,还有哪些隐蔽的注入点;另一类是业务开发同学,理解Spring容器的运行期变更风险,才能在写代码时下意识避开“高危写法”。这篇文章不会太浅,我默认你用过Spring Boot、看过一些内存马概念,但即使你是刚接触,我也会把关键链路一步步拆开讲清楚。

2. 内存马的本质:不是“马”,是“运行期对象注入”

在展开Interceptor之前,我得先把一个底层认知对齐:所谓内存马,本质上是往运行期的进程里塞入一个“活着”的对象,并让这个对象挂到某个会被反复触发的框架回调链上。

2.1 为什么“内存”两个字这么关键

传统Webshell是落一个文件到磁盘目录,比如shell.jsp、cmd.php,请求过来就能执行。而内存马不落盘,只在内存里构造对象并注册。这就带来三个对防守方极其不友好的特性:

  • 无文件痕迹:磁盘扫描、Webshell特征扫描全部失效,因为根本没有文件给你扫。
  • 进程重启即失:不过这也意味着重启是终极大招,只是业务连续性代价太高。
  • 隐蔽性取决于注入点:公共组件里的注入点比业务代码里的更隐蔽,因为它不产生业务日志,甚至不走业务过滤链。

理解了这个本质,你就能明白防御方的思路永远只有两条:要么在对象注册入口拦截,比如Hook掉BeanFactory的registerSingleton方法;要么在请求调用链路上做行为检测,看有没有异常的handler出现在链条里。

2.2 Spring容器为什么成了内存马的“温床”

原因其实很朴素:Spring的核心就是IoC容器,而IoC容器是运行期动态的。你可以在进程跑起来之后,随时向DefaultListableBeanFactory注册一个新的Bean,也可以通过RequestMappingHandlerMapping注册一个新的接口映射,甚至可以通过HandlerInterceptorRegistry追加一个新的Interceptor。这些能力本来是给开发同学做插件扩展、动态配置用的,但攻击者只要能执行任意代码,这些“合法能力”就会被拿来直接变成“持久潜伏能力”。

这里有一个关键前提:攻击者必须先拿到一次代码执行机会。所以内存马从来不是“入口”,而是“跳板之后的落点”。常见的入口包括反序列化漏洞、表达式注入(如SpEL)、JSP文件上传绕过等。我们的防御重心,其实是两层:一层是尽量堵住入口(这是另一个话题),另一层就是今天这篇文章要聊的——在入口被突破后,尽量发现落点、缩短驻留时间。

3. Spring-Interceptor的工作原理与风险点拆解

要防守Interceptor型内存马,第一步就是把Spring MVC的请求处理链路搞清楚:一个HTTP请求从进来到返回,Interceptor到底在哪个环节、以什么顺序、被谁触发。

3.1 Interceptor在Spring MVC处理链中的位置

我把这条链路简化成四个环节:

DispatcherServlet 接收到请求 ↓ HandlerMapping 根据URL找到对应的HandlerExecutionChain ↓ HandlerExecutionChain 按注册顺序执行 Interceptor.preHandle() ↓ 进入 Controller 业务方法 ↓ 返回时逆序执行 Interceptor.postHandle() / afterCompletion()

关键在第二步。DispatcherServlet拿到请求后,会调用HandlerMapping.getHandler(),得到一个HandlerExecutionChain,这个Chain里面同时包含了目标Handler和一组Interceptor。也就是说,Interceptor的执行并不由Spring容器直接管理,而是被包装在HandlerExecutionChain里,由DispatcherServlet统一调度。

这就带来一个对内存马非常有利的点:你不需要替换DispatcherServlet,只需要往某个HandlerMapping的interceptors列表里追加一个对象即可。这个列表是ArrayList,支持运行期动态添加,而且Spring本身不会去校验这个对象是不是“注册过的Bean”,只要你通过getHandler()返回了它,它就会执行。

3.2 三种典型注入路径:全局、单映射、手工封装

根据我的梳理,Interceptor型内存马在真实场景里通常通过以下三条路径落地:

路径一:通过HandlerInterceptorRegistry追加全局拦截器。

这种方式最“合法”,在代码里看起来和正常业务完全一致。攻击者拿到执行点后,直接获取WebMvcConfigurer的registry,或从WebApplicationContext里拿到RequestMappingHandlerMapping,再对HandlerInterceptorRegistry进行操作。

不过有个细节要留意:HandlerInterceptorRegistry.addInterceptor()会做一次getInterceptor()的适配包装,如果你传入的类实现了WebRequestInterceptor,会被适配成WebRequestHandlerInterceptorAdapter,否则作为普通HandlerInterceptor存起来。之后这个对象会被添加到所有注册的HandlerMapping里。因为涉及对象适配和跨HandlerMapping赋值,这一步的动作会比直接改List更“显眼”,在检测时属于高价值信号。

路径二:直接往HandlerMapping.adaptedInterceptors列表添加。

这个就粗暴多了。AbstractHandlerMapping基类里有一个adaptedInterceptors属性,是List<Object>类型,默认存的是MappedInterceptor或者HandlerInterceptor适配后的对象。攻击者如果能从容器里拿到RequestMappingHandlerMapping实例,直接反射getAdaptedInterceptors()并add()一个构造好的Interceptor对象,就完事了。不需要经过任何Registry校验、适配,因为对象已经是最终的适配形态了。

路径三:自定义HandlerMapping并注册成Bean。

这条路更隐蔽,因为攻击者可以自己构造一个HandlerMapping实现类,让它在getHandler()时返回一个带有恶意Interceptor的HandlerExecutionChain,再把这个Bean注册进Spring容器。对Spring来说,这只是一个新的HandlerMapping Bean,框架层面完全合法。检测难度极高,因为没有了“向现有对象添加元素”这个操作,新增的是整个Bean。

三种路径里,路径一和路径二在检测上还有迹可循,路径三几乎只能从父链路对比来发现。

3.3 关键参数与回调方法:preHandle才是“主战场”

HandlerInterceptor接口有3个默认方法和1个旧版方法需要了解:

方法执行时机内存马利用价值
preHandleController执行前最高,可以完全控制请求是否进入业务,直接返回内容
postHandleController执行后、视图渲染前高,可以篡改ModelAndView的内容
afterCompletion整个请求结束后中,适合做“事后记录类”的隐蔽操作
boolean isAsyncStart/isAsyncDispatch(旧版)异步请求相关低,一般用于适配异步场景

对于内存马来说,preHandle是绝对的主战场。原因很简单:它执行最早、对请求的控制力最强。攻击者可以在preHandle里直接走一套“暗号校验”——比如某个请求头X-Token等于一个约定值,就执行恶意逻辑、返回篡改响应,否则原样放行。这样平时完全不触发,流量特征极低。

有人可能问:postHandle也能拿到请求和响应对象,为什么不选它?因为preHandle返回false可以直接终止后续链路,连Controller都不用进,减少暴露面。同时从内存马驻留的稳定性来说,preHandle不依赖业务代码执行结果,请求一进来必然先经过它,可靠性最高。

4. 从“攻击逻辑”反推检测方案:四个检测维度的落地实现

前面原理部分,我刻意站在攻方视角去拆解,是因为防守方必须“知己知彼”。下面进入真正能落地的部分:我们到底怎么发现系统里多了一个陌生的Interceptor?

我自己的实践经验是:不要试图去“识别恶意”,而是去“识别异常”。恶意Interceptor的代码可能是任何样子的,但它的存在本身就是异常。所以检测思路可以拆成四个维度。

4.1 维度一:启动期快照比对——把“基线”建起来

这是我最推荐、也最可靠的一种方式。思路就是:在应用启动完成后,主动记录一组“合法拦截器指纹”,然后在运行期定期比对,看多了什么。

具体怎么做?在ApplicationRunner或ApplicationListener里,等Spring容器refresh完成之后,遍历所有HandlerMapping,取出HandlerExecutionChain里的拦截器列表,对每个拦截器的Class名、所在ClassLoader、所在Jar包路径生成Hash指纹,保存到一个只读的基准文件里。然后每隔一段时间或在收到敏感请求时,重新采集一遍进行比对。

采集的核心代码思路如下(伪代码级别):

@Component public class InterceptorBaselineCollector implements ApplicationRunner { // 注入ApplicationContext,拿到所有HandlerMapping @Override public void run(ApplicationArguments args) { Map<String, HandlerMapping> mappings = applicationContext.getBeansOfType(HandlerMapping.class); for (Map.Entry<String, HandlerMapping> entry : mappings.entrySet()) { if (entry.getValue() instanceof AbstractHandlerMapping) { AbstractHandlerMapping mapping = (AbstractHandlerMapping) entry.getValue(); // 反射获取adaptedInterceptors列表 List<Object> interceptors = (List<Object>) ReflectionUtils.findField(AbstractHandlerMapping.class, "adaptedInterceptors"); // 对每个对象生成指纹 for (Object interceptor : interceptors) { String fingerprint = generateFingerprint(interceptor); baseline.put(entry.getKey() + ":" + interceptor.getClass().getName(), fingerprint); } } } } }

经验提示:如果应用里有多个HandlerMapping,尤其是用了Spring Security的时候,拦截器列表会比较长。基线采集要注意过滤掉Spring自己注册的那些内部类,不然误报会非常严重。我是用一个白名单前缀来过滤的,比如org.springframework.security、org.springframework.web、org.springframework.boot开头的自动跳过。

4.2 维度二:运行期周期扫描——对“非基线”对象告警

采集了基线之后,运行期扫描就是水到渠成的事。这里我建议不要做成“定时任务傻傻地跑”,而是结合实际情况分层:

  • 高频检测:每5到10分钟一次,比对拦截器列表是否新增。因为内存马注入是一次性的,周期不需要太短,太短反而影响性能。
  • 事件驱动检测:在高风险动作之后触发一次扫描。什么叫高风险动作?比如出现了SpEL执行的告警日志、出现了反序列化异常的日志,或者Thread.currentThread().getContextClassLoader()被异常调用,这些都是入口被突破的信号,此时立即做一次拦截器比对。

扫描的时候除了比对新旧列表,还要对每一个新出现的拦截器对象做几项“体检”:

第一,它的ClassLoader是否属于Web应用本身。如果攻击者用自定义ClassLoader加载字节码,对象的getClass().getClassLoader()往往不是LaunchedURLClassLoader或者WebappClassLoader,而是一个陌生的类加载器实例,这一条就能筛掉很多人。

第二,它的Class文件是否有对应的磁盘来源。通过ProtectionDomain.getCodeSource().getLocation()拿到的URL指向的Jar包,是否存在于我们预期的Maven依赖清单里。如果指向一个临时目录、缓存目录、或者干脆是null,那是高可疑信号。

第三,这个对象是否是Spring容器管理的Bean。正常拦截器绝大多数都是@Component或者通过WebMvcConfigurer注册的,在applicationContext.getBeanNamesForType()里通常能找到对应。而恶意内存马直接new出来放进去的,没有BeanDefinition,这一点在排查时非常管用。

private boolean isSpringManaged(Object interceptor) { String[] beanNames = applicationContext.getBeanNamesForType(interceptor.getClass()); return beanNames.length > 0; }

4.3 维度三:请求链路的行为监控——从“执不执行”入手

前面两种方案针对的是“列表里多了对象”,但有一种情况它们会漏:攻击者直接替换了某个原有HandlerMapping,而不是往已有列表里加元素。这时候列表指纹可能没变化,但行为变了。所以我建议在Spring MVC链路上埋一个“探针”,对每个进入Controller的请求做一次拦截器命中情况记录。

具体做法:自己写一个HandlerInterceptor,放在拦截器列表最前面的位置(在WebMvcConfigurer里addInterceptor,并通过order保证最优先),在preHandle里遍历当前HandlerExecutionChain的所有拦截器,并记录它们的类名。然后交给一个后台任务分析,出现“从未见过的拦截器类名”时告警。

这个方案本质上也是基线比对,但维度和维度一不同:它是从实际请求执行路径的“命中情况”来做,避免了扫描静态列表看不到的运行时请求级差异。比如攻击者往adaptedInterceptors里塞了一个自定义包装类,但包装类里的真实拦截器是在每次getHandler()时才new的,静态扫描根本发现不了,请求级探针却能抓到。

如果你追求更低侵入性,也可以不自己写拦截器,而是基于HandlerExecutionChain做AOP切面:切DispatcherServlet.doDispatch()方法,在前置通知里拿到HandlerExecutionChain,反射遍历拦截器数组。不过AOP切Spring自身Servlet的代价比加一个自定义拦截器更高,稳定性上要谨慎权衡。我的建议是优先自定义拦截器方案,实在不能改代码,再上AOP。

4.4 维度四:结合Spring Security的Special Case

如果你的项目用了Spring Security,那情况会再复杂一层。因为Spring Security本身也大量依赖Filter和Interceptor,而且它的SecurityInterceptor(比如FilterSecurityInterceptor)是包裹在Spring MVC的Filter链里的。攻击者有一条非常经典的进阶路径:通过往Spring Security的过滤链里插入一个自定义Filter来“复用”安全上下文,再把它伪装成Security内部组件。

但这里我不建议深挖,因为它是另一个巨大的主题。我只想强调一个点:在Spring Security项目里做Interceptor基线时,白名单要更加宽松,并且要把Security的过滤器链和MVC的拦截器链分开比对。否则你会频繁误报,最后把自己搞得“狼来了”。

如果你实在想深挖,关注三个地方就行了:FilterChainProxy里有没有新增的FilterBean、WebSecurityConfiguration里有没有被注册的自定义SecurityFilterChain、以及DelegatingFilterProxy的targetBeanName是否指向了陌生Bean。这些和Interceptor型的检测方法论是相通的——都是基线比对加上行为观察。

5. 实操:一个可落地的拦截器检测/处置工具的设计要点

说了这么多理论,我干脆给你一个可以直接在项目里改造的小工具设计。它不需要你引入任何重量级组件,只需要Spring Boot + 基础反射API。

5.1 采集模块:定时与触发式双引擎

工具整体上分三块。第一块是采集模块,我设计为“定时基线+事件触发”双引擎。

定时引擎用@Scheduled注解实现(如果你对实时性要求高,可以用ScheduledThreadPoolExecutor自定义频率)。事件触发引擎手动提供一个scanNow()方法,让运维同学在发现可疑行为时手动触发一次。两个引擎共用同一个scan()核心方法,方法内部做以下几件事:

  1. 从ApplicationContext获取所有HandlerMappingBean。
  2. 对每个AbstractHandlerMapping子类,反射取出adaptedInterceptors列表。
  3. 遍历列表,对每个Interceptor生成对象哈希、类名、ClassLoader标识、代码源URL。
  4. 和基线HashMap比对,有差异立即输出告警对象。

这里有一个技术细节:反射读取adaptedInterceptors时,Spring版本不同字段名有差异。Spring 5.0以前叫interceptors,Spring 5.0+改名成adaptedInterceptors。如果你用的是旧版Spring,配一个“字段名自动探测”的逻辑比较稳妥:先尝试adaptedInterceptors,反射找不到再试interceptors。

5.2 告警模块:去除“无害异常”的干扰

比对之后一定会出现两类差异:一类是真正的恶意注入,另一类是业务代码运行期正常添加的拦截器。后者在成熟项目里并不少见,比如动态配置中心下发的某个Feature Flag触发了运行时加拦截器。所以告警一定要带“置信度”概念,不要一刀切。

我自己在排查时用一套打分机制:

  • 拦截器Class名以org.springframework、com.yourcompany等白名单开头,正常;以java.lang.reflect.Proxy或包含$Proxy,加分。
  • ClassLoader不是默认Web类加载器,加分。
  • 代码源URL指向file:/tmp/、file:/var/tmp/或空,加分。
  • 拦截器不是Spring管理的Bean(前面提到用getBeanNamesForType判断),加分。
  • 拦截器对象内部字段引用了类似于ProcessBuilder、ScriptEngine、Runtime等类,加分。

总分超过某个阈值才触发告警。这样能大大减少误报,保证告警质量。

5.3 处置模块:三种止损手段的取舍

发现内存马之后,最痛苦的问题不是我发现了,而是“怎么在不重启的情况下清掉它”。我按风险从高到低给你排序:

方案一:重启应用。这是最彻底的,但代价是业务中断。如果应用无状态、有多副本,建议直接滚动重启。有状态应用就麻烦了,得评估。

方案二:热删除拦截器。用反射把adaptedInterceptors里的对象移出List。这个方法理论上“无害”,但有一个致命问题:如果攻击者使用的是自定义HandlerMapping + getHandler()时现new的拦截器,你删掉列表是没有用的,下次请求照样创建。所以热删除只适合路径一和路径二,对于路径三必须在容器层面移除整个HandlerMapping Bean。

方案三:在DispatcherServlet层面加“门禁”。你可以用一个自定义的前置Filter,在进入MVC链之前比对当前请求URL是否命中“异常拦截器名单”。本质上是用一个过滤器去“屏蔽”攻击者的执行路径。这个方案的好处是不改动已有拦截器列表,坏处是请求还是走到了MVC,只是被第三方Filter拦住了,如果攻击者注入的节点在Filter之前,依然会执行。

我的最终建议:能重启就重启,不能重启就热删除加门禁双管齐下。但一定要记住,内存马清除之后,根本问题没有解决——攻击者的入口还在。如果不清入口,清掉一个他还会再注一个。

6. 排查过程中的常见坑与个人经验总结

最后这部分,我想写一点自己踩坑后的心得体会,也算是对前面内容的一个补充。

先说一个最常见的坑:基线采集的时机不对。很多人在监听ApplicationReadyEvent时去采集,但这个时候如果项目里有某些懒加载的Bean还没初始化,对应的HandlerMapping可能还没有完全组装好拦截器列表,导致基线本身就不完整。后来我改成监听ContextRefreshedEvent并延迟5秒再采集,配合一次主动getHandler()触发其初始化,基线才稳定下来。

第二个坑:Spring Security的误报率。我之前接过一个项目,安全团队反馈“天天告警有内存马”,我上去一看,全是FilterSecurityInterceptor在不同URL pattern下被生成了多个实例,各自的类名一样但Hash不同,导致基线比对疯狂误报。后来我在生成指纹时取消了“实例Hash”,改用“Class名+代码源”作为比对键,误报瞬间消失。记住这个教训:比对Class维度的指纹,而不是实例维度的指纹。

第三个体会:RASP不是万能,但没有RASP真的不行。说实话,纯靠日志和应用内巡检,对于高级内存马几乎是“事后发现”。如果预算允许,在Java应用里挂一个RASP,重点HookReflectionFactory.newConstructorForSerialization、Method.invoke、ClassLoader.defineClass这几个关键点,把“任意代码执行”的第一个动作扼杀在摇篮里,效果要比任何事后排查都好。RASP的误报需要团队投入精力调优,但对安全建设较完善的公司,这是值得的。

最后总结一句话:Spring-Interceptor内存马的防御难点不在“发现技术”,而在“发现思路”。你如果只盯着业务日志、扫描磁盘文件,那你永远看不到内存马。把视角切换到容器本身、切换到运行期对象列表、切换到请求执行链路上,那些藏得再深的东西,也会慢慢浮出水面。排查内存马没有银弹,基线、监控、门禁、重启,一环扣一环,做到哪一步算哪一步,但至少要把“一看二排三处置”的流程跑起来,才能在一次又一次的攻防对抗中积累出你的肌肉记忆。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 15:19:22

网页右键被禁用?从原理到破解,几行代码恢复原生菜单

你有没有遇到过这种情况&#xff1a;打开一个看起来平平无奇的网页&#xff0c;想选中一段文字&#xff0c;鼠标一拖发现选不了&#xff1b;想看看图片原地址&#xff0c;右键一点&#xff0c;弹出个“本页面禁止右键”或者干脆毫无反应。我平时搜集资料比较多&#xff0c;浏览…

作者头像 李华
网站建设 2026/10/3 15:18:44

GPU服务器运维实战:从驱动到集群的完整方法论

做运维这么多年&#xff0c;我最大的体会是&#xff1a;GPU服务器和普通CPU服务器的运维逻辑&#xff0c;完全是两码事。你可以在传统服务器上靠几条命令和监控大盘混得风生水起&#xff0c;但到了GPU集群面前&#xff0c;如果还是那套“CPU、内存、磁盘”三板斧&#xff0c;大…

作者头像 李华
网站建设 2026/10/3 15:18:20

燃料电池混动汽车能量管理的ADMM双层凸优化Matlab实现

燃料电池混合动力汽车的能量管理&#xff0c;圈内讨论得最多的就是怎么把氢耗压下来、同时把电池SOC稳住。这个方向我断断续续做了挺长时间&#xff0c;用过的算法从早期手画的规则表&#xff0c;到动态规划、粒子群&#xff0c;再到后来把ADMM和双层凸优化结合起来的完整Matla…

作者头像 李华
网站建设 2026/10/3 15:18:18

Windows下用adb reverse实现USB抓包:Charles稳定替代WiFi代理的方案

1. 为什么要在Windows上用USB过Charles抓包 做移动端开发或者接口联调的朋友&#xff0c;大概率都用过Charles抓包工具。它在Windows上配合手机抓HTTPS请求&#xff0c;常规思路是让手机和电脑连同一个WiFi&#xff0c;然后把手机WiFi代理指到电脑IP和8888端口。这个方案本身没…

作者头像 李华
网站建设 2026/10/3 15:17:35

电影知识图谱与KBQA从零构建:本体设计、Cypher查询与工程落地

简介&#xff1a;从零搭建电影知识图谱并实现智能问答的完整实践包&#xff0c;面向具备一定编程基础、希望系统掌握本体建模、图谱存储与语义查询的开发者&#xff0c;也适用于高校人工智能或自然语言处理课程设计。资源对应上下两篇教程&#xff0c;涵盖本体概念设计、RDF数据…

作者头像 李华
网站建设 2026/10/3 15:16:35

基于Python的心脏病数据分析与预测模型实战

简介&#xff1a;一套基于Python的心脏病数据分析与预测项目资料&#xff0c;面向计算机相关专业学生&#xff0c;适用于毕业设计、期末大作业或项目实训参考。项目依托UCI心脏病数据集&#xff0c;完整覆盖数据预处理、特征工程、模型构建与结果评估等技术环节。整套资料共84个…

作者头像 李华