Shiro权限验证绕过史

今年以来,shiro连续曝出多个身份验证绕过漏洞,在此梳理一下shiro身份验证绕过漏洞史。

shiro-security-reports

来自shiro安全通告

SHIRO介绍

很多人认识Shiro是因为其反序列化漏洞的广泛性和严重性。Shiro本身是一个常用的Java安全框架,用于执行身份验证、授权、密码和会话管理功能,有着易用、全面、灵活等特性,shiro被广泛使用。通常Shiro会和Spring等框架一起搭配用于Web应用系统的开发。

因为其本身就用于身份验证和权限控制,出现身份验证绕过的问题是比较严重的,CVSS给的评分就比较高

CVE-2020-1957 CVSS 3.x 评分 9.8,CVSS 2.0评分 7.5

CVE-2020-11989 CVSS 3.x 评分 9.8,CVSS 2.0评分 7.5

CVE-2020-13933 CVSS 目前还未评分,后续补上

Shiro是基于URI的权限认证,配置的url模式使用Ant风格模式。Ant路径通过通配符支持“?”、“*”、“**”。

对于“?”,其匹配一个字符串。如“/admin?”将匹配“admin1”,但不匹配“/admin”或“/admin/”。

对于“*”,其匹配零个或多个字符串。如“/admin/*”将匹配“/admin/”、“/admin/abc”,但不匹配“/admin/a/b”。

对于“**”,其匹配路径中的零个或多个路径。如“/admin/**”将匹配“/admin/a”或“/admin/a/b”。


常用的拦截器配置:

anon(anonymous)拦截器表示匿名访问(既不需要登录即可访问)。

authc(authentication)拦截器表示需要身份认证过后才能访问。

拦截器的匹配顺序采取第一次匹配优先的方式,即从头开始使用第一个匹配的url模式对应的拦截器链。


例如在配置中存在如下配置:

1
2
3
4
Map<String, String> map = new LinkedHashMap<>();
map.put("/doLogin", "anon");
map.put("/admin/*", "authc");
map.put("/manage/*", "authc");

/doLogin可以直接被访问,而/admin/*/manage/*需要进行身份认证。

SHIRO搭建

测试Demo:https://github.com/s31k31/shiro-simple-example

导入IDE,等待maven配置加载完成,点击运行/调试即可启动。

需测试不同版本的shiro时,在pom.xml文件中修改shiro版本即可。

src/main/resources/application.properties中配置的端口为8011,context path/shiro,有需要可以自行更改。

SHIRO-682

https://issues.apache.org/jira/browse/SHIRO-682

在2019年3月,中国开发者tomsun08在shiro项目中提出PR

在Spring web中/resource/menusresource/menus/都能访问到同一资源。

而shiro中的pathPattern只能匹配/resource/menus,而不能匹配 /resource/menus/

用户使用requestURI + "/"就能绕过权限控制。

但直到2019年11月,在shiro 1.5.0中修复这一问题。修改的代码在commit

tomsum08不是专门的安全研究人员,所以当时仅对URI最后的/做了处理。若URI最后为/,则去掉该/

code-compare-pathmatchingfilterchainresolver

这处改动的并非真正漏洞核心,漏洞本质是Spring处理URI和Shiro处理URI不一致导致的。

Spring处理URI和Shiro处理URI不一致性才是导致后续多个漏洞曝出的真正原因。


CVE-2020-1957

漏洞存在于1.5.2版本之前,复现时将pom.xml中的shiro版本设置为1.5.2版本之前,如下设置为1.5.1

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.5.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.1</version>
</dependency>

漏洞复现

访问:http://localhost:8011/shiro/admin/page ,此时跳转登录页面

burp-request-normal


访问:http://localhost:8011/shiro/xxxx/..;/admin/page ,成功绕过身份校验

burp-request-bypass

漏洞分析

shiro配置规则

1
2
3
4
Map<String, String> map = new LinkedHashMap<>();
map.put("/doLogin", "anon");
map.put("/admin/*", "authc");
map.put("/manage/**", "authc");

org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain是shiro判断输入的URI是否匹配拦截器的函数。匹配成功将返回相应的拦截器,进行对应的权限操作。

当我们传入/shiro/admin/page时(/shiro在此属于context path,不会被当作URI),会匹配到/admin/*这条规则,从而进行authc,也就是到org/apache/shiro/web/filter/authc/AuthenticatingFilter.java中去判断权限。

debug-getchain-pathpattern


但当我们传入/shiro/xxxx/..;/admin/page时,shiro获取到的URI是/xxxx/..,注意这里shiro移除了;后面的内容。

debug-getchain-requesturi


我们跟进getPathWithinApplication()中进一步查看,最终调用的是org.apache.shiro.web.util.WebUtils#getPathWithinApplication,可以看到requestUri是通过getRequestUri()方法获取得到的

debug-getpathwithinapplication

getRequestUri()代码如下,根据request.getRequestURI()获取的uri,传入decodeAndCleanUriString()方法。关于request.getRequestURI带来的安全问题可查看这篇文章

decodeAndCleanUriString()方法对;后面的内容进行删除,只获取;前的内容。所以我们传入的/xxxx/..;/admin/page,在shiro中得到的只是/xxxx/..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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));
}

...

private static String decodeAndCleanUriString(HttpServletRequest request, String uri) {
uri = decodeRequestString(request, uri);
int semicolonIndex = uri.indexOf(';');
return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri);
}

/xxxx/..没有匹配到shiro配置中的规则,默认放行。

但在spring web处理/shiro/xxxx/..;/admin/page时,Spring对..;/是包容的,会被当成../处理,所以最后访问的是/shiro/admin/page

当然/shiro/xxxx;/../admin/page同样能绕过身份验证

这就是由于shiro和spring对URL处理的不一致导致的第一个漏洞,后续的CVE-2020-11989和CVE-2020-13933属于同一类问题。

漏洞修复

漏洞修复代码在此commit,将request.getRequestURI()改成了request.getContextPath()+"/"+request.getServletPath()+request.getPathInfo()

code-compare-webutils


getServletPath得到的是实际Servlet路径,无法利用路径回溯../和分号;绕过。

在shiro1.5.2版本中,传入/shiro/xxxx/..;/admin/page,得到的结果是/shiro//admin/page

debug-getrequesturi


CVE-2020-11989

该漏洞由淚笑向 Apache Shiro 官方报告的漏洞

漏洞存在于1.5.3版本之前,复现时将pom.xml中的shiro版本设置为1.5.2版本

漏洞复现

在CVE-2020-1957中的漏洞修复中提到request.getServletPath只返回Servlet实际的路径,但是request.getContextPath是能获取到分号;的。

于是访问:http://localhost:8011/shiro;/admin/pagehttp://localhost:8011/;/shiro/admin/page ,实现绕过。

burp-request2-bypass

漏洞分析

当传入/shiro;/admin/page时,request.getContextPath得到的是/shiro;/request.getServletPath得到的是/admin/page

debug-getrequesturi2

decodeAndCleanUriString方法中的代码没有变动,还是删除;后面的内容进行,只获取;前的内容。

最后shiro得到的URI是/shiro;,不会匹配到身份认证校验规则,默认放行。

⚠️:该绕过方式一定需要有context path的配置,如果没有系统没有设置context path,request.getContextPath默认为空。


除此之外,腾讯玄武实验室的Ruilin发现另一个绕过方法

shiro配置的规则

1
map.put("/manage/*", "authc");

对应的Controller

1
2
3
4
@RequestMapping("/manage/{name}")
public String manageName(@PathVariable String name) {
return "manage: "+name;
}

http://localhost:8011/shiro/manage/test

burp-request3-normal


http://localhost:8011/shiro/manage/test%25%32%46test

burp-request3-bypass


原因是在decodeAndCleanUriString()方法中存在decodeRequestString用于URL解码。

1
2
3
4
5
private static String decodeAndCleanUriString(HttpServletRequest request, String uri) {
uri = decodeRequestString(request, uri);
int semicolonIndex = uri.indexOf(';');
return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri);
}

shiro二次解码得到的是/shiro/manage/test/test,因为鉴权规则设置的是/manage/*一个星号,只匹配一层目录,/shiro/manage/test/test,算是两层目录,也就不属于/manage/*。而Spring解析时只会将URI解码一次,得到的是/shiro/manage/test%2ftest,从而绕过访问。

该绕过的场景更严苛一些,可利用场景稍少。

漏洞修复

官方更改了URI的获取逻辑,使用移除分号后的request.getRequestURIrequest.getPathInfo()进行URI拼接。并且没有用decodeAndCleanUriString()方法处理URI,避免了二次URL解码。

code-compare-webutils2


CVE-2020-13933

该漏洞由蚂蚁非攻实验室codeplutos提交,具体可看这里。其中说到新增了Global Filter来缓解该漏洞。

漏洞存在于1.6.0版本之前,复现时将pom.xml中的shiro版本设置为1.5.3版本,该漏洞触发条件稍微苛刻一些,也是需要将请求映射成路径变量的形式。

漏洞复现

Controller对应代码:

1
2
3
4
@RequestMapping("/manage/{name}")
public String manageName(@PathVariable String name) {
return "manage: "+name;
}

访问:http://localhost:8011/shiro/manage/index ,302跳转到登录页面

burp-request4-normal

访问:http://localhost:8011/shiro/manage/%3Bindex ,绕过身份验证

burp-request4-bypass

漏洞分析

查看漏洞修复的commit,其中新增了一个全局过滤器,路径为web/src/main/java/org/apache/shiro/web/filter/InvalidRequestFilter.java,该过滤器对分号;、反斜杠\、和ascii不可打印字符的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private static final List<String> SEMICOLON = Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));
private static final List<String> BACKSLASH = Collections.unmodifiableList(Arrays.asList("\\", "%5c", "%5C"));
...
private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
...
if (c < '\u0020' || c > '\u007e') {
return false;
}
return 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;
}

WebUtils.toHttp(request).getRequestURI()获取到的URI存在分号;、反斜杠\、ascii不可打印字符时,抛出400错误。

当我们传入/manage/%3Bindex时,shiro得到的是/manage/

debug-evaluate

而spring得到的是/manage/;index;index作为{name}参数

debug-spring-getlookuppathforrequest

所以能绕过shiro的身份认证

漏洞修复

把shiro版本设置成1.6.0,再次访问,此时URI中存在分号;,返回400错误。

burp-request5-normal

参考

https://shiro.apache.org/security-reports.html

https://l3yx.github.io/2020/06/30/Shiro-权限绕过漏洞-CVE-2020-11989/

https://xlab.tencent.com/cn/2020/06/30/xlab-20-002/

https://lists.apache.org/thread.html/r539f87706094e79c5da0826030384373f0041068936912876856835f%40%3Cdev.shiro.apache.org%3E