浅谈SpringSecurity与CVE-2023-22602

发布者:SecIN
发布于:2023-06-30 14:13

一、前言

  前段时间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风格模式进行匹配的实现类,那么是否也会有类似的问题。查看源码进行简单的分析:

5fdK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4g2N6I4b7g2m8b7h3h3k6m8b7f1q4C8N6q4)9#2k6V1S2T1L8e0N6g2y4U0R3K6i4K6u0W2M7r3&6Y4" alt="wKg0C2Qq5WqAPPYfAAAktHbm7U683.png" />

二、相关原理

  按照前面的猜想,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方法进行进一步的匹配:

d11K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4V1k6e0b7f1I4@1K9#2u0m8b7f1p5^5P5i4u0V1e0V1W2@1z5o6V1#2z5g2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq6FSALtkRAAA8yrdNIt8959.png" />

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

174K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4V1W2a6b7g2q4B7e0Y4c8m8b7f1y4p5N6X3I4i4g2p5V1H3b7e0t1H3x3#2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq6IOAQjNtAACDvlWTI0A203.png" />

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

be6K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4V1E0w2b7f1&6j5y4%4c8m8b7f1u0H3L8#2f1#2k6f1A6e0j5K6j5K6x3q4)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq6KKANX7tAABpoU5eJSc630.png" />

  如果 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/目录的访问方式):

