整体来说就是shiro与spring对uri处理差异导致的漏洞
下面三个CVE复现都是基于此源码:https://github.com/l3yx/springboot-shiro
配置tomcat+war即可。注意如果maven resolve失败的话,证明本地之前已经download过相应的依赖,需要手动到~/.m2/respository下删除
丢出来filter的逻辑,以下操作通过ant风格的语法设置了去检查在访问/admin路由之后的一级目录的用户是否有权限。
路由控制器逻辑(Spring业务层)
关于Shiro自定义Reaml处理流程看笔记“shiro安全框架基础”
这里需要了解一下配置shiroConfig-Filter里的URL是ant格式,路径是支持通配符表示的
?:匹配一个字符
*:匹配零个或多个字符串
**:匹配路径中的零个或多个路径
所以未认证的用户访问/admin/xxx的资源会被重定向到login
访问/admin/xxx%252fxxx/会进入/admin/{name}逻辑,其中对%252f是对/二次url编码
当然这个场景下需要一些限制条件,首先权限ant风格的配置需要是*而不是**,同时controller需要接收的request参数(@PathVariable)的类型需要是String,否则将会出错。
GET请求的数据包如下
GET /srpingboot_shiro_war/admin/gss%252fe HTTP/1.1
Host: 192.168.31.101:8081
Upgrade-Insecure-Requests: 1首先进入shiro的逻辑,WebUtils#getPathWithinApplication处理request请求
步入getRequestUri函数,getServletPath()经过一次urldecode之后赋值给uri,接着uri会步入函数decodeAndCleanUriString
继续跟进decodeRequestString,此时传入的实参uri为/srpingboot_shiro_war//admin/gss%2fe
其中decodeRequestString也是对uri进行urldecode解码操作,decode后的结果如下
至此,我们在get请求传递的2次编码url已经被完全decode。下一步就是Shiro的filter对请求资源的权限校验,主要的步骤跟进AntPathMatcher.class#doMatch

path的值是二次解码后的/admin/gss/e,pattern为Shiro配置中ant格式的匹配字符串。
doMatch函数的大致逻辑是以/分割pattern字符串与path字符串,存为数组pattDirs与pathDirs。而后循环遍历patDirs中的值,通过matchStrings函数与pathDirs数组中的值进行匹配,匹配失败会直接返回false(也就是证明没有权限问题)
如果匹配到了则会每次使pattIdxStart++,例如这里的pattern数组分为[admin,*],则会依次匹配到admin与gss,那么pattIdxStart为2
pattIdxEnd是pattern数组的末尾索引,这里为1
而在之后的判断中,赋值后的pattIdxStart>pattIdxEnd返回false绕过了判断
也就是说,/*这种匹配只能命中/admin/gss这种格式,无法命中/admin/gss/xxx。
绕过Shiro的权限判断后。进入Spring处理路由的流程如下,解码出来的路由为/admin/gss%2fe,当然可以进入@GetMapping("/admin/{name}")控制器中的逻辑
在1.5.3版本,采用标准的 getServletPath 和 getPathInfo 进行uri处理,同时取消了url解码,这样pattIdxStart与pattIdxEnd就是相同的数值
public static String getPathWithinApplication(HttpServletRequest request) {
return normalize(removeSemicolon(getServletPath(request) + getPathInfo(request)));
}
请求数据包如下
GET /srpingboot_shiro_war/admin/%3beee HTTP/1.1
Host: 192.168.31.101:8081
Upgrade-Insecure-Requests: 1其实就是对11989的绕过,思路都是一样的。1.5.3修复之后不会二次解码url但是新增的removeSemicolon函数在分割”;”的时候又产生了问题
步入removeSemicolon发现它就是返回”;”之前的内容,实际上是为了处理GET请求中”/admin/;jessid=xxxx”这样的需求
由于此时path值为/admin,当然不属于请求/admin/*下的资源,因此不会被鉴权
之后进入Spring处理路由时仍匹配到/admin/;xxxx,进入业务逻辑

也可以是把它看作CVE-2020-13933的绕过,1.7.0以前的版本AntPathMatcher.class#doMatch在处理空格时又又又又又把/admin/%20trim后作为/admin,而当作一层目录。导致/admin/与/admin/*不匹配
GET /srpingboot_shiro_war/admin/%20 HTTP/1.1
Host: 192.168.31.101:8081
Upgrade-Insecure-Requests: 1仍然跟到doMatch,tokenizeToStringArray函数将path以反斜线分割为数组,这里我传入的path值为/admin
跟进tokenizeToStringArray函数看具体实现,对于”/“分割之后的token进行了trim()操作
tokenizeToStringArray的返回值会将path字符串分割为数组['admin'],可见此时的空格已被去除,path又被作为一层路径。
接下来的for循环在之前提到过。由于path只包含一个元素,因此我们的pathIdxStart与pathIdxEnd初值均为0;经过一次循环后,pathIdxStart自增为1
因此进入判断去匹配是否有**,很明显没有则返回false绕过权限
原理上来说trim()会清空字符串前后所有的whitespace,空格只是其中的一种,但是在测试中发现除了空格以外的其他whitespace,例如%08、%09、%0a,spring+tomcat 处理时都会返回400

增加了选项,tokenizeToStringArray函数默认不进行trim的操作
这三个洞的精髓就在于:通过二次编码或者”;”绕过Shiro filter对/admin/*的ant。让shiro判断时认为/admin/;sds是一层目录,或/admin/x%252fe认为是三层目录从而绕过权鉴。而Spring在处理这样的uri时只是把它当作了二层目录,会被/admin/{xxx}匹配到从而分发到业务逻辑部分的代码中
当然这几种绕过有一个最大的局限性,就是Spring控制器的代码层在写路由注解时,使用通配符或者泛解模式,比如这里的/admin/{xxx}
如果路由写死为/admin/hpdoger则无法进入Spring相应代码逻辑