剖析Java内存马2

前文

上一节主要讲了常见的三种类型,今天主要说一下Spring系列框架型内存马,SpringMVC内存马的实现方式与Servlet内存马类似,都是通过在内存中创建一个恶意类,然后通过反射的方式将恶意类注入到SpringMVC的执行流程中。spring对象间的依赖关系可以用配置文件的bean定义,SpringMVC则是基于Spring功能的Web框架。Spring作为Java框架,核心组件有三个:Core、Context、Bean。其中context又叫IOC容器;Bean构成应用程序主干,Bean就是对象,由IOC容器统一管理;Core为处理对象间关系的方法。

SpringMVC

SpringMVC是基于Spring功能的Web框架,SpringMVC的入口是DispatcherServlet,DispatcherServlet是SpringMVC的核心,它负责接收HTTP请求,并将其分发到相应的处理器(Handler)进行处理。而DispatcherServlet是SpringMVC的核心,它负责接收HTTP请求,并将其分发到相应的处理器(Handler)进行处理。放一张网上的图,可能就更好理解一些

因此SpringMVC的执行流程可以分为以下几个步骤:

1
2
3
4
5
1. 用户发送请求到DispatcherServlet
2. DispatcherServlet调用HandlerMapping查找处理器
3. DispatcherServlet调用处理器适配器执行处理器
4. 处理器执行完毕后,返回ModelAndView
5. DispatcherServlet将ModelAndView返回给用户

下面是SpringMVC九大组件的介绍

1
2
3
4
5
6
7
8
9
1. DispatcherServlet:SpringMVC的核心,负责接收HTTP请求,并将其分发到相应的处理器(Handler)进行处理。
2. HandlerMapping:用于确定请求的处理器(Controller)。
3. HandlerAdapter:将请求映射到合适的处理器方法,负责执行处理器方法。
4. HandlerInterceptor:允许对处理器的执行过程进行拦截和干预。
5. Controller:处理用户请求并返回适当的模型和视图。
6. ModelAndView:封装了处理器方法的执行结果,包括模型数据和视图信息。
7. ViewResolver:用于将逻辑视图名称解析为具体的视图对象。
8. LocaleResolver:处理区域信息,用于国际化。
9. ThemeResolver:用于解析Web应用的主题,实现界面主题的切换。

url和Controller的映射关系

假设当我们用@RequestMapping(“/“)注解在方法上的,那Spring MVC是怎么根据这个注解就把对应的请求和这个方法关联起来呢?在SpringMVC中,url和Controller的映射关系是通过HandlerMapping来实现的。HandlerMapping是SpringMVC九大组件之一,它用于将请求的URL映射到相应的处理器(Controller)。在九大组件初始化的时候,有个方法叫做initHandlerMappings,我们可以来看一下

代码和自带的注释写的还是比较好理解的,这里就不多说了,我们主要关注一下注释里面提到的,如果需要检测所有处理程序映射,那就去ApplicationContext(包括ancestor contexts)里面找所有实现了HandlerMappings接口的类,如果找到了至少一个符合条件的HandlerMapping bean,那就把它的值转化为列表,并按照Java的默认排序机制对它们进行排序,最后将排序后的列表赋值给 this.handlerMappings;那如果没有找到,this.handlerMappings就依然保持为null;如果不需要检测所有处理程序映射,那就尝试从ApplicationContext中获取名称为 handlerMapping 的bean,如果成功获取到了则将其作为单一元素的列表赋值给 this.handlerMappings,如果获取失败了,那也没关系,因为人家注释里面讲的很明白,会添加一个默认的HandlerMapping,这也就是我们要讲的第二部分的代码。第二部分说的是,如果之前一套操作下来,this.handlerMappings还是为null,那么就调用 getDefaultStrategies 方法去获取默认的HandlerMapping,并将其赋给 this.handlerMappings。由此看来,getDefaultStrategies这个方法还是很关键的,跟踪一下

