URL解析导致的鉴权绕过问题探究-SpringSecurity篇
上篇文章中分析了Resin解析URL的流程和解析过程中存在的绕过技巧。本文则主要分析Spring生态中的核心鉴权框架-Spring Security,研究了其鉴权解析流程、用于匹配鉴权的RequestMatcher实例类的逻辑、可能存在鉴权绕过的场景,复现分析了由于Spring Security与多个后端框架(SpringMVC、自写Servlet、SpringWebflux)解析不一致而导致的鉴权绕过漏洞。在分析过程中也发现了公网未提到的几个绕过场景, 如果对文章有疑问/建议或者想一起研究交流的师傅,欢迎私信~
1、Spring Security 解析过程
解析过程基于的是spring-security 5.6.3版本
真实情况是最开始从org.apache.catalina.core.ApplicationFilterChain#doFilter开始解析,这部分属于tomcat的解析逻辑,一直到org.springframework.security.web.FilterChainProxy#doFilter就属于了spring security的范畴。在当前类的doFilterInternal方法中,调用spring security的HttpFirewall接口进行恶意字符的校验
HttpFirewall接口有2个实现类:DefaultHttpFirewall、StrictHttpFirewall(严格模式)
spring security默认使用的就是StrictHttpFirewall严格模式的校验
依次来看下:
1、rejectForbiddenHttpMethod 校验请求方法,如果请求方法不在默认的这7个方法中,会报错RequestRejectedException
2、rejectedBlocklistedUrls 这个方法会对url内容进行检查。如果url中包含特殊字符,那么会报错不再执行后续逻辑
1 |
|
主要限制了:
1 |
|
对于请求路径/hello/./../test2来说,filterchain调用rejectedBlocklistedUrls时并不会报错,但是会在后面的isNormalized 方法中报错
/hello/%2e/test2 访问时
当然,如果系统本身业务确实需要传入限制字符,也可以调用方法去除限制:StrictHttpFirewall#setAllowSemicolon设置为true表示允许;出现、setAllowUrlEncodedPeriod设置%2e、setAllowUrlEncodedDoubleSlash设置//字符
3、rejectedUntrustedHosts 默认为true
4、isNormalized 对4个地址进行处理,如果返回false 那么请求就会报错:The request was rejected because the URL was not normalized.
1 |
|
如果包含/. /..
字符,说明路径未规范化,那么会报错
5、containsOnlyPrintableAsciiCharacters 查看request.getRequestURI()
是否包含非ascii字符
另外一个 DefaultHttpFirewall相对严格模式的StrictHttpFirewall 而言逻辑很简单,在getFirewalledRequest方法中:1、调用isNormalized 检查ServletPath跟PathInfo的归一化; 2、调用containsInvalidUrlEncodedSlash检查RequestURI是否包含%2f(/)
1 |
|
2、Spring Security RequestMatcher匹配及绕过
RequestMatcher接口的实现类用于匹配请求url是否符合系统定义的匹配规则,如果match方法返回true表示匹配 需要认证。
下文提到的鉴权绕过问题 总的来说就是Spring Security与后端框架对于url pattern的解析不一致导致RequestMatcher#match匹配不到请求路由 返回false,但是后端却能将路由解析到具体方法的情况
Spring Security历史上出现的绕过问题多是这样的情况,如CVE-2023-34034 是与Spring WebFlux组合时的绕过、CVE-2023-34035 是与自写Servlet组合时的绕过、CVE-2023-20873 是与Cloud Foundry组合时的绕过、CVE-2022-22978 RegexRequestMatcher、CVE-2023-20860 mvcRequestMatcher 是与spring mvc组合时正则.默认不匹配换行符、 无前缀的通配符无法正确匹配路由的问题。
如下是几个重点的RequestMatcher鉴权类及一些鉴权绕过的细节分析
2.1 AntPathRequestMatcher
AntPathRequestMatcher#matches
首先判断鉴权配置方法与用户传入的方法是否一致,如果不一致,返回false 放过鉴权;如果正则为/**
,那么返回true;如果不是/**
,那么调用AntPathRequestMatcher#getRequestPath获取url后进行matcher匹配。根据系统设置的过滤规则,matcher分为SpringAntMatcher、SubpathMatcher,在实例化AntPathRequestMatcher对象时就会指定matcher到底是哪一个,如果pattern满足条件:不为/**
、不为**
、以/**
结尾、且不包含 ? { }
这三种字符、倒数第二个位置的字符是*
,则创建一个 SubpathMatcher 对象,否则指定为SpringAntMatcher对象
实例配置如下,spring security就会创建SubpathMatcher进行匹配
1 |
|
SubpathMatcher#matches,区分大小写开关caseSensitive(默认为true),然后将用户传入的path与subpath进行比对,subpath是pattern.substring(0, pattern.length() - 3),即本次演示的/hello
如果path与subpath相同,或者以subpath开头、后面紧跟着/,那么会鉴权,返回403
如果配置是/hello/test2,那么使用SpringAntMatcher进行匹配判断
1 |
|
2.2 AntPathRequestMatcher 绕过
spring mvc trailingSlashMatch 属性绕过
当spring security对后端的spring mvc controller进行鉴权时,由于spring mvc的trailingSlashMatch配置属性默认为true,会认为/hello/test2/与/hello/test2 相同,都能定位到具体业务方法。但是spring security会认为这是两个url,所以当配置对/hello/test2鉴权时,用户可以使用/hello/test2/进行绕过
1 |
|
访问/hello/test2 需要鉴权
访问/hello/test2/ 绕过鉴权
这种绕过方式最早(不完全考证)由landgrey师傅报送官方,但是官方回应文档已建议使用MvcRequestMatcher替代AntPathRequestMatcher,没发布专门的安全公告
https://docs.spring.io/spring-security/site/docs/5.5.0/reference/html5/#MvcRequestMatcher
spring mvc SuffixPatternMatch 后缀匹配模式绕过
当spring security对后端的spring mvc controller进行鉴权时,如果启用了SuffixPatternMatch 后缀匹配模式,/hello/test2与/hello/test2.do 的匹配结果是一样的。但是spring mvc在5.3及之后的版本将 RequestMappingHandlerMapping#useSuffixPatternMatch 默认值true改为了false,所以此绕过方式适用于spring mvc <5.3
https://github.com/spring-projects/spring-framework/issues/23915
在国产系统蓝*就出现过这个问题,该系统使用了低版本的spring mvc 5.0.19,useSuffixPatternMatch默认为true,支持后缀匹配。同时此系统在spring.xml进行权限配置时,判断gif、jpg、tmpl等结尾的路由是静态资源,默认不经过系统安全鉴权
这样利用SuffixPatternMatch 后缀匹配的特性,访问/xx/target.tmpl(不需要权限)达到与/xx/target(需要权限)相同的效果,从而绕过spring secutiry的鉴权
低版本关闭SuffixPatternMatch 的方法
1 |
|
2.3 RegexRequestMatcher
RegexRequestMatcher#matches方法根据正则模式进行匹配,如果是正常的请求方法,会获取servletpath 调用java.util.regex.Pattern#matcher进行正则表达式匹配
使用如下权限配置,/hello/下的路径都需要经过认证
1 |
|
2.4 RegexRequestMatcher绕过
当使用RegexRequestMatcher配置校验固定路径、/.*
、/*
等时,可使用?字符进行绕过
1 |
|
/hello/test2?
2.5 RegexRequestMatcher 绕过 CVE-2022-22978
RegexRequestMatcher#pattern默认的匹配模式不匹配\r \n换行符,所以当配置校验是/hello/.*
时,可使用/hello/test2%0a
绕过
修复的commit: https://github.com/spring-projects/spring-security/commit/70863952aeb9733499027714d38821db05654856
在默认情况下,正则表达式中的.不会匹配换行符。而修复后的Pattern.DOTALL模式是单行模式:更改了.的含义,使它与每个字符都匹配,包括换行符\r \n
常量值参考:https://docs.oracle.com/javase/8/docs/api/constant-values.html
2.6 MvcRequestMatcher
早期的spring security默认使用AntPathRequestMatcher匹配鉴权,AntPathRequestMatcher与spring mvc HandlerMappingIntrospector匹配路由的解析差异 导致鉴权/index被 /index/, /index.html 绕过。为此官方新增了MvcRequestMatcher类,改为与后端spring mvc一致的HandlerMappingIntrospector类匹配路径
鉴权匹配的起点是org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher#matches
该方法主要操作有两步:1是根据用户请求的url获取处理的HandlerMapping实例类;2是根据第一步获取的HandlerMapping#parser解析器解析规则pattern,再进行路由url与规则pattern的匹配工作
1、根据url获取对应的MatchableHandlerMapping实例类,这里获取的是PathPatternMatchableHandlerMapping
HandlerMappingIntrospector#doWithMatchingMapping依次遍历handlerMappings,调用其getHandler方法后将返回结果传递给PathSettingHandlerMapping并返回
handlerMappings有5个,依次遍历。第一个是RequestMappingHandlerMapping,不存在getHandler,所以会一直调用到爷爷类AbstractHandlerMethodMapping#getHandlerInternal,其调用的lookupHandlerMethod方法返回的HandlerMethod实例是具体处理该路由的hander方法(Controller中定义的方法)
而且doWithMatchingMapping方法这里是个lambda表达式,在方法中执行apply方法时会执行{}内的逻辑代码
1 |
|
最终根据获取到的mapping及requestPath路径组装HandlerMappingIntrospector.PathSettingHandlerMapping实例并返回,接着执行match方法
2、调用PathPatternMatchableHandlerMapping#match进行匹配,分为两步:
第一步调用PathPatternParser#parse解析系统配置的鉴权正则pattern,处理了/ ? { } : *
这几个字符出现在pattern的情况,根据/将正则pattern拆分得到多个对应的PathElement实例集合,最终返回PathPattern实例
第二步调用PathPattern#matches开始真正的匹配工作:遍历上一步得到的PathElement实例节点,调用对应的pathElements实例match方法进行匹配
其他类型规则的PathElement实例
1 |
|
第一步,如果权限匹配规则是**,最终返回的PathPattern实例
第二步接着调用PathPattern#matches开始真正的匹配工作:依次根据上一步获取的PathElement进行匹配
**对应的PathElement实例是RegexPathElement,所以会调用匹配到的是RegexPathElement#matches
2.7 MvcRequestMatcher 绕过 CVE-2023-20860
在23年3月份,Spring官方发布了一份cve-2023-20860安全通告,表示MvcRequestMatcher 在使用无前缀双通配符(**)的情况下,会存在Spring Security与Spring MVC的解析逻辑不一致导致的绕过
CVE-2023-20860官方公告链接:https://spring.io/security/cve-2023-20860
影响版本: Spring Framework 6.0.0 to 6.0.6、5.3.0 本地测试复现版本:5.3.24
开发者通常会认为**表示全路径,在使用Spring Security的mvcMatchers鉴权时,可能会使用如下配置规则
1 |
|
控制器HelloController
1 |
|
按照平常思维理解,**鉴权表示对任意路径进行鉴权。但是实际测试发现访问/hello/test2、/test3 都不需要进行鉴权,这里的鉴权是失效的
绕过的核心本质在于:MvcRequestMatcher对于pattern规则的解析与spring webmvc对于无前缀url的解析不一致导致的绕过问题
对于spring controller而言,如下两个的路由是等价的
1 |
|
但是对于spring security的MvcRequestMatcher使用的PathPattern来说,并不等价。所以当正则不以/开头时,是匹配不到/hello/test2路由的
MvcRequestMatcher使用PathPattern对鉴权正则pattern与请求路由url两者是否匹配进行判断,而PathPattern根据不同的pattern会选择不同的PathElement实例处理
1 |
|
想到之前的RegexRequestMatcher存在换行符绕过的问题,RegexPathElement是否存在呢,查看最新版代码发现增加了Pattern.DOTALL,换行符无法利用了
修复:在新版本的HandlerMappingIntrospector#getMatchableHandlerMapping中将返回的PathSettingHandlerMapping替换为LookupPathMatchableHandlerMapping,这两个的区别是后者的match方法对于正则pattern不是/开头的 都在开头添加/,解决了**匹配不到路由的问题
HandlerMappingIntrospector.LookupPathMatchableHandlerMapping#match
仔细观察修复方案是在HandlerMappingIntrospector中修复的,那么spring security与其他框架结合时的匹配是不是也可能存在问题?tkswifty师傅排查到了spring security与webflux结合的时候也存在类似于CVE-2023-20860的问题:CVE-2023-34034,复现详情见:https://forum.butian.net/share/2373
2.8 requestMatchers 绕过 CVE-2023-34035
除了spring mvc、spring webflux,还有可能集成自写的servlet,spring security与自写servlet的解析也存在着解析差异,官方分配漏洞编号:CVE-2023-34035
发现者是在测试 GraphQL 项目与 Spring Security集成时, Spring Security得鉴权不生效 从而报告给了官方。官方判定是由于 GraphQL 依赖项将一个额外的 servlet 注册到 servlet 上下文中 。导致与 requestMatchers(String) 匹配规则冲突导致的鉴权失效问题,发现者记录原文:https://deepkondah.medium.com/how-i-discovered-cve-2023-34035-improper-authorization-f597812f2fac
漏洞复现:总体项目代码有三个类:鉴权类、Controller控制器类、自写的Servlet类,另外还需要在springboot启动类添加@ServletComponentScan注解,具体代码如下
鉴权SecurityConfigHigh类代码
1 |
|
Controller控制器类代码
1 |
|
自写的Servlet类代码
1 |
|
鉴权配置含义是对除/manage/**
、/admin/**
外的路由放行,对/admin/**
进行ADMIN角色权限判断,对/manage/**
进行MANAGE角色权限判断。但是由于 Spring Security 的requestMatchers("/admin/**")
规则不能正常匹配用户自写的Servlet url导致的鉴权功能失效
访问/manage/page,返回403无权限,鉴权功能正常
访问/admin/page,可以看到成功请求到UserServlet#doGet方法并返回方法执行结果,鉴权功能失效
修复:使用AntPathRequestMatcher是安全的,而使用MvcRequestMatcher存在匹配不到的问题。所以spring security改变了AbstractRequestMatcherRegistry创建RequestMatcher实例的逻辑
总体来说是:5.8.4及之前的spring security在存在spring controller的情况下就会直接创建MvcRequestMatcher,而新版本并未对MvcRequestMatcher代码做改变,而是改变了创建MvcRequestMatcher的条件:1、存在spring controller;2、context继承WebApplicationContext、且存在servletContext;3、存在DispatcherServlet且只有一个Servlet。只有满足了这几个条件,才会调用createMvcMatchers创建MvcRequestMatcher
这样一来,我们上面复现的例子存在除DispatcherServlet另外的Servlet,所以创建的是AntPathRequestMatcher,修复了鉴权绕过的问题。但其实如果系统显式指定自写Servlet使用MvcRequestMatcher,依旧会存在匹配不到导致鉴权绕过的问题。但是官方也针对这个情况做了处理,如下图是手动指定MvcRequestMatcher匹配自写Servlet时,spring项目启动报错提示
1 |
|
尝试利用寻找能注册servlet,但是spring无法完全统计识别servlet数量
的思路,绕过如上的检测。但是测试了4种注册servlet的方式,spring security均能检测到,都无法强制使用MvcRequestMatcher进行强制匹配
1 |
|
最后一种注册的方式比较奇特,开发者不需要指定路由,路由url是servlet的名字前缀,且末尾需要添加/才能正常访问。所以在使用ant进行匹配时,单纯匹配/admin是无效的,需要匹配/admin/
3、挖掘鉴权绕过场景
根据我们上面漏洞分析的经验来看,想要挖掘一个新的绕过场景,可以从两方面进行发力:1、找各种后端解析的pattern,挖掘ant、regex、mvc等解析器与后端对同一个pattern理解不一致的情况;2、基于历史漏洞找,找同一个利用方式在不同框架的应用,例如CVE-2023-34034与CVE-2023-20860的关系
对于第一种方式,我注意到了在servlet中/admin/*
的pattern,可以匹配/admin,但是在spring security理解中是无法匹配/admin,可能会存在潜在的安全绕过
在CVE-2023-34035的修复方案中,官方建议后端是spring mvc endpoints 只能使用MvcRequestMatcher ,其他的endpoints都使用AntPathRequestMatcher,所以我在尝试将AntPathRequestMatcher与自写的servlet结合时,发现了这个问题
自写的servlet 有三种匹配模式:1精确匹配 /admin/index;2路径匹配 /admin/*;3后缀匹配 *.do 精确匹配已经被分配了CVE-2023-34035,后面两种匹配模式都存在绕过的场景
漏洞复现,使用spring security V5.8系列的最新版5.8.7
1 |
|
当访问/admin/mawkdemawk路径时,spring security起了作用,返回403无权限
而当访问/admin时,spring security的AntPathRequestMatcher对匹配/admin失效,但servlet依然能够请求到UserServlet#doGet方法
另外一个问题就是上文提到的RegexRequestMatcher关于?处理的问题
这个问题在最新版依然存在
1 |
|
希望对正在研究此类问题的师傅有帮助~