目录
0x01 漏洞描述
1.1 影响版本
0x02 原理分析
2.1 MvcRequestMatcher
2.2 PathPattern
2.3 绕过分析
0x03 漏洞复现
0x04 其他
0x05 修复方式
Spring官方发布了Spring Framework 身份认证绕过漏洞(CVE-2023-20860),当Spring Security使用mvcRequestMatcher配置并将**作为匹配模式时,在Spring Security 和 Spring MVC 之间会发生模式不匹配,最终可能导致身份认证绕过。
Spring官方发布了Spring Framework 身份认证绕过漏洞(CVE-2023-20860),当Spring Security使用mvcRequestMatcher配置并将**
作为匹配模式时,在Spring Security 和 Spring MVC 之间会发生模式不匹配,最终可能导致身份认证绕过。
(其他低于5.3.x的系列版本不受此漏洞影响)
根据漏洞描述,主要是mvcRequestMatcher的问题。参考Spring MVC Integration :: Spring Security
发现mvcRequestMatcher主要使用Spring MVC的HandlerMappingIntrospector来匹配路径并提取变量。相比AntPathRequestMatcher会更严谨。例如mvcMatchers("/index") ,除了匹配/index,对于/index/, /index.html, /index.do也会匹配。避免了AntPathRequestMatcher的绕过一些问题。
在查看mvcRequestMatcher首先简单描述下两个概念:
Pattern:
httpSecurity.authorizeRequests().mvcMatchers("admin/**").authenticated();
这里的/admin/**
就是Pattern。
Path:实际请求的路径
以spring-webmvc-5.3.20版本为例,查看具体实现,主要是在org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher#matches方法进行匹配:
首先会在notMatchMethodOrServletPath方法对请求方法以及servletPath进行简单的比对:
然后调用getMapping方法对当前请求进行处理(实际调用的是HandlerMappingIntrospector的getMatchableHandlerMapping方法):
主要是寻找能处理指定请求的HandlerMapping,继续往下跟进会发现实际调用的是org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal方法,然后调用lookupHandlerMethod方法,首先直接根据路径获取对应的Mapping,获取不到的话调用addMatchingMappings遍历所有的ReuqestMappingInfo对象并进行匹配,实际上就是spring web解析的逻辑:
获取到mapping后调用对应的match方法,跟pattern进行匹配,然后将匹配的结果封装在RequestMatchResult中返回:
继续跟进具体的match方法:
实际上调用的是org.springframework.web.servlet.handler.PathPatternMatchableHandlerMapping#match方法:
继续调用的PathPattern(根据影响的版本可以确定对应的Spring使用的是PathPattern进行解析)的matches方法将pattern跟path进行匹配:
而PathPattern首先会根据/将URL拆分成多个PathElement对象,以/admin/index/为例,这里会分割成多个对象,然后根据PathPattern的链式节点中对应的PathElement的matches方法逐个进行匹配。
简单地分析了mvcRequestMatcher以后,看看PathPattern的工作原理。
2.6以及之后的Spring会使用PathPatternsRequestCondition通过PathPattern来进行URL匹配。
主要在org.springframework.web.util.pattern.PathPattern#matches方法:
首先会根据/将URL拆分成多个PathElement对象,以/admin/index/为例,这里会分割成多个对象,然后根据PathPattern的链式节点中对应的PathElement的matches方法逐个进行匹配:
例如Pattern为/admin/*的话,首先第一个元素是分隔符/
,会调用SeparatorPathElement的matches方法进行处理:
处理完后pathIndex++,继续遍历下一个元素进行处理,下一个是admin,会通过LiteralPathElement#matches进行处理,同样的最后会对pathindex进行+1,然后继续遍历PathElement元素直到遍历结束为止:
在最后会根据matchOptionalTrailingSeparator(此参数为true时,默认为true)进行一定的处理,如果Pattern尾部没有斜杠,请求路径有尾部斜杠也能成功匹配(类似TrailingSlashMatch的作用):
所以这里/admin/index和/admin/index/都是可以访问到对应的路由的。
除此之外,根据不同Pattern的写法,还有很多PathElement:
根据前面的分析,因为mvcRequestMatcher主要使用Spring MVC的HandlerMappingIntrospector来匹配路径并提取变量。猜测大致的问题也应该出现在这里。对比修复前后版本的HandlerMappingIntrospector代码:
这里的返回值从PathSettingHandlerMapping变成了LookupPathMatchableHandlerMapping:
结合前面的分析可知,修改的其实是在调用PathPattern的match方法前的处理。对比下代码可以发现,在调用PathPattern的match方法之前多了一个步骤,首先判断pattern是否以/
开头,如果不是的话进行补全:
根据前面的分析,做以下猜想,在Spring Controller中,以下两个路由访问是等价的:
@GetMapping("/admin/*")
@GetMapping("admin/*")
当Spring Security使用mvcRequestMatcher模式进行权限控制时,如果对应的配置没有以/
开头,根据前面的分析,猜测会因为解析差异导致绕过的问题。
例如如下配置,admin目录下的路由会经过认证处理,非认证通过的会返回403:
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
httpSecurity.authorizeRequests().mvcMatchers("admin/**").authenticated();
}
Controller:
@GetMapping("/admin/index")
public String Manage(){
return "manage";
}
根据前面对PathPattern的matches的分析,这里会根据PathPattern的链式节点中对应的PathElement的matches方法逐个进行匹配,当pattern为admin/**
时,此时第一个Element是admin,对应LiteralPathElement#matches解析:
此时会获取path的第一个Element(如果访问的path是/admin/index,那么第一个Element是/
):
而/
明显不是PathSegment的实例,此时匹配失败会返回false,但是Spring Controller却能正常解析,那么便导致了绕过问题。
根据前面的猜想,同样的是前面的case,当对应的配置没有以/
开头,会因为解析差异导致绕过的问题,可以看到成功绕过了设置的Spring security规则,访问了/admin/index:
这里再做一个对比,将spring-webmvc切换到5.3.9版本,此时上述的case是无法绕过的:
在Spring生态中,除了PathPattern以外,还有一种解析模式是AntPathMatcher。
同样的根据之前的分析,其实主要是PathPattern的解析方式问题,这里再做一个假设,同样是存在缺陷的版本spring-webmvc-5.3.20版本,此时通过properties配置切换匹配模式为AntPathMatcher:
spring.mvc.pathmatch.matching-strategy = ant_path_matcher
此时发现没办法绕过了:
简单说下原因,主要是AntPathMatcher的实现,其大概方式是将需要匹配的path和Pattern分割成string数组,分别是pathDirs和pattDirs两个数组,然后从左到右开始匹配,主要是一些正则的转换还有通配符的匹配。例如/admin/*的*
实际上是正则表达式.*
,然后通过java.util.regex.compile#matcher进行匹配,这里进行分割时不会将/
作为内容引入分析,从调试信息也可以看到,在此之前也将/
进行了补全:
将/
进行了补全的操作主要是在RequestMappingHandlerMapping#match方法中进行的:
这里实际上调用的是RequestMappingInfo#build方法:
这里对Pattern进行了重新处理,在调用PatternsRequestCondition的构造方法的时候,调用了initPatterns方法:
如果Pattern不是以/
开头会进行补全:
目前官方已有可更新版本,建议升级至:
同样是上面的case,将spring-webmvc版本升级到5.3.26后,上述case已无法绕过:
org.springframework
spring-webmvc
5.3.26