Shiro越权学习2.0
前言
之前年初看到su18大师傅对Shiro的CVE做了整理,里面的好几个漏洞都没有调过,写这篇文章记录自己的学习过程。可能有些地方有问题还请师傅们指教。
CVE-2020-11989
https://issues.apache.org/jira/browse/SHIRO-782
影响范围:shiro < 1.5.3
原理
ContextPath
前置条件
- 必须配置有context-path
- Spring控制器中没有另外的权限校验代码
参考作者文章
这个洞算是前面CVE-2020-1957补丁的一个绕过,前面学习CVE-2020-1957时了解到的修复是针对PathMatchingFilterChainResolver#getChain
获取URI的底层实现不再直接request.getRequestURI()
而是拼接getContextPath()
+getServletPath()
+getPathInfo()
在这里下断点,看看当我们请求;/bypass/;/bypass/;/aaa
时各个部分的取值
request.getContextPath()
得到/;/bypass
request.getServletPath()
得到/bypass/aaa
request.getPathInfo()
得到null
则最终拼接得到/;/bypass//bypass/aaa
,然后进入decodeAndCleanUriString()
方法
前面讲过这个decodeAndCleanUriString()
方法遇到;号会截断
所以会返回一个/
最终在getChain处,requestURI的值是/
,用/
去参与Ant匹配可以正常通过Shiro的鉴权。
而spring处理/;/bypass//bypass/aaa
请求可以正常顺利的到达路由。
所以这个越权的大锅在request.getContextPath()
这里
URL编码
前置条件
- PathVariable 注解的参数只能是 String 类型
- Ant匹配/*
参考腾讯文章
前面讲到getRequestUri()
方法取uri的值时,拼接getContextPath()
+getServletPath()
+getPathInfo()
后会进入decodeAndCleanUriString()
方法
这里对uri做了一次url解码操作,也就是说Shiro会对url有2次解码,而tomcat只会进行1次解码,利用中间的差异性就能达到绕过鉴权的效果。
当Ant表达式这样写
map.put(“/login”, “anon”);
map.put(“/aaaaa/**”, “anon”);
map.put(“/bypass/*”, “authc”);
正常触发/bypass/xx 这个路由就是我们的目的
请求bypass/bypass/2%252f1
,在PathMatchingFilterChainResolver#getChain
下断点
可以看到getChain的requestURI就变成了/bypass/2/1
当我们Ant配置/bypass/*
时,是匹配不到/bypass/2/1
的,只有当Ant配置/bypass/**
才能匹配到/bypass
多级子目录的内容,所以可以绕过Ant的匹配实现权限绕过
实战
只有当配置/byass/*这种Ant表达式才能触发,而且两种利用方式都还有一些各自的额外条件。
所以总的来说感觉这个漏洞实战不太容易黑盒遇见。
一定要打就下面两种POC
1 | /;/bypass |
修复
https://github.com/apache/shiro/commit/01887f645f92d276bbaf7dc644ad28ed4e82ef02
首先不再用request.getContextPath()
额外处理context
还有getPathWithinApplication()
方法之前是从requestUri
减去contextPath
,现在是getServletPath()
加上getPathInfo()
,然后用removeSemicolon
方法截断;
号,现在直接拼接得到uri,所以不会再触发decodeAndCleanUriString
做url解码。
CVE-2020-13933
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-13933
影响范围:shiro < 1.6.0
原理
前置条件
- PathVariable 注解的参数只能是 String 类型
- Ant匹配/*
这个漏洞是CVE-2020-11989补丁的一个绕过。
CVE-2020-11989补丁后,Shiro通过拼接getServletPath()+getPathINfo()
,然后执行removeSemicolon()
方法处理;
号,最后normalize()
方法标准化路径。其中getXXX(request)
方法会做一次url解码操作。
即Shiro获取URI,首先是url解码处理,其次才会对;
号做处理。
然而在Spring的处理顺序却略有不同,在方法UrlPathHelper#decodeAndCleanUriString
中
是先用removeSemicolonContent()
方法对;
号做处理,再做url解码处理。
而这个对;
做处理的实现中,显然我们url编码可以绕过这里对;
号的处理。
举一个简单的例子说明Shiro和Spring对URI处理的差异,当我们发起/bypass/%3bok
请求时,
- Shiro会先对url做解码,然后遇到
;
号做截断处理,最终参与Ant匹配的是/bypass/
,而用Ant配置/bypass/*
匹配不了/bypass/
,绕过了权限鉴定 - Spring会先处理
;
号,但是我们url编码可以绕过这个处理,其次才会做url解码,则经过Spring处理得到/bypass/;ok
,Spring把;ok
整体当成字符串,参与Handler的匹配,可以匹配到/bypass/{id}
这个路由
关于Spring的详细处理过程可以参考这个Hu3sky师傅的文章,总之由于Spring和Shiro处理后结果的差异,导致我们可以实现权限绕过
实战
因为必须配置/bypass/*
才能利用,一般程序员不太会这样写,所以实战中黑盒很难碰到..
POC记录
1 | 在要访问的路径前加%3b |
修复
Shiro 1.6.0对这个漏洞做了修复,参考https://github.com/apache/shiro/commit/dc194fc977ab6cfbf3c1ecb085e2bac5db14af6d
新增了InvalidRequestFilter
用于全局匹配过滤
isAccessAllowed
方法遇到下面中任一个字符就会返回false
;
号及其url编码\
号及其url编码- 非Ascii字符
CVE-2020-17510
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-17510
影响范围:shiro < 1.7.0
原理
前置条件
类似CVE-2020-11989
感觉也算上面漏洞的绕过,还是利用Spring和Shiro处理uri字符的结果差异实现权限绕过,这次用到的是%2e
。
具体效果如su18师傅文章中所述:
例如访问:”/audit/%2e/“时
- Shiro url decode:”/audit/./“
- Shiro 标准化路径:”/audit/“
- Spring 标准化路径:”/audit/%2e/“
- Spring url decode:”/audit/.”
和上面一个漏洞很像,Shiro和Spring处理后得到的uri值不一样,Shiro那边/audit/
通过了权限验证,然而Spring这边真正访问的却是/audit/.
,达成了权限绕过。
但我这里复现不了su18师傅文章中的情景,我用的spring-boot-starter-parent-2.3.x
和spring-boot-starter-parent-1.5.x
搭建的demo环境spring部分的代码和师傅的代码有出入。
我这边复现时遇到的关键点在UrlPathHelper.getLookupPathForRequest
,这里会由于不同的spring版本导致return的结果不同。
这里有个关于springboot %2b的trick,如Ruilin师傅所说:
当Spring Boot版本在小于等于2.3.0.RELEASE的情况下,
alwaysUseFullPath
为默认值false,这会使得其获取ServletPath,所以在路由匹配时相当于会进行路径标准化包括对%2e
解码以及处理跨目录,这可能导致身份验证绕过。而反过来由于高版本将alwaysUseFullPath
自动配置成了true从而开启全路径,又可能导致一些安全问题。
假如请求/bypass/%2e2e
在高版本的Spring中,由于alwaysUseFullPath
默认为true,所以在UrlPathHelper#getLookupPathForRequest
直接返回了用this.getPathWithinApplication()
处理后的/bypass/..
,所以在Spring中仍然把..
当成字符拿去请求/bypass/*
路由。
直接绕过
但是当Spring版本不高时,用getPathWithinServletMapping
处理后得到的却是bypass/..
,少了关键的一截/
导致匹配不上,
看看getPathWithinServletMapping
中的处理,核心代码如下
关键在这个getRemainingPath
方法
getRemainingPath
把requstUri和mapping差异的部分取出来,即把getPathWithinApplication
和getServletPath
结果中相同的部分做个减法。
这个处理很恶心,直接导致我在spring-boot-starter 2.3.0及以下的版本我这边都复现不了。
不过我换上更高版本的Shiro后,在高版本spring依然可以利用%2e
这个姿势绕过。
实战
实战中也有点鸡肋..
在要尝试越权的路由后面加上盲测POC
1 | %2e |
修复
https://github.com/apache/shiro/commit/6acaaee9bb3a27927b599c37fabaeb7dd6109403
建议看看su18师傅的分析
创建了 UrlPathHelper 的子类 ShiroUrlPathHelper,并重写了
getPathWithinApplication
和getPathWithinServletMapping
两个方法,全部使用 Shiro 自己的逻辑WebUtils#getPathWithinApplication
进行返回。设置后,Spring 匹配 handler 时获取路径的逻辑就会使用 Shiro 提供的逻辑,保持了二者逻辑的一致。从而避免了绕过的情况。
CVE-2020-17523
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-17523
影响范围: shiro < 1.7.1
原理
前置条件
类似CVE-2020-11989
又一个和前面差不多的绕过,这次换了个字符%20
。
请求/bypass/%20
,
在PathMatchingFilterChainResolver#getChain
断点看看Shiro中的处理
会进入pathMatches
方法匹配pattern
一路可以跟到doMatch方法
可以看到在tokenizeToStringArray
对path参数处理后空格被处理掉了,一路跟tokenizeToStringArray
方法
可以看到有一个trim()处理空格,即Shiro在做pattern匹配时默认会处理空格。最终/bypass/%20
变成了/bypass/
,绕过Shiro鉴权。
而在Spring对uri的处理流程中,默认把%20
当成字符处理了,所以可以正常匹配到/bypass/*
这个路由的handler
成功绕过
参考这个思路,fuzz发现在Shiro 1.6.0以前%00--%20
都可以绕过
但是前面讲到CVE-2020-13933的补丁对非ASCII字符做了判断,所以在Shiro 1.7.0只有%20这个特殊字符。
实战
这一篇文章讲的洞,截至目前为止,利用场景都和CVE-2020-11989的场景类似,都有很多条件限制,实战很难遇到。
黑盒盲打情景就在想要尝试越权的路由后面加上%20
1 | /admin/%20 |
修复
https://github.com/apache/shiro/commit/ab1ea4a2006f6bd6a2b5f72740b7135662f8f160
关键就是tokenizeToStringArray这里第三个参数设置了false
这个参数为false则默认不会做trim()操作处理空格
CVE-2021-41303
https://issues.apache.org/jira/browse/SHIRO-825
影响范围: shiro =1.7.1 ?
原理
搜了一下公开分析这个CVE的好像只有threedr3am师傅和su18师傅,但两个师傅也没有百分百确定这个CVE就是它们分析的这样,因为这个如果算成一个漏洞的话确实有点鸡肋..不管了记录下师傅们的分析..
首先是Shiro-825的一个bug
当配置
map.put(“/audit/list”, “authc”);
map.put(“/audit/*”, “anon”);
即/audit/
目录下只有list需要权限验证,
此时我们访问/audit/hello
正常
但访问/audit/hello/
抛出报错
跟到DefaultFilterChainManager#proxy
根据我们console里的报错信息,可以推断是this.getChain这里返回了null,跟进getChain方法
这里的chainName竟然是/audit/hello
,this.filterChains
里当然是没有这个chainName的,所以会抛出报错
根据报错的堆栈网上找找chainName是怎么来的,跟到DefaultFilterChainManager#proxy
的上面PathMatchingFilterChainResolver#getChain
这里的requestURINoTrailingSlash
就是后面的chainName,而requestURINoTrailingSlash
的值是removeTrailingSlash(requestURI)
拿到的,这里removeTrailingSlash就是把reuqestURI去除/
后缀,这个就是我们用户输入可控的,即Shiro会去自己的filterChains中找完全跟用户输入相匹配的uri,显然是找不到的。threedr3am师傅分析了似乎只有Shiro 1.7.1这里是这样实现的,所以这个bug只影响Shiro 1.7.1。
那怎么利用这个bug实现越权呢?首先配置
map.put(“/audit/*”, “auth”);
map.put(“/audit/list”, “anon”);
注意这个顺序,虽然/audit/list
可以支持匿名访问,但是/audit/*
在前面,所以正常情况下访问/audit/list也需要权限认证。
正常访问/audit/list提醒登录
但是访问/audit/list/可以绕过
实战
这个洞真是鸡肋他妈给鸡肋开门,鸡肋到家了..
我不太觉得实战环境能遇到这洞…要黑盒找就随缘加个/
符号在末尾试试..
修复
https://github.com/apache/shiro/commit/4a20bf0e995909d8fda58f9c0485ea9eb2d43f0e
把requestURINoTrailingSlash
换回pathPattern即可
总结
总算把su18师傅文章里梳理的Shiro权限绕过CVE全部过了一遍,有一说一,真正有用点的洞好像还是上一篇文章里的Shiro-682和CVE-2020-1957…目前为止看到的这些洞,可以用以下几点总结
- Ant表达式的
/bypass/*
匹配不到/bypass/或/byapss/xxx/
- Spring版本对CVE有影响,主要是
alwaysUseFullPath
默认值不同,建议阅读Ruilin师傅 Spring Boot 中关于 %2e 的 Trick - 框架例如Spring和Shiro对URI的处理顺序或对某些特殊字符处理有差异(CVE-2020-13933,CVE-2020-17510,CVE-2020-17523),在CVE-2020-17510补丁后Spring 匹配 handler 时获取路径的逻辑就会使用 Shiro 提供的逻辑,保持了二者逻辑的一致。
- 更新的补丁存在缺陷(CVE-2020-11989,CVE-2021-41303)
- 最强大的洞个人觉得是CVE-2020-1957,可以绕过
/**
的匹配 - 还有个李三师傅用ajp协议实现越权,所以估计后续还会有一些不同协议对URI处理差异的细节可以利用。