5deK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4W2k6i4b7g2c8n7j5U0k6m8b7f1y4m8M7h3t1J5e0e0u0A6h3e0V1H3y4g2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq6VWATBb6AACAqb2M2iY905.png" />

  • SpringAntMatcher

  例如Pattern为/admin/*,此时会使用SpringAntMatcher#matches进行解析。具体实现如下:

  从构造方法可以看出实际上是调用的org.springframework.util.AntPathMatcher#match进行匹配:

e40K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4W2W2A6b7f1q4c8N6%4y4m8b7f1x3K6g2K6N6K6f1K6k6H3f1e0R3$3x3g2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq6YiAAQwsAAC3W7sS6pQ861.png" />

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

d87K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4X3u0S2b7f1g2j5f1g2k6m8b7f1u0Q4x3X3c8s2b7e0y4k6x3p5u0g2y4e0M7&6i4K6u0W2M7r3&6Y4" alt="wKg0C2Qq6baAEXQVAABGA3Y0BU579.png" />

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

6c9K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4X3g2&6b7f1E0B7x3X3#2m8b7f1q4E0b7W2W2Q4y4h3k6D9d9h3A6C8x3o6M7@1i4K6u0W2M7r3&6Y4" alt="wKg0C2Qq6eyAKj2mAAAmBYlIjk074.png" />

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

926K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4X3S2A6b7h3c8&6g2W2A6m8b7f1q4$3K9Y4A6m8k6i4m8p5N6K6l9H3x3W2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq6hiAdyVZAAAvjzAepDw002.png" />

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

034K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4X3E0o6b7g2q4Z5z5h3N6m8b7f1y4H3y4#2k6K6b7$3c8c8M7K6M7$3x3g2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq6kCAQh9gAACp7VsCdQs761.png" />

  这里*最后会变成.*

2d8K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4X3!0e0b7f1N6#2z5i4q4m8b7f1b7%4z5q4N6f1f1p5g2#2y4o6f1I4z5q4)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq6oSAGu9qAAD78WTPEu4518.png" />

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

aefK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4Y4q4i4b7g2c8Q4y4h3k6$3K9@1q4m8b7h3#2y4i4K6u0V1L8X3k6@1P5e0l9H3z5o6g2Q4x3X3g2H3L8X3M7`." alt="wKg0C2Qq6qWATvkAAAmMnfty0085.png" />

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

ecbK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4Y4y4&6b7f1c8^5h3i4W2m8b7f1y4b7k6$3Z5&6j5h3E0$3z5o6x3#2y4W2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq6syADxYyAACPgj9akv8356.png" />

2.2 PathPattern

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

a6fK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8g2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4Y4N6S2b7g2c8W2P5h3&6m8b7f1t1H3j5V1W2U0M7q4q4U0h3e0j5H3x3q4)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq6waATeynAAB0bIcpQcY600.png" />

  以spring-webmvc-5.3.22为例,查看具体的解析过程:

  首先从request域中获取PATH_ATTRIBUTE属性的内容,然后使用defaultInstance对象进行处理:

eedK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4Y4W2W2b7g2W2$3y4$3I4m8b7f1u0n7h3g2N6a6k6%4c8%4L8K6l9^5x3#2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq6yeAYv7lAABBYWOgtwo083.png" />

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

f01K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4U0m8w2b7f1c8u0N6K6N6m8b7f1q4K6y4V1c8i4x3h3M7^5j5K6b7$3z5q4)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq60KADIw7AAAs6DW1g8c468.png" />

  这里逻辑会比较简单,缺少一些归一化的处理,例如并不会将//处理成/,也没有处理路径穿越

  通过initLookupPath获取到路径后,会调用lookupHandlerMethod方法,根据请求的uri来找到对应的Controller和method。

  直接查看PathPattern的核心实现,主要在org.springframework.web.util.pattern.PathPattern#matches方法:

0e0K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4U0y4s2b7f1@1H3b7g2y4m8b7f1u0Y4c8Y4V1H3x3@1t1$3d9e0l9@1z5g2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq63GAM0ASAABgFy03B6I049.png" />

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

43eK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4U0k6I4b7f1S2J5h3h3E0m8b7f1u0F1P5W2S2I4g2Y4q4z5z5o6f1%4z5g2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq66qAHrYkAABnzXqVqN8579.png" />

  除此之外,根据不同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 字符。

358K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4#2g2o6b7h3y4U0j5X3N6m8b7f1q4Q4y4h3j5K6g2V1R3&6L8h3V1@1x3U0p5&6i4K6u0W2M7r3&6Y4" alt="wKg0C2Qq7UCAccbgAAA3VH9mi4219.png" />

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

  • spring-core-5.3.21

e47K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4#2A6o6b7f1#2B7b7Y4u0m8b7f1u0w2y4X3I4Y4P5i4q4J5g2e0p5I4z5q4)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq7ZCAMjBrAABK6lgyqrU118.png" />

  • spring-core-5.3.22

e13K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4$3q4e0b7g2g2h3x3Y4q4m8b7f1u0a6e0@1y4B7K9p5A6$3N6K6p5%4y4W2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq7aSAUV2qAABOOCjhJvw176.png" />

  可以看到,在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接口应该是需要认证以后才可以访问的:

d48K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4$3N6e0b7f1A6K6x3%4W2m8b7f1u0V1g2U0m8Q4y4h3k6%4K9$3N6K6x3e0l9$3i4K6u0W2M7r3&6Y4" alt="wKg0C2Qq7gSAJs3yAABdV0wkgs106.png" />

  当使用高版本的Spring时,在进行路由解析时使用的是PathPatternParser。且当这个版本低于5.3.22时,AntPathRequestMatcher是无法匹配行结束符的。

  以5.3.21版本的Spring为例,使用\r或者\n(\r的URl编码为%0d,\n的URL编码为%0a)即可绕过当前的鉴权规则:

fe9K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4$3I4I4b7f1N6h3P5U0g2m8b7f1u0$3b7g2N6o6c8h3W2t1d9e0f1I4z5q4)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq7lqAGVz5AABvAWCEiHI518.png" />

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

e14K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4%4m8E0b7g2u0v1K9K6q4m8b7f1y4K9b7f1c8y4h3i4p5^5y4o6b7#2y4g2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq7pmARJk1AACZADMYq84455.png" />

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

27bK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4%4t1J5b7f1R3J5P5W2m8m8b7f1u0Y4P5o6u0y4h3X3k6z5L8K6b7H3z5q4)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq7r2AH2zPAABgx2MZfNo408.png" />

  同样是上面的SpringSecurity配置,当请求的Controllerr如下,也存在Auth Bypass的问题:

@GetMapping("/admin/{param}")
public String Manage(){
    /*return "Manage page";*/
    return "manage";
}

  当处理{param}时,PathPattern会使用CaptureVariablePathElement进行处理,因为通配符{}中没有正则,所以这里只需要pathElements的元素个数和PathPattern中的元素个数一致都会返回true:

5e5K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4%4g2A6b7g2N6v1b7e0q4m8b7f1y4C8N6Y4S2K9g2#2k6Q4x3X3c8g2x3K6M7H3i4K6u0W2M7r3&6Y4" alt="wKg0C2Qq7uiAWJA1AACkvxZWVU370.png" />

866K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4%4k6S2b7f1N6J5M7U0m8m8b7f1u0&6d9f1u0U0f1Y4g2H3h3e0p5^5x3g2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq7vaAGrr0AAByIBcRupY181.png" />

