查看原文
其他

浅谈SpringSecurity与CVE-2023-22602

tkswifty SecIN技术平台 2024-05-25

点击蓝字




关注我们



前言

前段时间Apache报告了CVE-2023-22602,由于 1.11.0 及之前版本的 Shiro 只兼容 Spring 的ant-style路径匹配模式(pattern matching),且 2.6 及之后版本的 Spring Boot 将 Spring MVC 处理请求的路径匹配模式从AntPathMatcher更改为了PathPatternParser,当 1.11.0 及之前版本的 Apache Shiro 和 2.6 及之后版本的 Spring Boot 使用不同的路径匹配模式时,攻击者访问可绕过 Shiro 的身份验证。
  在Java生态中,还有一个常用的鉴权组件SpringSecurity,其中AntPathRequestMatcher是SpringSecurity中基于Ant风格模式进行匹配的实现类,那么是否也会有类似的问题。查看源码进行简单的分析:


相关原理

   按照前面的猜想,SpringSecurity某种条件下使用的是AntPathMatcher进行匹配,而高版本的Spring使用的是PathPatternParser,利用解析的差异在某些条件下可能会存在绕过鉴权的风险。
  AntPathRequestMatcher是SpringSecurity中基于Ant风格模式进行匹配的实现类。一般使用如下:
protected void configure(HttpSecurity httpSecurity) throws Exception{ httpSecurity.authorizeRequests().antMatchers("/admin/*").authenticated();}
  查看AntPathRequestMatcher以及PathPattern的具体实现:

2.1 AntPathRequestMatcher

  主要的匹配在org.springframework.security.web.util.matcher.AntPathRequestMatcher#matches方法中进行。
  首先判断请求方法是否一致,然后如果pattern是/**的话,说明是全路径匹配返回true。否则获取当前请求的url,然后调用当前matcher的matches方法进行进一步的匹配:

  首先是当前请求url的获取方法,如果没有配置urlPathHelper的话,则通过请求的ServletPath和PathInfo进行拼接,获取归一化处理后的url,否则调用Spring中的 UrlPathHelper (封装了有很多与URL路径处理有关的方法)的getPathWithinApplication方法(进行了URI解码、移除分号内容并清理斜线等进一步的处理)进行获取:

  获取完url后,会调用当前matcher#matches方法进行匹配,从AntPathRequestMatcher的构造器可以看出,这里涉及到两个matcher(SubpathMatcher&SpringAntMatcher):

  如果 pattern 的值以 /**结尾并且不包含路径变量(即通配符{}),会使用SubpathMatcher,否则 matcher 赋值为 SpringAntMatcher 类的对象。
  • SubpathMatcher
  例如Pattern为/admin/**,此时会使用SubpathMacher#matches进行解析。具体实现如下:
  subpath从前面构造方法的调用可以知道主要是通过切割Pattern得到的(pattern.substring(0, pattern.length() - 3)),例如/admin/**切割后的subPath就是/admin。
  首先是统一转换成小写,然后如果请求的path以subpath开头,并且path的长度等于subpath的长度或者subpath长度后第一个字符是/则返回ture(满足/admin或者/admin/目录的访问方式):

  • SpringAntMatcher
  例如Pattern为/admin/*,此时会使用SpringAntMatcher#matches进行解析。具体实现如下:
  从构造方法可以看出实际上是调用的org.springframework.util.AntPathMatcher#match进行匹配:

  核心方法是org.springframework.util.AntPathMatcher#doMatch,首先会调用tokenizePattern()方法将pattern分割成String数组,如果是全路径并且区分大小写,那么就通过简单的字符串检查,看看path是否有潜在匹配的可能,没有的话返回false:

  然后调用tokenizePath()方法将需要匹配的path分割成string数组,主要是通过java.util 里面的StringTokenizer来处理字符串:

  接着将pathDirs和pattDirs两个数组从左到右开始匹配,这里涉及一些正则的转换还有通配符的匹配。以/admin/*为例,首先会调用getStringMatcher方法:

  这里会调用AntPathStringMatcher的构造方法,实际上就是对Patten里的字符进行正则转换:

  这里*最后会变成.*

  最后封装java.util.regex.Pattern对象返回:

  最后调用matchStrings方法调用java.util.regex.compile#matcher进行匹配:

2.2 PathPattern

  Spring Framework的逻辑中,org.springframework.web.servlet.handler.AbstractHandlerMapping#initLookupPath方法中会初始化请求映射的路径,因为高版本默认使用的是PathPattern进行解析,所以会执行this.usesPathPatterns()为true时的逻辑:

  以spring-webmvc-5.3.22为例,查看具体的解析过程:
  首先从request域中获取PATH_ATTRIBUTE属性的内容,然后使用defaultInstance对象进行处理:

  这里会根据removeSemicolonContent的值(默认为true)确定是移除请求URI中的所有分号内容还是只移除jsessionid部分:

  这里逻辑会比较简单,缺少一些归一化的处理,例如并不会将//处理成/,也没有处理路径穿越。
  通过initLookupPath获取到路径后,会调用lookupHandlerMethod方法,根据请求的uri来找到对应的Controller和method。
  直接查看PathPattern的核心实现,主要在org.springframework.web.util.pattern.PathPattern#matches方法:

  这里根据/将URL拆分成多个PathElement对象,然后根据PathPattern的链式节点中对应的PathElement的matches方法逐个进行匹配。举个例子:
  例如Pattern的第一个元素为/的话,会调用SeparatorPathElement的matches方法进行处理,结束后pathIndex++,继续遍历下一个元素进行处理:

  除此之外,根据不同Pattern的写法,还有很多PathElement:
  • WildcardPathElement(/api/*)
  • SingleCharWildcardedPathElement(/api/?)
  • WildcardTheRestPathElement(/api/**)
  • CaptureVariablePathElement(/api/{param})
  • CaptureTheRestPathElement(/api/{*param})
  • LiteralPathElement(/api/index)
  • RegexPathElement(/api/.*)


绕过场景

  

根据前面的分析,利用解析的差异在某些条件下可能会存在绕过鉴权的风险。

3.1 java.util.regex.Pattern模式差异

  对于默认的Pattern模式,不开启DOTALL时候,在默认匹配的时候不会匹配\r \n 字符。

  根据前面的分析,AntPathRequestMatcher解析时,会调用AntPathStringMatcher的构造方法对Patten里的字符进行正则转换并封装成java.util.regex.Pattern对象返回,然后跟请求的Path进行匹配。不同版本间是存在差异的。
  • spring-core-5.3.21

  • spring-core-5.3.22

  可以看到,在5.3.22版本之前,Pattern并没有配置dotall模式,从5.3.22版本开始,配置了dotall模式,此时的表达式.匹配任何字符,包括行结束符。
  结合前面的分析,结合实际场景进行Auth Bypass尝试。
  假设SpringSecurity配置如下:
protected void configure(HttpSecurity httpSecurity) throws Exception{ httpSecurity.authorizeRequests().antMatchers("/admin/*").authenticated();}

访问的Controller如下:

@GetMapping("/admin/*")public String Manage(){ return "manage";}
  正常情况下/admin/page接口应该是需要认证以后才可以访问的:

  当使用高版本的Spring时,在进行路由解析时使用的是PathPatternParser。且当这个版本低于5.3.22时,AntPathRequestMatcher是无法匹配行结束符的。
  以5.3.21版本的Spring为例,使用\r或者\n(\r的URl编码为%0d,\n的URL编码为%0a)即可绕过当前的鉴权规则:

  因为没有启用dotall模式,SpringSecurity匹配/admin/page%0a会失败,然后转由Spring的PathPattern进行解析,首先是admin字符匹配,当解析到*时会使用WildcardPathElement进行解析,若没有下一个Element元素的话,只要pathElements的元素个数和PathPattern中的元素个数一致都会返回true,也就是说page%0a跟*是可以成功匹配的:

  利用上述的差异即可达到auth Bypass的效果,当使用spring-core-5.3.22时,因为AntPathRequestMatcher在匹配时启用了dotall模式,此时的表达式.匹配任何字符,包括行结束符,无法auth Bypass:

  同样是上面的SpringSecurity配置,当请求的Controllerr如下,也存在Auth Bypass的问题:
@GetMapping("/admin/{param}")public String Manage(){ /*return "Manage page";*/ return "manage";}
 当处理{param}时,PathPattern会使用CaptureVariablePathElement进行处理,因为通配符{}中没有正则,所以这里只需要pathElements的元素个数和PathPattern中的元素个数一致都会返回true:

PS:
  • 低版本的Spring使用的是AntPathMatcher,即使绕过了SpringSecurity也会因为解析差异找不到对应的Controller返回404。
  • SpringSecurity高版本的StrictHttpFirewall对\r或者\n(\r的URl编码为%0d,\n的URL编码为%0a)进行了拦截处理:

3.2 请求Path归一化差异

  根据前面的分析,对于SpringSecurity来说,在获取当前请求url时会对请求的url进行一定的处理,例如/admin/..最终会处理为/

  而在Spring Framework的逻辑中,因为高版本默认使用的是PathPattern进行解析,所以会执行this.usesPathPatterns()为true时的逻辑,根据之前的分析,这里会根据主要是对请求URI中的所有分号内容进行处理,判断是移除全部部分还是只移除jsessionid部分,并没有处理编码,路径穿越符等内容:

  同样是前面的场景:
  假设SpringSecurity配置如下:
@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception{ httpSecurity.authorizeRequests().antMatchers("/admin/*").authenticated();}
  访问的Controller如下:
@GetMapping("/admin/*")public String Manage(){return "manage";}
  当尝试访问/admin/..时,AntPathRequestMatcher在处理时会认为当前请求的path是/,在进行匹配的时候因为请求的path为/,在isPotentialMatch方法处理时会认为没有潜在匹配的可能返回false:

  但是对于PathPattern而言,WildcardPathElement解析时若没有下一个Element元素的话,只要pathElements的元素个数和PathPattern中的元素个数一致都会返回true,也就是说..*是可以成功匹配的。

  同样的,如下的场景也会有绕过的风险:
  假设SpringSecurity配置如下:
@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception{ httpSecurity.authorizeRequests().antMatchers("/admin/**").authenticated();}
 访问的Controller如下:
@GetMapping("/admin/**")public String Manage(){ return "manage";}
  如果 pattern 的值以 /**结尾并且不包含路径变量(即通配符{}),会使用SubpathMatcher。匹配逻辑也比较简单,若请求的path以subpath开头,并且path的长度等于subpath的长度或者subpath长度后第一个字符是/则返回ture(满足/admin或者/admin/目录的访问方式)。这里//admin以及/admin/明显是不匹配的。
  但是PathPattern在解析/admin/**时候,在解析/**时会调用WildcardTheRestPathElement进行处理,因为PathPattern通配符只能定义在尾部(不能以/结尾),所以pathElements的元素个数大于PathPattern中的元素个数即可匹配,所以..是可以匹配上/**的,同样的由于SpringSecurity不能解析但是Spring Framework的PathPattern可以解析导致了Auth Bypass问题。
  PS:SpringSecurity高版本的StrictHttpFirewall对路径穿越符进行了拦截处理:


其他

  
实际上Spring官方很早就意识到解析差异带来的风险了。SpringSecurity还有一种匹配模式MvcRequestMatcher。
  参考https://docs.spring.io/spring-security/reference/servlet/integrations/mvc.html#mvc-requestmatcher
  其使用Spring MVC的HandlerMappingIntrospector来匹配路径并提取变量。相比AntPathRequestMatcher会更严谨。在一定程度解决了差异的问题。避免了前面AntPathRequestMatcher的绕过一些问题。
  同样是前面的例子,使用MvcRequestMatcher 后无法绕过鉴权逻辑:




往期推荐



原创 |2023 阿里云CTF / AliyunCTF 部分WriteUp

原创 |基于Win32k内核提权漏洞的攻防对抗

原创 | windows权限维持



继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存