首先是加载资源文件,从资源文件中读取内容,并将其存储为键值对形式到defaultStrategies中,然后从strategyInterface中获取一个名称。根据名称在defaultStrategies中查找对应的值(类名数组),然后将类名数组按逗号分隔,并遍历每个类名,并执行以下操作:1、使用ClassUtils.forName方法加载类。2、调用createDefaultStrategy方法创建类的实例,最后,将所有创建的策略对象添加到strategies列表中,并返回该列表。那DEFAULT_STRATEGIES_PATH里面是啥?

追踪一下

可以看到是一个DispatcherServlet.properties的文件,然后我们在依赖里面找到了这个文件

看看文件内容是什么

有三个值,分别是BeanNameUrlHandlerMapping、RequestMappingHandlerMapping和RouterFunctionMapping,我们一般常用的是第二个,追踪一下

可以看到,它父类的父类AbstractHandlerMethodMapping实现了InitializingBean这个接口,这个接口用于在bean初始化完成后执行一些特定的自定义初始化逻辑,跟进一下这个接口,可以发现只有一个afterPropertiesSet方法

因为还是回到AbstractHandlerMethodMapping,看看如何实现InitializingBean的afterPropertiesSet,其实是调用了initHandlerMethods这个方法

继续跟进这个方法,发现注释写的清晰明了:扫描ApplicationContext中的bean,然后检测并注册handler methods

那我们在itHandlerMethods这里打断点调试一下

step into:

可以看到步入到了processCandidateBean这个方法,里面有个判断,这个isHandler是在判断什么?跟进看一下

并没有给出实现,说明子类中应该会给出override,直接看子类RequestMappingHandlerMapping中的isHandler方法

上图代码中的isHandler是用来检测给定的beanType类是否带有Controller注解或者RequestMapping注解。OK,那回到AbstractHandlerMethodMapping继续往后看,发现还调用了一个detectHandlerMethods方法,跟进去看一下代码

我们拆看来看

上面这一段先判断handler是否是字符串类型,如果是字符串类型,则通过ApplicationContext获取它的类型,如果不是,直接获取handler的类型。再看下面的部分:

这一段主要是先获取处理器的用户类(未经过代理包装的类),以确保获取到的是实际处理请求的类,至于selectMethods方法,它有两个参数:用户类和回调函数(处理每个方法的映射信息),对于用户类中的每个方法,会尝试调用getMappingForMethod来获取方法的映射信息。跟进getMappingForMethod

额,有点抽象,看不懂,那就去看看子类,看看有没有对应的实现,定位到RequestMappingHandlerMapping#getMappingForMethod,并打上断点调试一下

“RequestMappingInfo info = createRequestMappingInfo(method);”解析Controller类的方法中的注解,生成一个对应的RequestMappingInfo对象,step into进入到RequestMappingHandlerMapping#createRequestMappingInfo中

可以看到这个info里面保存了访问该方法的url pattern是”/“,也就回答了我们在一开始设想的:“假设当我们用@RequestMapping(“/“)注解在方法上的,那Spring MVC是怎么根据这个注解就把对应的请求和这个方法关联起来呢?”,我们继续往下调试,走到了AbstractHandlerMethodMapping#detectHandlerMethods的最后,看一下lambda表达式写的啥

先用selectInvocableMethod方法根据method和userType选择出一个可调用的方法,然后把bean、Method和RequestMappingInfo注册进MappingRegistry

接下来再看看Spring Interceptor引入与执行

Spring Interceptor引入与执行

Spring Interceptor是Spring框架中的一种拦截器机制,而之前提到的Filter是一种过滤机制。在开始之前,先写一个简单的内存马子,方便后面的调试

我们在preHandle函数那里打个断点,一步步调试,发现进入org.springframework.web.servlet.DispatcherServlet#doDispatch这个方法

在doDispatch方法的第一行打下断点,重新访问页面进入调试

可以看到,调用了getHandler这个函数,通过注释了解到:确定处理当前请求的handler,我们step into跟进一下

