之前协助渗透做审计的时候遇到了shiro版本不高,当时想到了shiro的权限绕过漏洞,但是在网上试了好几个payload都没成功 ,现在空下来了分析下shiro权限绕过的原理及触发条件。
为什么会导致权限绕过?
首先观察一下shiro权限绕过的payload为/xx/..;/
,并且在官方也强调了是和spring配合使用时出现的问题,不难判断出出现这个问题的原因在于shiro和spring处理url的不一致导致的,而spring在请求处理过程中会去除/./和/;xxxasxdasd/中的内容,通过这种方式变形的url可能和shiro配置的filter不一致,导致了权限绕过。
shiro的认证流程是怎样的?
既然所有的权限绕过的漏洞都是在认证过程中导致的bypass,所有要了解bypass权限认证的关键点在于了解shiro的认证流程。通过大概入门了shiro的流程,发现其实shiro本质上是通过配置filter来对用户请求的url进行认证的。
简单配置一个shiro的环境,其中关于Shiro filter的配置是在ShiroFilterFactoryBean中,可以看到这里将拦截的url放到一个map中,通过setFilterChainDefinitionMap设置到filterChainDefinitionMap属性中。
ShiroFilterFactoryBean实现了FactoryBean接口,在spring容器启动时会调用getObject获取对应的bean对象。在ShiroFilterFactoryBean
中,通过createInstance创建Bean对象。
在createInstance
中首先获取SecurityManager对象,这个对象是shiro的核心管理器,所有的安全相关的操作都是通过SecurityManager执行的。由于我们在配置ShiroFilterFactoryBean是已经创建了SecurityManager对象,所以这里是不为null的。下来创建了FilterChainManager,FilterChainManager中维护了拦截器链。
在org.apache.shiro.spring.web.ShiroFilterFactoryBean#createFilterChainManager
中,首先通过DefaultFilterChainManager创建默认过滤器。
下面获取了我们配置的两个过滤器,将这两个过滤器添加到过滤器链,并封装到DefaultFilterChainManager中,最终将过滤器链封装到PathMatchingFilterChainResolver
中。
通过上面的配置,我们已经拿到了我们配置的过滤器链。现在我们看下请求时过滤器链如何工作。
在org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain
中,会获取我们配置好的过滤器链,和请求的url进行匹配,匹配成功则创建一个proxy过滤器链,并将匹配到的filter放进去,匹配不到则返回空。
得到FilterChain后,会调用doFilter过滤方法,也就是调用代理filter的doFilter方法,在代理filter中其实也就是循环调用匹配到的过滤器的doFilter方法。
CVE-2020-1957
shiro是如何导致权限绕过的?
经过上面的分析,我猜测绕过的主要原因是在org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain
中绕过了url匹配的步骤。下面为了证实我的想法我使用payload进行测试并观察。当我使用/;/test/1
请求时,可以看到这里拿到的请求已经发生了变化。按照上面的url请求,其实spring只会去除/;asdasdas/
中的内容,并不会将后面的内容去空,而在shiro中已经将;后的内容全部去掉了,这里出现了和spring处理的不一致性。
而我们得到的requestURI是经过org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getPathWithinApplication
处理得到的。其中url获取的部分在WebUtils.getPathWithinApplication
,而经过getRequestUri的处理后uri变成了/,所以问题主要出在这个方法中。
获取到uri后经过了decodeAndCleanUriString和normalize的处理。在decodeAndCleanUriString中首先对url请求解码,解码后再提取;前的内容作为url,这也是;截断请求的原因。
而normalize中会对请求中的/./或者//或/../进行替换,这也是为什么在shiro中无法通过/./绕过的原因。
上面我们分析的是shiro是如何处理uri的,但是这个权限绕过还有一个前提,就是spring在去请求具体的controller时,请求的不是shiro处理后的uri。经过调试可以看到org.springframework.web.util.UrlPathHelper#getRequestUri
中spring并没有获取shiro处理后的url做匹配。接下来spring会通过移除/;/的内容,并导致我们可以请求到/test/1这个路由。
还有没有其他绕过方法?
1./.;/test/1
这个url会被shiro当作/.识别,在spring中经过decodeAndCleanUriString处理后会变成/./test/1
,再通过org.springframework.web.util.UrlPathHelper#getServletPath
处理,会变成/test/1,因此这种变形也可以实现。
/..;/test/1
无法绕过,直接使用这种方式在tomcat处理时就会报错。
/aaa/..;/test/1
这个url经过shiro处理后,会变成
/aaa/..
,当然匹配不到任何过滤器,在spring中经过处理会变成/aaa/../test/1
,最后经过getServletPath处理后转成/test/1
最终执行请求。
绕过的条件?
虽然看上去很多配置都可以绕过,但是其实和shiro的config配置有关,经过上面的分析,我们的能绕过的请求在shiro处理后会变成下面几种形式。
1 | 1. / |
如果在使用shiro时,做了这样的配置map.put("/**", "authc");
,那么无论怎么接收的url是什么样的,也会被shiro的过滤器匹配到,所以这么配置的shiro是无法绕过的。
如何修复这个漏洞?
在高版本中,主要修复代码如下:
这里不再直接对用户传入的url进行处理了,而是通过getServletPath处理后再经过removeSemicolon的处理。getServletPath是通过request.getServletPath获取的。
而之前分析请求绕过,其实spring最终决定哪个url匹配是通过getServletPath获取的,也是通过request.getServletPath获取的。这里就保证了处理的url的一致性,也就没办法绕过了。
CVE-2020-11989
为什么可以通过这种方式绕过?
通过上一个权限绕过的分析,想要绕过shiro的权限认证,其实本质上也是绕过了filter的正则匹配。这个洞本质上来讲也是绕过了权限匹配。首先,根据https://xlab.tencent.com/cn/2020/06/30/xlab-20-002/
的描述,只有在满足特定的条件下,才能导致权限绕过的漏洞。
1 | 1. 使用map.put("/hello/*", "authc"); 而不是map.put("/hello/**", "authc"); |
首先了解下在shiro中配置*和**的区别
1 | *表示/后的一个或多个字符串,也就是`/hello/aa/x`这种是匹配不到的 |
下面看下使用/hello/*
是怎么进行路径匹配的,匹配路由的逻辑还是在org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain
中。
其中主要在org.apache.shiro.util.AntPathMatcher#doMatch
中完成匹配,通过/将路径分割,并将分割后的路径分组后依次匹配,匹配成功则增加pattIdxStart
计数,否则直接return fasle结束。
下面有个比较重要的判断,当pathIdxStart
和pattIdxEnd
不一致时,也会返回false。
比如当我请求/hello/aaaa/111
时,就会导致这种不一致。
虽然使用/hello/aa/11
这种语法可以绕过shiro的过滤器,但是同样不会被Spring匹配到,因为spring这里只使用了一个参数。
所以有没有什么方法可以让shiro在匹配时当作两个参数,而在spring接收时当作一个。我们知道spring在处理时的url是和request.getServletPath()
对应的路径是一致的。而在修复方案中,确实已经通过接收request.getServletPath()
来当作shiro的处理路径了,但是这里有个问题,就是shiro在接收到request.getServletPath()
的路径后还通过decodeAndCleanUriString
进行了一次url解码。所以只要让request.getServletPath()
接收到的是url编码后的值就可以产生不一致导致到绕过。
payload如下:/hello/a%25%32%66a
为什么只能使用@PathVariable
的方式绕过?
虽然使用上面的方式确实绕过了shiro的权限认证,但是我们想想,spring在处理请求时还是通过request.getServletPath()
的结果处理的,而如果我们使用编码的方式绕过,spring最终接收到的url是/hello/a%2fa
,其中包含了一个url编码后的值,那么如果我们使用@RequestMapping
指定值xxx
来接收,一定是匹配不到任何路由的。所以在/hello
后只能是一个变量,所以这也就是只能使用@PathVariable
的原因。而且由于我们接收的内容中有url编码后的内容,后端如果拿到这个内容进行处理会报错,所以如果想控制这个@PathVariable
的内容来做某些操作也是不行的,所以这个漏洞虽然是一个CVE,但是在实际利用时是没什么用的,除非通过@PathVariable
接收到内容后不使用,但是这与开发者使用@PathVariable
来接收内容的本意肯定是不符的。
如何修复这个漏洞?
Shiro在1.5.3版本在获取到servletPath后不再使用url解码。
CVE-2020-13933
为什么可以绕过?
1 | /hello/%3ba |
根据之前的经验,绕过的还是org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain
的匹配。这里可以看到我们的输入被处理为/hello
,这里根据CVE-2020-11989
的经验,这里会由于pathIdxStart
和pattIdxEnd
不一致而导致匹配不上。
而在request.getServletPath
获取的路径仍然是/hello/;a
,这里又导致了解析url不一致的问题。
为什么请求会转为/hello
?
在获取servletpath后,会通过removeSemicolon
移除;
后的内容,所以此时url变为/hello/
。
在getChains
方法中,当匹配url以/结尾,会去除末尾的/。
漏洞利用条件是什么?
通过上面的分析,上述利用方式是CVE-2020-11989
的变形,所以利用条件还是需要通配符使用*
而不是**
,通过下面的demo可以证明。
同理,也需要@PathVariable
,;
也会被带入到变量中,想要通过控制变量的内容深入利用是不行的。
为什么需要对;进行url编码?
通过上面的分析,我们即使不给;
编码,同样可以绕过shiro的匹配,那么为什么要给;
进行url编码呢?
下面我们回顾下SpringMVC的处理流程,当发起一个请求到达SpringMVC,首先会经过DispatcherServlet#doDispatch
,由this.getHandler(processedRequest);
获取对应的Handler,再通过Handler.handle方法处理请求,最后将返回的ModelAndView对象交给视图解析器解析后返回。
所以请求被哪个Handler处理取决于如何根据URL匹配到对应Handler的,这个过程在getHandler中执行,handlerMappings包含多个HandlerMapping
,SpringMVC会循环调用HandlerMapping.getHandler
方法,直到获取到一个handler。
而我们通过@RequestMapping
注册的方法是由RequestMappingHandlerMapping
来处理的,所以我们主要关注RequestMappingHandlerMapping.getHandler
方法,由于这个类没有getHandler方法, 所以会执行父类AbstractHandlerMapping.getHandler
方法,在这个方法中,通过getHandlerInternal
获取handler。
AbstractHandlerMethodMapping#getHandlerInternal
首先获取requestPath,再调用lookupHandlerMethod
查找path对应的HadlerMethod,在springMVC启动时,会将@RequestMapping
修饰的方法注册为HandlerMethod。
lookupHandlerMethod
还有一些复杂的方法调用,最终交给org.springframework.web.servlet.mvc.condition.PatternsRequestCondition#getMatchingPatterns
完成请求url和RequestMapping 设置的请求路径的匹配。
当;
没有编码时,Spring在处理过程中会将;及其之后的内容替换为空,所以得到的lookupPath为/hello/
,这是匹配不到/hello/{name}
的。
而当;
编码后,这里得到的lookupPath为/hello/;test
是可以匹配到的,才能交给对应的方法处理。
如何修复这个漏洞?
升级shiro到1.6.0
版本,在这个版本,新增了默认的Filter,这个Filter的拦截范围为/**
,在org.apache.shiro.spring.web.ShiroFilterFactoryBean#createFilterChainManager
中可以看到对应的代码。
继续跟进,发现将/**
交给InvalidRequestFilter进行处理。
新增了默认Filter后,无论我们怎么绕过一定会被/**
匹配到。
在InvalidRequestFilter的doFilter方法执行前,先调用onPreHandle
做一些前置处理,通过isAccessAllowed
方法对请求验证。
在isAccessAllowed
中验证请求内容是否包含黑名单中的内容。
总结
Shiro的权限认证漏洞归根结底还是和Spring会处理/;xxx
的内容有关,通过shiro过滤器和Spring获取请求URL的不一致性产生了绕过。从漏洞利用的角度来讲,只有CVE-2020-1957
影响最广,利用条件是不能配置map.put("/**", "authc");
,也就是说即使版本存在漏洞,也可以通过配置map.put("/**", "authc");
来防御权限绕过漏洞。至于其余两个绕过则显得比较鸡肋,即使绕过了Shiro的权限认证,也无法有效的控制输入。