PS:

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

d8eK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4%4S2o6b7f1k6^5d9i4N6m8b7f1q4D9P5r3M7H3k6V1V1^5g2e0R3@1z5q4)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq7xCAFxIwAAAlxg0fI8U848.png" />

3.2 请求Path归一化差异

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

ad8K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4K6q4I4b7g2u0k6x3p5E0m8b7f1y4p5N6X3I4i4g2p5V1H3b7e0V1K6z5q4)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq71qARY0KAACDvlWTI0A938.png" />

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

816K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4K6y4E0b7h3c8Q4y4h3k6U0K9f1q4m8b7U0m8T1d9h3y4H3f1h3y4k6x3e0p5%4i4K6u0W2M7r3&6Y4" alt="wKg0C2Qq73mAdciAAB0bIcpQcY117.png" />

  同样是前面的场景:

  假设SpringSecurity配置如下:

@Override
protected 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:

7c6K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4y4K6k6E0b7f1I4z5g2r3E0m8b7f1u0@1M7e0N6k6h3V1x3#2z5o6t1%4x3q4)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq76mALNTkAABtq7YZC58270.png" />

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

aabK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4z5p5N6a6b7f1u0n7g2V1y4m8b7f1t1%4g2i4q4Y4k6h3A6p5M7K6t1#2x3q4)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq8GOABBVCAAB7UqgejDs250.png" />

  同样的,如下的场景也会有绕过的风险:

  假设SpringSecurity配置如下:

@Override
protected 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对路径穿越符进行了拦截处理:

8cdK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4z5p5y4a6b7f1#2E0e0$3c8m8b7f1q4&6h3h3S2u0z5e0t1I4b7e0x3&6y4g2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq8COAMmOdAAAyYhI921A395.png" />

四、其他

  实际上Spring官方很早就意识到解析差异带来的风险了。SpringSecurity还有一种匹配模式MvcRequestMatcher。

  参考393K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1L8$3y4K6i4K6u0W2M7%4m8J5K9h3&6Y4i4K6u0W2K9h3!0Q4x3V1k6K6M7s2u0A6L8X3N6Q4x3X3c8K6k6h3y4#2M7X3W2@1P5g2)9J5c8Y4u0W2k6X3g2J5k6h3&6U0k6g2)9J5c8Y4y4W2M7Y4k6D9k6i4c8Q4x3V1k6A6L8Y4c8W2k6%4u0S2N6r3W2G2L8Y4y4Q4x3V1k6E0N6X3y4Q4x3X3g2Z5N6r3#2D9i4K6t1K6L8i4k6U0i4K6u0V1M7X3g2I4N6h3g2K6N6r3#2S2N6r3y4Z5k6i4t1`.

  其使用Spring MVC的HandlerMappingIntrospector来匹配路径并提取变量。相比AntPathRequestMatcher会更严谨。在一定程度解决了差异的问题。避免了前面AntPathRequestMatcher的绕过一些问题。

  同样是前面的例子,使用MvcRequestMatcher 后无法绕过鉴权逻辑:

784K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4z5p5!0i4b7g2c8r3z5p5#2m8b7f1u0$3z5e0c8g2k6g2)9J5k6r3!0G2z5e0R3J5i4K6u0W2M7r3&6Y4" alt="wKg0C2Qq8OWATF8MAABv94Ueoo982.png" />

39dK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4z5p5&6w2b7f1q4#2d9g2)9#2k6V1q4m8b7@1q4W2K9$3u0e0k6$3y4g2x3K6p5K6i4K6u0W2M7r3&6Y4" alt="wKg0C2Qq8NKAAuIAACAekbSgcU313.png" />

89aK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4y4W2j5#2)9J5k6r3W2F1i4K6u0W2j5$3!0E0i4K6u0r3K9h3#2Y4i4K6u0r3M7$3W2F1i4K6u0r3e0e0l9H3i4K6u0r3x3o6q4Q4x3V1j5#2c8W2)9J5c8Y4N6w2k6K6m8o6x3W2q4I4z5r3u0I4b7g2m8h3e0r3#2m8b7f1q4E0g2K6k6K9g2X3q4r3y4o6l9I4y4g2)9J5k6i4m8F1k6H3`.`." alt="wKg0C2Qq8bqAPVLmAAAmW6ZVaF4015.png" />



声明:该文观点仅代表作者本人,转载请注明来自看雪