通过上图代码可以知道,通过遍历当前handlerMapping数组中的handler对象,来判断哪个handler来处理当前的request对象,继续跟踪这个函数里面所用到的mapping.getHandler方法(AbstractHandlerMapping#getHandler)

首先通过getHandlerInternal方法尝试获取处理器,如果获取不到,调用getDefaultHandler方法获取默认处理器,如果仍无法获取,返回null。如果handler是一个字符串,说明它可能是Bean的名字。并通过ApplicationContext获取对应名字的Bean对象,如果没有缓存,调用initLookupPath(request)方法初始化请求路径的查找,最后通过getHandlerExecutionChain方法创建一个处理器执行链。所以接下来,我们步入跟踪一下getHandlerExecutionChain

操作是:遍历adaptedInterceptors,判断拦截器是否是MappedInterceptor类型,如果是那就看MappedInterceptor是否匹配当前请求,如果匹配则将其实际的拦截器添加到执行链中,如果不是这个类型的那就直接将拦截器添加到执行链中,剩下的代码就没什么要说的了,比如下面是getHandler方法的后半段,都是些处理跨域资源共享(CORS)的逻辑

还有就是org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle这个方法,他是getHandler最一开始调用的一个方法,主要是来遍历所有拦截器进行预处理

WebFlux过滤器WebFilter

Spring WebFlux 是 Spring 5 引入的响应式框架,用于构建高性能、非阻塞的 Web 应用程序。传统的Spring MVC在处理请求时,每个请求都会占用一个线程,如果有大量请求同时到达,就需要大量线程来处理,可能导致资源耗尽。WebFlux 提供了与 Spring MVC 类似的编程模型,但更适合处理高并发场景,因为它避免了传统阻塞式 I/O 的性能瓶颈。WebFlux开发的接口无非就两种返回类型:Mono或者是Flux,而对于Spring WebFlux而言,由于没有拦截器和监听器这个概念,要想实现权限验证和访问控制的话,就得使用Filter。它存在两种过滤器,一个是WebFilter,它可以在请求被路由到handler之前或者之后执行一些逻辑;另一个就是HandlerFilterFunction,与WebFilter相比它更加注重函数式编程的风格,可以用于处理基于路由的过滤逻辑。我们以WebFilter为例,搞一搞,先写个简单的Filter,运行的效果如下

直接在filter那里打上断点,开始调试

一步步调试发现return中调用了filter函数,于是step into步入跟踪看一下

可以看到调用了invokeFilter函数,我们仔细看一下这个DefaultWebFilterChain类

有三个名为DefaultWebFilterChain的函数,其中第一个是公共构造函数,第二个是私有构造函数(用来创建chain的中间节点),第三个是已经过时的构造函数,其中,注释上有这么一句话

通过调用 DefaultWebFilterChain 类的公共构造函数,我们初始化了一个完整的过滤器链,其中的每个实例都代表链中的一个link,而不是一个chain,但是通过注释还是可以发现有个方法用来初始化过滤器链,这个方法里面调用的是这个私有构造方法

跟踪一下这个公共构造方法看看是在哪里调用的,跟踪之后发现是在org.springframework.web.server.handler.FilteringWebHandler#FilteringWebHandler

那这样的话,只需要构造一个DefaultWebFilterChain对象,,然后把它通过反射写入到FilteringWebHandler类对象的chain属性中就可以了,那就剩下传入handler和filters这两个参数了,这个handler参数很好找,就在chain里面

至于filters,我们可以先获取到它本来的filters,然后把我们自己写的恶意filter放进去,放到第一位,就可以了,那现在的问题就是怎么从内存中找到DefaultWebFilterChain,我们可以用第三方工具:java-object-searcher,然后将jar包放到Structure中的Libraries里面,然后得稍微修改一下我们写的这个Filter,设置的关键字为DefaultWebFilterChain

设置好之后,直接运行,就可以找到位置

找到位置之后,一步步反射就可以了


剖析Java内存马2
http://example.com/2025/01/22/剖析Java内存马2/
作者
liuty
发布于
2025年1月22日
许可协议