浅谈SpringMVC参数处理过程

在Java生态中,Spring全家桶是比较常见的。浅谈SpringMVC的参数处理过程。

0x00 前言

传统的 Java Web 项目,通常会通过 HttpServletRequest 来获取请求相关的参数。

request.getParameter("param")

而Spring MVC 简化了请求参数的获取方式,直接将请求参数定义为Controller方法参数即可。当Controller方法被 Spring MVC 调用时,Spring MVC 会根据请求上下文信息解析出给定类型的方法参数值,并自动进行类型转换和参数校验。

image.png

下面看看SpringMVC是如何接收并处理请求有关的参数的。

0x01 请求参数解析过程

以5.3.26版本为例,查看具体的解析过程。

当Spring MVC接收到请求时,Servlet容器会调用DispatcherServlet的service方法(方法的实现在其父类FrameworkServlet中定义),首先会获取request请求的类型,除了PATCH方法以外都会通过HttpServlet的service方法进行处理:

image.png

实际上根据不同的请求方法,会调用processRequest方法进行处理,例如GET请求会调用doGet方法:

image.png

在执行doService方法后,继而调用doDispatch方法处理,首先会对multipart请求进行处理,然后获取对应的mappedHandler,其实就是获取到url 和 Handler 映射关系,然后就可以根据请求的uri来找到对应的Controller和method,处理和响应请求:

image.png

然后会获取适合处理当前请求的HandlerAdapter,并通过调用handle()方法处理请求:

image.png

实际上调用的是handleInternal方法进行处理:

image.png

首先会对请求进行检查,确保请求的有效性,然后判断是否需要在会话级别进行同步,如果需要,获取当前请求的HttpSession,并根据会话获取一个互斥对象,对该对象进行同步:

image.png

否则调用invokeHandlerMethod()方法,传递HttpServletRequest、HttpServletResponse和HandlerMethod对象,执行实际的请求处理逻辑,并返回一个ModelAndView对象:

image.png

首先创建ServletWebRequest对象,将HttpServletRequest和HttpServletResponse封装起来,以便后续处理:

image.png

根据处理器方法获取DataBinderFactory和ModelFactory,然后创建ServletInvocableHandlerMethod对象,用于执行处理器方法的调用和处理:

image.png

经过一系列的初始化还有处理后会执行处理器方法并处理结果:

image.png

这里会调用invokeForRequest()方法对请求进行处理:

image.png

在invokeForRequest方法中,会调用 getMethodArgumentValues() 方法获取方法的参数值。这些参数值会根据请求的特定上下文和配置进行解析:

image.png

首先调用 getMethodParameters() 方法获取参数列表,如果参数列表为空,表示没有参数,直接返回空数组:

image.png

然后遍历参数列表,依次处理每个参数,调用 findProvidedArgument() 方法查找是否有在 providedArgs 中提供的参数值,如果有则将其赋给 args[i]:

image.png

否则需要使用参数解析器进行解析,首先检查是否有合适的参数解析器支持当前参数,如果不支持则抛出异常:

image.png

这里实际调用了getArgumentResolver进行处理:

image.png

在getArgumentResolver中,首先,检查缓存中是否已经存在适用于给定参数的解析器。如果存在,则直接返回缓存中的解析器。如果缓存中不存在适用的解析器,则遍历已配置的解析器列表。对于每个解析器,它会调用supportsParameter方法来判断是否支持给定的参数类型。如果找到了支持的解析器,则将其缓存,并返回该解析器:

image.png

得到解析器后就可以调用参数解析器的 resolveArgument() 方法解析参数的值:

image.png

首先调用 getArgumentResolver() 方法获取与方法参数类型相对应的参数解析器。 如果解析器为 null,表示不支持当前参数类型,抛出异常并指明不支持的参数类型,如果解析器存在,则调用解析器的 resolveArgument() 方法来解析参数的值:

image.png

以解析带有命名值注解(如 @RequestParam)的方法参数的方法为例,首先会获取方法参数上的命名值注解的相关信息,例如参数名称、默认值等,然后获取对应注解中定义的参数名称,并根据需要解析其中的表达式和占位符,然后通过解析后的参数名称,在请求中查找对应的参数值,如果找不到则抛出异常,因为因为参数不能为空:

image.png

否则将解析后的参数名传递给 resolveName() 方法,从请求中获取参数值:

image.png

解析得到的参数值将被用作方法参数的实际值,并在后续处理中进行转换、绑定和验证。

以上是请求参数解析的大概过程。实际上主要是通过 HandlerMethodArgumentResolver 接口解析Controller的参数。根据前面的分析,会在getArgumentResolver方法中遍历已配置的解析器列表,并找到合适的解析器,大概有28个解析器:

