Shiro/Spring权限绕过
MVC框架
MVC:Model-View-Controller,即控制器(Controller),模型(Model),视图(View)
使用MVC模式的好处是,Controller专注于业务处理,它的处理结果就是Model。Model可以是一个JavaBean,也可以是一个包含多个对象的Map,Controller只负责把Model传递给View,View只负责把Model给“渲染”出来,这样,三者职责明确,且开发更简单,因为开发Controller时无需关注页面,开发View时无需关心如何创建Model
Servlet
Servlet 是一个实现了特定接口的 Web 组件,由 Servlet 容器去加载并运行。容器
本身并不一定是 Web 服务器,但容器需要至少支持 HTTP 请求,并将请求的内容封装成 Servlet 接口的参数;因此容器通常与 Web 服务器集成或者作为其拓展而存在。目前常见的 Servlet 容器如 Tomcat、GlassFish、JBoss 等,同样也具备 Web 服务器的功能
- Servlet:能处理HTTP请求并将HTTP响应返回
- JSP:一种嵌套Java代码的HTML,将被编译为Servlet
- Filter:能过滤指定的URL以实现拦截功能
- Listener:监听指定的事件,如ServletContext、HttpSession的创建和销毁
SpringBoot
核心功能
- 起步依赖
起步依赖本质上是一个Maven项目对象模型(Project Object Model,POM),定义了对其他库的传递依 赖,这些东西加在一起即支持某项功能
简单的说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能
- 自动配置
Spring Boot的自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素,才决定 Spring配置应该用哪个,不该用哪个。该过程是Spring自动完成的
威胁模型
Java Web 应用的请求流程,大致可以抽象成如下的链路:
1 | Client -> Filter_1 -> Filter_2 -> ... -> Filter_n -> Servlet |
即客户端的请求会经过一个或者多个 Filter,然后再到达实际处理请求的 Servlet 中。通常 Filter 以级联方式运作,称为 FilterChain,而出于低耦合的设计模式考虑,开发者一般会将重要的鉴权逻辑放在 Filter 中实现。一旦认证失败,可以提前终止请求,这对于 Servlet 而言是透明的。
因此所谓的鉴权绕过,很多情况下就是鉴权 Filter 中的逻辑错误。而 Filter 中的鉴权,大部分情况下也是 URL 粒度的鉴权,毕竟在一个网站中总是会有无需认证的前台界面(如登录界面),以及需要认证的后台服务(如管理后台)
从代码上看,Filter 中鉴权使用的多是 HttpServletRequest.getRequestURI() 接口,判断对应路径是否需要鉴权,如果需要则进行 Session 的判断和认证,对于鉴权失败的请求则返回 403 拒绝访问或者 302 跳转到登录界面
大多的路径归一化问题是因为鉴权的过滤器或者拦截器对于url的处理与spring最后对路由分发时的处理不一致,导致鉴权失败,从而可以未授权访问系统
Tomcat
首先是第一步,parsePathParameters
,该方法会解析 Path Parameter 即路径参数,路径参数是针对每一级 URL 目录的参数,形如 /api;a=b/flag;c=d
,使用 分号 ;
进行指定,并以等号 =
指定 key 和 value。解析路径参数之后会将其使用 Request.addPathParameter
加入到请求信息中,并且将其从 decodeURI 中删除
第二步,URL Decode,正常的 URL 解码
第三步,Normalization,主要针对 URL 中包含 “\“, “//”, “/./” 以及 “/../” 的情况,操作过程如下:
- 将
\
替换为/
; - 将
//
替换为/
; - 对于
/.
或者/..
结尾的 URI,先在末尾额外添加一个/
; - 递归解析 URI 中的
/./
字符串,将其替换为/
; - 递归解析 URI 中的
/../
字符串,移动相应的目录;
在解析 /../
时如果超出了根目录会直接返回 false。此外一些其他的错误也会返回 false,比如 CoyoteAdapter#ALLOW_BACKSLASH
为 false 却包含反斜杠,路径不以 /
或者 \
开头等。
第四步,Character decoding,使用 convertURI 对路径进行字符解码。因为此时的路径还是以 ByteChunk
的格式进行存储的,这一步会将其转换为 CharChunk
;
在依次经过上述处理后,最终的 URI 才会用来进行 Servlet 路由查找
Bypass Tricks
据此,我们可以得到一系列 “Bypass Tricks”,即用不同的方式路由到同一个 Servlet 中,比如目标路由是 /api/flag
的话,以下请求都能寻址到目标:
/api;a=b/flag
: 通过 Path Parameter 路径参数变异;/api/%66%6C%61%67
: 通过 URL 编码进行变异;\api\flag
: 通过 Normalization 1 变异,当前需要 Tomcat 配置ALLOW_BACKSLASH
为true
;//api/flag
: 通过 Normalization 2/3 变异;/api/./flag
: 通过 Normalization 4 变异;/foo/api/../api/flag
: 通过 Normalization 5 变异;
这些变异方法可以相互组合进行使用,另外配合 DefaultServlet 针对磁盘文件和资源的路由也可以组合出其他的 URI
removeSemicolonContentInternal
removeSemicolonContentInternal这个函数主要做了两件事:
1、移除所有的分号
2、移除分号后面直到下一个斜杠”/”之间的所有字符
因此
鉴权案例
starts/endswith
springweb对请求的处理:url先经过过滤器,如果过滤器中逻辑稍有不当,便有可能存在绕过鉴权的情况
1 | String requestUri = request.getRequestURI(); |
这是一个常见的对静态文件不进行鉴权的过滤器,然而此时则可以通过**/api/xxxxxx;js**对该鉴权进行绕过。因为filter处理url时if是通过了if的逻辑,而到了spring的doDispatch中则由刚刚所述的处理,将其分发到了/api/xxxxxx正确对应的接口
由此产生了很多鉴权问题,在filter之中对url处理稍有不慎变会导致该问题的产生
某OA
1 | public String path(String path) { |
首先 URI 解码,然后替换反斜杠,也不管 URI 的标准了,..
直接删掉,路径参数也直接删掉,还有空格、//
之类的都进行了处理,看起来无懈可击。但其实仔细看一下会发现还是有一些问题:
- uriDecode 中可能会失败,导致根本没解码成功而返回原始的路径;
- URI 解码之后,路径可能会存在大写,如果是 WIndows 中请求 JSP 等文件可以正确路由;
- 路径参数的删除在
./
之后,所以.;xxx/
处理后依旧存在./
; - 删除空格
\s
在归一化//
之后,所以/%20/
依旧可以绕过替换返回//
; - …
可见尽管有时候开发者知道要过滤什么字符,但是思维局限不够发散全面,写出的代码依旧漏洞百出。因此,更为科学的方案是使用知名的、经过检验的鉴权框架,而不是尝试自己处理
alwaysUseFullPath
alwaysUseFullPath主要用于判断是否使用servlet context中的全路径匹配处理器,会对路径进行规范化处理
小于等于spring-boot-autoconfigure-2.3.0.RELEASE默认为false,会获取ServletPath并进行对应的处理,主要是对uri标准化处理,例如解码然后处理跨目录等一系列操作
大于spring-boot-autoconfigure-2.3.0.RELEASE默认为true,当请求路径中包括类似..
的关键词时,调用getPathWithinApplication方法解析后,会因为没有处理跨目录的字符,导致找不到对应的Handler而返回404
例如:
当alwaysUseFullPath为false时,调用了getPathWithinServletMapping进行处理,跨目录字符解码并规范化后,成功匹配对应的handler并访问成功
当alwaysUseFullPath为true时,调用的是getPathWithinApplication,没有对跨目录进行标准化处理,最终找不到对应的handler,返回404状态码
SuffixPatternMatch/TrailingSlashMatch(后缀/结尾匹配模式)
低版本的 spring-webmvc 及其相关组件,包括:
1 | spring-webmvc <= 5.2.4.RELEASE |
SuffixPatternMatch是后缀匹配模式,用于能以 .xxx 结尾的方式进行匹配。这里46对应的Ascii码是.
,根据具体代码可以知道,当启用后缀匹配模式时,例如/hello和/hello.do的匹配结果是一样的
TrailingSlashMatch为true时,会应用尾部的 **/ ** 匹配,例如/hello和/hello/的匹配结果是一样的:
假设存在资源文件 resources/static/secret.txt
,对应的路由是 /secret.txt
,可以使用下面的变体:
/secret.txt/
: 预处理时候会变成secret.txt
删除前方以及末尾的分隔符;/;/secret.txt
: 预处理时候删除连续的分隔符,和//
的差别是处理阶段不同;/secret.txt;a=b
/secret;a=b.txt
: 预处理时候会删除路径参数;\secret.txt
: 基于 processPath 中将\
替换成/
的变异;//secret.txt
: 基于 cleanDuplicateSlashes 的变异,因此前面遗留的连续分隔符不会影响;/ / // secret.txt
: 基于 cleanLeadingSlash 的变异;/secret.%74%78%74
: 寻找文件前会通过 encodeOrDecodeIfNecessary 进行 URL 解码;/foo/.././/secret.txt
: createRelative 中会组合路径,并将其当做最终路径;
这些路径变异方式可以组合进行使用,从而根据需要构造出更加复杂的变异。值得注意的是其中某些变异可能会在 Web 容器就被拦截,比如Tomcat在碰到路径中包含(未编码的)空格时会直接返回400错误
\x00
Spring-security 认证绕过
CVE-2016-5007
影响版本:
Spring Security 3.2.x,4.0.x,4.1.0
访问控制匹配器不识别受保护的路径,因此应用默认的“允许”规则,而控制器查找器匹配器找到受保护的控制器。两个匹配器之间的严格性不匹配导致了这种情况。
该漏洞危害性高低与否取决越权查看页面后查询数据的时候是否有校验当前用户的session
绕过方式: 在URL中将空格(或其他空格字符)附加到受保护路径前后
1 | /admin%20/ |
CVE-2022-22978
影响版本:
Spring Security 5.5.x < 5.5.7 Spring Security 5.6.x < 5.6.4
当Spring-security使用 RegexRequestMatcher 进行权限配置,由于RegexRequestMatcher正则表达式配置权限的特性,正则表达式中包含“.”时,利用换行符可实现权限认证进行绕过
绕过方式:
1 | /admin%0a |
shiro权限绕过
Apache Shiro 是一款 Java 安全框架,不依赖任何容器,可以运行在 Java SE 和 Java EE 项目中,它的主要作用是用来做身份认证、授权、会话管理、缓存和加密等操作
其和SpringSecurity的作用是一致的,但SpringSecurity由于其功能丰富而复杂则多用在大型项目中,而Shiro则适用于中小型项目中,上手快
CVE编号 | 漏洞说明 | 漏洞版本 |
---|---|---|
CVE-2016-6802 | ContextPath 和 RequestURI 处理不一致导致绕过 | shiro <1.3.2 |
CVE-2020-1957 | Spring 与 Shiro 对于 “/“ 和 “;” 处理差异导致绕过 | Shiro <= 1.5.1 |
CVE-2020-11989 | Shiro 二次解码导致的绕过以及 ContextPath 使用 “;” 的绕过 | shiro < 1.5.3 |
CVE-2020-13933 | 由于 Shiro 与 Spring 处理路径时 URL 解码和路径标准化顺序不一致 导致的使用 “%3b” 的绕过 | shiro < 1.6.0 |
CVE-2020-17510 | 由于 Shiro 与 Spring 处理路径时 URL 解码和路径标准化顺序不一致 导致的使用 “%2e” 的绕过 | Shiro < 1.7.0 |
CVE-2020-17523 | Shiro 匹配鉴权路径时会对分隔的 token 进行 trim 操作 导致的使用 “%20” 的绕过 | Shiro <1.7.1 |
1 | authc:配置的url都必须认证通过才可以访问,它是Shiro内置的一个过滤器 |
CVE-2010-3863
影响版本: shiro <1.1.0
1 |
|
绕过方式:
1 | /./admin |
CVE-2016-6802
影响版本: shiro <1.3.2
Shiro 使用非根 servlet 上下文路径中存在安全漏洞。远程攻击者通过构造的请求, 利用此漏洞可绕过目标 servlet 过滤器并获取访问权限 https://www.cnblogs.com/backlion/p/14055279.html
1 | 比如http://127.0.0.1:8080/samples-web-1.2.4/account/index.jsp需要鉴权 |
CVE-2020-1957
影响版本: shiro <= 1.5.1 && springboot <= 2.3.0.RELEASE
绕过方式:
1 | /无鉴权路由/..;/鉴权路由 |
整体流程:
1.客户端请求URL: /demo/..;/admin/index
2.shrio 内部处理得到校验URL为 /demo/..
,校验通过
3.springboot 处理 /demo/..;/admin/index
, 请求 /admin/index
, 成功访问鉴权接口
CVE-2020-11989
影响版本: shiro < 1.5.3
1.ContextPath 使用 “;” 的绕过
应用不能部署在根目录,即需要 context-path,如server.servlet.context-path=/test 。如果为根目录则 context-path 为空,会被 CVE-2020-1957 的 patch 将 URL 格式化,若 Shiro 版本小于 1.5.2 的话该条件不需要
绕过方式:
1 | /;/test/admin/page |
当 URL 进入到 Tomcat 时, Tomcat 判断 /;test/admin/page 为 test 应用下的 /admin/page 路由,进入到 Shiro 时被 ; 截断被认作为 / ,再进入 Spring 时又被正确处理为 test 应用下的 /admin/page 路由,最后导致 Shiro 的权限绕过
2.二次url编码导致的绕过
利用条件是Shiroconfig的配置,权限ant风格的配置需要是*
而不是**
,同时controller需要接收的request参数(@PathVariable)的类型需要是String,否则将会出错
1 | bean.setLoginUrl("/login"); |
1 |
|
绕过方式:**%252F** or %25%32%66 ( / 的两次url编码)
1 | /hello/a%252Fa |
利用二次url编码,中间件收到我们的请求会先进行一次url解码,然后shiro会调用decodeRequestString
再次解码从而解析为/hello/a/a
字符,因为匹配规则是*
,并不会匹配到多个目录。而在进入到spring controller处理后,/hello/a%2fa是能匹配到路由/hello/{name}的
漏洞防护方案
- 通过WAF检测请求的uri开头是否为
/;
关键词 - 通过WAF检测请求的uri中是否包含
%25%32%66
关键词
CVE-2020-13933
影响版本: shiro < 1.6.0
和CVE-2020-11989利用条件类似,Shiro权限配置必须为 /xxxx/* ,同时controller需要接收的request参数(@PathVariable)的类型需要是String
1 | /admin/%3bxxx |
当输入为 http://127.0.0.1:8080/res/%3bpoc 时:
Shiro 逻辑 | 路由 | Spring 逻辑 | 路由 |
---|---|---|---|
输入 | /res/%3bpoc |
输入 | /res/%3bpoc |
URL 解码 | /res/;poc |
截断 ; |
/res/%3bpoc |
截断 ; |
/res/ |
URL 解码 | /res/;poc |
URL 标准化 | /res/ |
URL 标准化 | /res/;poc |
资源名 | 无资源名 | 资源名 | ;poc |
结果 | 不触发验证 | 结果 | 返回资源页面 |
当输入为 http://127.0.0.1:8080/res/;poc 时:
Shiro 逻辑 | 路由 | Spring 逻辑 | 路由 |
---|---|---|---|
输入 | /res/;poc |
输入 | /res/;poc |
URL 解码 | /res/;poc |
截断 ; |
/res/ |
截断 ; |
/res/ |
URL 解码 | /res/ |
URL 标准化 | /res/ |
URL 标准化 | /res/ |
资源名 | 无资源名 | 资源名 | 无资源名 |
结果 | 不触发验证 | 结果 | 无资源返回 |
当输入为 http://127.0.0.1:8080/res/poc 时:
Shiro 逻辑 | 路由 | Spring 逻辑 | 路由 |
---|---|---|---|
输入 | /res/poc |
输入 | /res/poc |
URL 解码 | /res/poc |
截断 ; |
/res/poc |
截断 ; |
/res/poc |
URL 解码 | /res/poc |
URL 标准化 | /res/poc |
URL 标准化 | /res/poc |
资源名 | poc |
资源名 | poc |
结果 | 302 跳转身份认证 | 结果 | 返回资源页面 |
当输入为 http://127.0.0.1:8080/res/ 时:
Shiro 逻辑 | 路由 | Spring 逻辑 | 路由 |
---|---|---|---|
输入 | /res/ |
输入 | /res/ |
URL 解码 | /res/ |
截断 ; |
/res/ |
截断 ; |
/res/ |
URL 解码 | /res/ |
URL 标准化 | /res/ |
URL 标准化 | /res/ |
资源名 | 无资源名 | 资源名 | 无资源名 |
结果 | 不触发验证 | 结果 | 无资源返回 |
URL 的处理:
- URL 进入到 Shiro 时,因为先做 URL 编码
%3b => ;
,使得路由中的资源名被截断删除 - Shiro 误以为请求路由中无资源名,故该次请求不触发身份认证逻辑
- 因不需要身份认证,请求路由被直接转发到 Spring
- Spring 从请求路由中正确截取资源名称,并返回资源页面
- 最终导致 Shiro 身份认证被绕过
造成这种情况的根本原因在于 Shiro 和 Spring 在 URL截断 和 URL解码 的操作顺序不一致
CVE-2020-17510
影响版本: Shiro <1.7.1
绕过方式:
1 | /admin/%2e |
当Shiro获得的uri为/hello时,是无法和/hello/*匹配的,所以就在/hello后面加上%2e,这样Shiro解码之后变成/hello/. 然后路径标准化成为/hello,绕过身份验证
CVE-2020-17523
影响版本: Shiro <1.7.1
1 | /admin/%20 |
调用tokenizeToStringArray
方法时,trimTokens
参数默认为true,空格会经过trim()
处理,因此导致空格被清除。再次返回getChain
时最后一个/
被删除,所以/admin
与/admin/*
匹配失败,导致鉴权绕过。而Spring接受到的访问路径为/admin/%20
,按照正常逻辑返回响应,因此导致权限被绕过
1.7.1版本后在新版本中trimTokens
参数为false,不会调用trim()
方法进行处理
1 | /admin/. |
在shiro中的normalize
方法中会变成/admin/./
再变成/admin/
,/admin/ 与 /admin/* 不匹配,绕过了shiro鉴权
Spring收到的请求为/admin/.
,如果SpringBoot开启全路径匹配的话,会匹配整个url,因此Spring返回200。如果没有开启全路径匹配的话,在Spring中.
和/
是作为路径分隔符的,不参与路径匹配。因此会匹配不到mapping,返回404
CVE-2021-41303
影响版本: Shiro < 1.8.0
需要特定配置,在真实环境能见到的可能性不大
1 |
|
绕过方式: 路径末尾加 /
1 | /admin/page/ |
CVE-2022-32532
影响版本: Shiro < 1.9.1
在正则表达式中元字符 . 是匹配除换行符(\n 、\r)之外的任何单个字符。要匹配包括\n在内的任何字符,需使用类似(.|n)的模式
新增Pattern.DOTALL模式后,正则表达式 . 就可以匹配任何字符包括换行符
在shiro-core-1.9.0.jar中存在一个RegExPatternMatcher类,提供请求路径匹配功能及拦截器参数解析的功能。pattern存在带.的正则表达式匹配,若source中存在\r或\n字符时,将判断错误
绕过方式: 使用%0a进行权限绕过
1 | /permit/a%0any |
CVE-2023-22602
影响版本: Shiro < 1.11.0
1.11.0 及之前版本的 Shiro 只兼容 Spring 的ant-style路径匹配模式,2.6 及之后版本的 Spring Boot 将 Spring MVC 处理请求的路径匹配模式从AntPathMatcher更改为了PathPatternParser。因此当 1.11.0 及之前版本的 Apache Shiro 和 2.6 及之后版本的 Spring Boot 使用不同的路径匹配模式时,攻击者访问可绕过 Shiro 的身份验证
根据官方文档描述,PathPattern跟AntPathMatcher匹配规则区别不大,PathPattern在保持其匹配规则的基础上,新增了 {*spring}
的语法支持,表示匹配余下的path路径部分并将其赋值给名为spring的变量(变量名可以根据实际情况随意命名,与@PathVariable
名称对应即可),同时, {*spring}
是可以匹配剩余所有path的,类似 /**
,只是功能更强,可以获取到这部分动态匹配到的内容
1 |
|
1 | @GetMapping("/admin/page") |
绕过方式:
1 | /admin/.. |
CVE-2023-34478
影响版本: Shiro < 1.12.0
Jersey在整个解析过程中并没有对路径穿越符../进行额外的处理。有点类似Spring高版本PathPattern
绕过方式:
1 | /admin/.. |
CVE-2023-46749
影响版本: Shiro < 1.13.0
1.13.0 或 2.0.0-alpha-4 之前的 Apache Shiro 可能容易受到路径遍历攻击,与路径重写一起使用时会导致身份验证绕过。当blockSemicolon配置关闭时(默认开启),攻击者可能构造包含恶意字符(如:/..;)的URL参数绕过身份验证,读取系统敏感文件信息。
绕过方式: 暂未公开
fuzz
1 | api/flag |
这些变异可以组合使用,从而形成更加丰富的 URI 结果
实战利用:
搜寻同样类型站点,收集后台各种接口,利用Shiro权限绕过漏洞,将这些接口拼接在目标站点,达到未授权访问的目的
参考文章:
https://forum.butian.net/share/2214
https://cloud.tencent.com/developer/article/2345086?areaId=106001
https://tttang.com/archive/1592/