0x00 前言
最近代表某不知名学校参加了一场比赛,结果比完之后要我们去自费核酸检测,还不报销,天使学校,给cheater发奖学金,都不肯报销核酸检测的60块钱。因为有比赛,所以就有一周的时间没有学习了。本篇包含以下元素:
- Shiro-682 (<=1.4.9的未授权访问漏洞,在1.5.0被修复,但仍可被其他方法绕过)
- CVE-2020-1957 (<=1.5.1的未授权访问漏洞,在1.5.2被修复)
0x01 Shiro-682
漏洞简介
在Spring中,形如/admin/show
和/admin/show/
的URL都能够成功访问admin页面。但由于Shiro拦截器的通配符问题,会导致虽然用通配符设定了访问admin
下所有页面都需要先登录,却会因通配符只能够匹配到admin/show
却无法匹配到admin/show/
而存在绕过并且也能成功访问相关页面。
Shiro拦截器
在Shiro中,我们可以通过在Shiro.ini中对URL进行拦截器设置,不同的拦截器有不同的效果,其中一种常见的拦截器是:authc。当对一个路径设置了此种拦截器后,当访问时,便需要先登录才能够访问。如:
[urls]
/admin/show = authc
在进行完上面的设置之后,当我们访问/admin/show
时便需要先登录才能够访问。但如果我们需要让admin
下所有目录都需要先登录时,是不是只能一个个页面进行配置呢?并不是,在Shiro中为我们准备了通配符,如下:
? : 匹配一个字符,如 /user? , 匹配 /user3,但不匹配/user/;
\* : 匹配零个或多个字符串,如 /add* ,匹配 /addtest
** : 匹配路径中的零个或多个路径,如 /user/** 将匹配 /user/xxx 或 /user/xxx/yyy
所以我们似乎可以通过如下设置来让admin
下所有页面都需要认证:
[urls]
/admin/* = authc
漏洞详情
上面的设置看似是可行的,但值得注意的是*
通配符只支持字符而不支持路径,当我们输入/admin/show/
的时候并不会被成功匹配,所以也就不会进行拦截,进而便实现了拦截器的绕过而不会跳转到登录,而在Spring中,admin/show
和admin/show/
都是可以正常访问show
页面的,这就造成了未授权的访问。
漏洞修复
在1.5.0版本中,合并了国内开发者的PR,对此漏洞进行了修复(当然下面的代码还有点Bug,后来在1.5.1又合并了PR一次)。我们可以在Github中找到:[SHIRO-682]
#web/src/main/java/org/apache/shiro/web/filter/PathMatchingFilter.java
private static final String DEFAULT_PATH_SEPARATOR = "/";
//...
String requestURI = getPathWithinApplication(request);
if (requestURI != null && requestURI.endsWith(DEFAULT_PATH_SEPARATOR)) {
requestURI = requestURI.substring(0, requestURI.length() - 1);
}
if (path != null && path.endsWith(DEFAULT_PATH_SEPARATOR)) {
path = path.substring(0, path.length() - 1);
}
//...
可以看到,当你用xxx/
访问时,会进行截取,变成xxx
,然后再进行匹配。
0x02 CVE-2020-1957
漏洞简介
虽然避免了通配符的匹配问题,但是Shiro的绕过还存在另外一种方式。
我们知道在浏览器的搜索栏使用http://xxx.com/admin/foobar/../show
或者http://xxx.com/foobar/../admin/show
时,浏览器会帮我们自动变成http://admin/show
。而在Spring中也是如此,使用admin/foobar/../show
依然可以访问到admin/show
。由于Shiro<=1.5.1版本中拦截过程中对URI的获取会进行清洗,而导致了带有分号的URL会被截断一部分,如:/foobar;/../admin/show
会被截断为/foobar
然后进行拦截匹配,这样便绕过了过滤器的设置,而因为/foobar;/../admin/show
也能成功访问,从而造成了未授权访问。
漏洞详情
注意到我们上一个漏洞,有这么一行代码: String requestURI = getPathWithinApplication(request);
,跟进这个函数,代码如下:
protected String getPathWithinApplication(ServletRequest request) {
return WebUtils.getPathWithinApplication(WebUtils.toHttp(request));
}
继续跟进:
public static String getPathWithinApplication(HttpServletRequest request) {
String contextPath = getContextPath(request);
String requestUri = getRequestUri(request);
if (StringUtils.startsWithIgnoreCase(requestUri, contextPath)) {
// Normal case: URI contains context path.
String path = requestUri.substring(contextPath.length());
return (StringUtils.hasText(path) ? path : "/");
} else {
// Special case: rather unusual.
return requestUri;
}
}
跟进getRequestUri:
public static String getRequestUri(HttpServletRequest request) {
String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE);
if (uri == null) {
uri = request.getRequestURI();
}
return normalize(decodeAndCleanUriString(request, uri));
}
最后跟进到decodeAndCleanUriString:
private static String decodeAndCleanUriString(HttpServletRequest request, String uri) {
uri = decodeRequestString(request, uri);
int semicolonIndex = uri.indexOf(';');
return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri);
}
发现会对有无分号;
进行判断,如果有,则只会截取分号前的部分。这就导致了当我们输入形如/foo;/../admin/show
的URL只截取到/foo..
,这个URL不符合我们拦截器中的设定,所以便绕过了拦截器,但是/foo;/admin/show
又能够正常访问到/admin/show
,所以产生了未授权访问。
漏洞修复
在1.5.2版本中,对此漏洞进行了修复,原来的 uri = request.getRequestURI();
直接获取请求URL,改为了如下:
if (uri == null) {
uri = valueOrEmpty(request.getContextPath()) + "/" +
valueOrEmpty(request.getServletPath()) +
valueOrEmpty(request.getPathInfo());
}
getContexPath返回的是Web应用所在的目录;getServletPath返回的是web.xml中url-pattern完全匹配的部分,如url-pattern=/admin/*,当访问/admin/show
时,返回的/admin
,getPathInfo则是返回url-pattern中通配符匹配到的那一部分,如/show
。
所以当我们访问http://xxx.com/foobar;/../admin/show
时,得到的路径会为://admin/show
, 再经过normalize进行标准化处理之后,就会变成/admin/show
,于是便不能绕过拦截器。