image.png

0x02 解析器解析过程

根据前面的分析,在getArgumentResolver中,如果缓存中不存在适用的解析器,则遍历已配置的解析器列表。对于每个解析器,它会调用supportsParameter方法来判断是否支持给定的参数类型。如果找到了支持的解析器,则将其缓存,并返回该解析器。也就是说可以通过解析器的supportsParameter方法查看当前解析器解析的参数类型

举个例子,以下是RequestParamMapMethodArgumentResolver的supportsParameter方法:

image.png

从supportsParameter可以看到,使用@RequestParam注解标注且为Map类型的参数均会被该解析器解析,举例说明:

@RequestParam Map param

下面以最常见的param=value为例,查看具体的解析过程。主要是通过RequestParamMethodArgumentResolver的supportsParameter方法进行解析的:

image.png

从supportsParameter可以看到,满足下列条件的都可能被RequestParamMethodArgumentResolver解析:

  • @RequestParam注解标注且非Map类型

举例说明:

@RequestParam("param") String param
@RequestParam String param
  • 未经过@RequestPart注解标注
  • Multipart参数(包括Part、Part[]、List、List、MultipartFile[]等)

举例说明:

MultipartFile file
  • 基本类型及包装类型(包括Enum、CharSequence、Number、Date、Temporal、URI、URL、Locale、Class类型)

image.png

举例说明:

String param
  • 可选请求参数

举例说明:

@RequestParam("password") Optional password
@RequestParam(required = false) String param

然后是具体的解析逻辑,主要是在resolveName方法进行解析,首先,通过 request.getNativeRequest(HttpServletRequest.class) 获取到 HttpServletRequest 对象,以便后续处理:

image.png

然后是对multipart请求的处理:

image.png

简单的跟进可以看到这里主要是调用对应上传解析类进行处理文件上传相关的参数:

image.png

如果不存在文件参数,则通过request.getParameterValues(name) 获取普通的请求参数值。如果存在请求参数,则返回单个参数值或参数值数组:

image.png

这里有个有趣的trick,HTTP参数污染漏洞(HTTP Parameter Pollution)简称HPP,由于HTTP协议允许同名参数的存在,同时,后台处理机制对同名参数的处理方式不当,造成“参数污染”。攻击者可以利用此漏洞对网站业务造成攻击,甚至结合其他漏洞,获取服务器数据或获取服务器最高权限。例如tomcat对同样名称的参数出现多次的情况会取第一个参数。

而对于Spring来说,当处理@RequestParam("param") String param时,若请求param=1&param=2时,根据前面的分析,主要会进行如下处理,首先通过request.getParameterValues(name) 获取普通的请求参数值。如果存在请求参数,则根据实际情况返回单个参数值或参数值数组:

image.png

当请求param=1&param=2时,会以数组的形式返回1,2:

image.png

利用该特性,在某种程度上可能可以绕过类似WAF这类的安全防护软件。

举个例子:

一般情况下,对于SQL注入的拦截思路通常包括检测和过滤可能包含恶意SQL代码的输入。其中,检测闭合的括号前后内容是一种常见的策略。

正常情况下,sortType是用于OrderBy查询的参数,存在SQL注入的风险:

image.png

查询1/0时返回500,说明division by zero异常被触发:

image.png

通过盲注尝试进一步利用,对应的payload:1/(case+when+ascii(substr(user,1,1))=112+then+1+else+0+end),通过枚举对应用户名的ascii码触发division by zero异常进行利用,这里类似substr()函数基本上是会被waf/filter拦截掉的。

根据前面的分析,因为resolveName方法返回的arg类型是Object,可以适应各种不同类型的结果。这使得方法可以在不同的上下文中使用,无需限定具体的返回类型:

image.png

那么此时就可以利用RequestParamMethodArgumentResolver的解析方式通过,对payload进行拆分。

当枚举当前数据库用户的第一位字符ascii为111时,返回0,触发division by zero异常:

image.png

当枚举当前数据库用户的第一位字符ascii为112时,正常返回,说明当前数据库用户的第一位字符为p:

image.png

整个过程对substr()进行了一定的拆分,在某些情况下利用该思路可能可以绕过一些安全防护。

除此之外,还有还多不同类型的解析器,例如PathVariableMethodArgumentResolver和PathVariableMapMethodArgumentResolver用于解析@PathVariable注解,RequestPartMethodArgumentResolver用于解析@RequestPart注解等等。就不一一赘述了。

  • 发表于 2023-08-03 09:00:00
  • 阅读 ( 5945 )
  • 分类:代码审计

0 条评论

请先 登录 后评论
tkswifty
tkswifty

64 篇文章

站长统计