Apache Shiro 权限绕过(CVE-2020-13933)
漏洞影响范围
Apache Shiro 版本 < 1.6.0
漏洞环境搭建
下载官方直接写好的源码github-apache-Shiro:
<!--more-->
由于漏洞版本为 Apache shiro < 1.6.0 所以我们直接下载tag
中的1.5.3
版本即可。
下载好对应源码之后,解压导入idea
项目中,idea
会为我们安装好pom.xml
中的依赖(该过程有点长,请耐心等待),安装好后,选择WEBAPP
启动,(这里会访问外网资源,由于众所周知的原因访问比较慢,可以选择断网调试,或者开飞机)
看到该页面,就说明测试环境已经搭建完成了:
环境改造
从漏洞报告来看,应该是Shiro
对动态URL
的解析出现了问题,导致了未授权访问的情况,因此为了方便查看,我们新添加一个Controller
,作为测试页面。
TestController.java:
package org.apache.shiro.samples;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class TestController {
@RequestMapping(value = "/test/{id}",method = RequestMethod.GET)
public String test(@PathVariable String id, Model model){
model.addAttribute("id",id);
return "test";
}
}
test.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vuln Test Page</title>
</head>
<body>
<h1 th:text="'Auth Page Test:'+${id}"></h1>
</body>
</html>
当然别忘了将这个路由加入FilterChain
:
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/login.html", "authc"); // need to accept POSTs from the login form
chainDefinition.addPathDefinition("/test/*","authc");
// 新添加的需要验证的路由,注意这里只使用了一个*号
chainDefinition.addPathDefinition("/logout", "logout");
return chainDefinition;
}
项目结构如下:
最后效果只要如下图就算可以开始复现了,当未登录时,自动跳转登录界面:
登录后可以正常访问:
PS:有关Shiro-FilterChain是什么
如果简单阅读这个Shiro
的官方例子,你会发现此处登录页面提交参数之后,完全没有在Controller
处进行UsernamePasswordToken
函数进行验证,那么自然会产生参数是在哪里处理这个疑问。由于使用的是Springboot
,很自然就明白应该是在filter
处进行了参数处理,我们可以将FIlterChain
简单理解成为Filter
的列表,它告诉应用在什么时候,该调用哪个FIlter
。Shiro
会对Servlet
容器进行代理,先执行自己的FIlterChain
,再执行Servlet
的FIlterChain
,下面这些代码就是在添加Shiro
:
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/login.html", "authc"); // need to accept POSTs from the login form
chainDefinition.addPathDefinition("/test/*","authc"); // 新添加的需要验证的路由,一个星号表示弱匹配
chainDefinition.addPathDefinition("/logout", "logout");
return chainDefinition;
}
如果跟进addPathDefinition
这个函数,可以发现最后是返回一个map
类型的参数
实质上和网上目前的复现方式使用map.put
设置路由是差不多的。
Shiro
有自己的默认FilterChain
,也可以自己实现,默认FilterChain
及其对应功能如下图(抄的网上的表):
漏洞复现
其实漏洞复现很简单,只需要对;
,进行url编码即可,如下图:
http://<Your-server>/test/%3B1<Any String>/
需要注意的是,如果我们此处路由为强匹配,则不会出现权限绕过的情况:
具体为什么,见后文强匹配与弱匹配的区别
漏洞分析
漏洞成因
Shiro处理
此处漏洞成因和Shiro
爆出的CVE-2020-11989类似,是由于Shiro
和Spring
对URL解析的差异造成的,调试过程中我们顺便梳理一下匹配的流程。根据前文所提到的FilterChain
,我们能够找到Shiro
对路径匹配的FIlter
。
Shiro
的处理URL入口在这里/org/apache/shiro/shiro-web/1.5.3/shiro-web-1.5.3.jar!/org/apache/shiro/web/util/WebUtils.class
:
后面改用windows环境调试了,所以看着主题不一样2333
这里注释也写的很清楚,该函数会返回web应用程序中当前访问的路径
,自然路径解析是从这里开始的,跟一下,主要注意的是,我们发现这里已经进行过一次URL编码了
removeSemicolon
会去除掉;
这个字符
最后结果是,将;
号后面的内容去除:
而经过处理,最后参与匹配的字符串为:
然后shiro
仅仅只会和源码中写明了FilterChain
的路由进行匹配,毕竟shiro
只是进行鉴权工作。
其他不用shiro
参与的处理,需要调用Spring
自己Filter
。
在此处Shiro
完成了自己的任务,将移交给spring的Filter
(注意函数栈出现了spring
)
因此最后Shiro
得到的路由为/test
,这个路由在我们源码中不存在,会移交给error
处理路由,返回404
,这个是任何用户有权访问的。
也因为如此shiro
判断可以放权访问。
Spring的处理
一路疯狂F7
我们发现SpringBoot
的处理URL
处在这:
继续跟发现会在此处进行URL
字符处理:
随后根据得到的URL
,遍历所有Controller
寻找匹配
梳理了一下,共五次匹配操作:
- First Match
这里第一次匹配与Accountinfo
进行比较,查看是不是该路由。
- Second Match
第二次匹配与/
比较,看看是不是该路由。
- Third Match
第三次匹配与login.html
比较,看是不是该路由。
- Fourth Match
第四次匹配与error
路由匹配,看是不是该路由。
- Fifth Match
第五次匹配和 GET/test/{id}
,发现匹配上了,直接返回结果。
细心的同学会发现这不是和我们Controller
定义的顺序一模一样吗?
我们在第五次匹配处一路F8
看看。发现最后第五次匹配成功后会返回对应的路由:
从这里可知最后spring
得到的路由为/test/;Bypass
,而之前Shiro
已经放权完成,因此我们可以正常访问该路由。
不同的处理方式
Shiro
也存在和Spring
处理URL的同名函数decodeAndCleanUriString
,函数体如下:
这里Shiro
的处理是先进行url
解码,然后再对;
进行处理
Spring
函数体如下:
Spring
则完全相反,首先进行了;
号的处理,然后再进行URL
解码。
像往常一样,一张图进行总结:
强匹配与弱匹配的区别
从漏洞的整体流程来看,其实我们可以把关注点放在shiro
的AntPath
处理引擎上,正是这一步的判断导致了越权
代码位置org.apache.shiro.util.AntPathMatcher#doMatch
- 弱匹配的情况
函数体如下,我将常量进行了替换,看着方便一点:
protected boolean doMatch(String pattern, String path, boolean fullMatch) {
if (path.startsWith("/") != pattern.startsWith("/")) {
return false;
}
String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, "/");
String[] pathDirs = StringUtils.tokenizeToStringArray(path, "/");
//通过"/"符号划分数组
int pattIdxStart = 0;
int pattIdxEnd = pattDirs.length - 1;
int pathIdxStart = 0;
int pathIdxEnd = pathDirs.length - 1;
// Match all elements up to the first **
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
String patDir = pattDirs[pattIdxStart];
//patDir = "test"
if ("**".equals(patDir)) {
break;
}
if (!matchStrings(patDir, pathDirs[pathIdxStart])) {
return false;
}
//matchStrings(patDir, pathDirs[pathIdxStart] = true
pattIdxStart++; // pattIdxStart=1
pathIdxStart++;// pathIdxStart=1
}
if (pathIdxStart > pathIdxEnd) {
// 1 > 0 走这个分支
// Path is exhausted, only match if rest of pattern is * or **'s
if (pattIdxStart > pattIdxEnd) {
// 1 > 1 False 不走该分支
return (pattern.endsWith("/") ?
path.endsWith("/") : !path.endsWith("/"));
}
if (!fullMatch) {
// 初始传参fullMatch = true 不走该分支
return true;
}
if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") &&
path.endsWith("/")) {
// false
return true;
}
//走下面这个循环
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
// pattDirs里的元素均不满足
return false; // <-函数将在这里退出
}
}
return true;
.....
}
/*
* 因此通过该函数没有匹配成功,会交给error或者shiro自带的错误处理器处理(错误为404没有匹配,即找不到该路由)
* 一般也不会有给错误页面设置权限的需求,所以该请求可放行
*/
- 强匹配的情况
前面匹配的流程和弱匹配一模一样除了这一步:
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
// pattDirs里的1号元素满足 => 该判断返回!true 跳出循环
return false;
}
}
return true;<-函数将在这里退出
这里由于能够匹配到**
,所以退出该循环,即匹配成功,这时候Shiro
会判断该路由,是否有权限访问,这个路由我们是用auth
标记的,因此该用户无权访问,即不放行该请求,此处shiro
进行的操作是,将访问路由替换为登陆路由:
Shiro的修复方式
我们比较一下shiro-web-1.5.3.jar
和shiro-web1.6.0.jar
两个jar
包看看shiro
是怎么修复的,我们使用相同的payload
,进行访问发现1.6.0
返回了400
而非404
,
翻看shiro-web-1.6.0.jar
,发现WebUtil
和AntPathMather
没有变化,最终在filter
文件夹中找到了多了一个InvalidRequestFilter
该类注释为,从源码来看过滤是默认开启的,满足Secure By Default
原则:
很显然是为了修复权限绕过而做的了,我们看看源码是如何过滤的:
public class InvalidRequestFilter extends AccessControlFilter {
private static final List<String> SEMICOLON = Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));
private static final List<String> BACKSLASH = Collections.unmodifiableList(Arrays.asList("\\", "%5c", "%5C"));
//这里对分号和反斜杠的URL编码进行了标记
private boolean blockSemicolon = true;
private boolean blockBackslash = true;
private boolean blockNonAscii = true;
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
String uri = WebUtils.toHttp(request).getRequestURI();
return !containsSemicolon(uri)
&& !containsBackslash(uri)
&& !containsNonAsciiCharacters(uri);
}
//分别判断是分号,反斜杠,还是不可打印字符的情况,如果检测到了就由下面这个函数 返回
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
WebUtils.toHttp(response).sendError(400, "Invalid request");
return false;
}
//判断不可打印的字符采用了类似白名单的模式,个人感觉修复是比较彻底的。
private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
int length = uri.length();
for (int i = 0; i < length; i++) {
char c = uri.charAt(i);
if (c < '\u0020' || c > '\u007e') {
return false;
}
}
return true;
}
参考链接
Apache Shiro 权限绕过漏洞 CVE-2020-13933