Shiro-682 : Shiro < 1.5.0权限绕过

环境搭建

https://github.com/lenve/javaboy-code-samples/tree/master/shiro/shiro-basic

在LoginController里添加如下代码

1
2
3
4
@GetMapping("/admin/{currentPage}")
public String admin(@PathVariable Integer currentPage) {
return "hello admin";
}

2

在ShiroConfig里面设置Shiro拦截器,添加一行map.put("/admin/*", "authc");

1

漏洞复现

当访问admin下的内容时需要认证

3

再URL后面添加一个反斜杠成功绕过认证访问

4

漏洞分析

Shiro解析部分

Shiro对URL的获取以及匹配拦截器对应的方法如下:

5

接下来进入while循环判断

6

这里的requestURL即为请求时的URL,即/admin/1,pathPattern为/doLogin,将这两个参数代入pathMatches方法,跟进

7

8

9

跟进doMatch,代码量有点多,直接看到关键代码部分

10

这里return的意思是判断pattern是否是以/结尾,如果为True则返回path是否以/结尾的值,如果为False则返回path是否以/结尾的值的相反值。这里pattern值为/admin/*不是以/结尾,所以返回path是否以/结尾的值的相反值,path值为/admin/1/`所以返回False。

于是回到之前的do-while循环那儿

11

因为返回False,所以while里面的值为True,于是就能通过验证进入到do里面

这里我们访问通过使用/admin/1来看看二者区别,直接看到doMatch里面的关键代码

12

pattern是我们写定了的不允许访问的路径所以不变,唯一变的是path少了一个\,也就是我们只需要看!path.endsWith(this.patheparator)的值如何变化,现在path值为/admin/1不以\结尾,所以为False,又因为是相反值所以返回True,在while循环那儿就就为False跳出循环。

13

也就是被过滤器检测到

Spring解析部分

从DispatcherServlet#getHandler开始,从var2中取出一个值作为mapping

14

接着调用mapping.getHandler,第一次这里得到的handler为空,就不去跟进了,给出第一次调用流程

1
2
3
4
5
DispatcherServlet#getHandler
AbstractUrlHandlerMapping#getHandler
AbstractUrlHandlerMapping#getHandlerInternal
UrlPathHelper#getLookPathForRequest
UrlPathHelper#getPathWithinApplication

第二次回到while循环,此时mapping值为RequestMappingHandlerMapping

跟进AbstractHandlerMapping#getHandler

15

跟进AbstractHandlerMethodMapping#getHandlerInternal

16

跟进AbstractHandlerMethodMapping#lookupHandlerMethod

17

跟进AbstractHandlerMethodMapping#addMatchingMappings

18

跟进RequestMappingInfoHandlerMapping#getMatchingMapping

19

跟进RequestMappingInfo#getMatchingCondition

20

跟进PatternsRequestCondition#getMatchingCondition

21

跟进PatternsRequestCondition#getMatchingPatterns

22

跟进PatternsRequestCondition#getMatchingPattern

23

关键点就在这里,三个条件都为True,所以返回pattern+/,即/admin/1/

可以看出,漏洞的成因在于Shiro与Spring解析的差异性,Shiro中的/admin/*不能匹配到/admin/1/,而Spring控制器/admin/1能够被/admin/1/调用,所以就导致了该漏洞

漏洞修复

去除了requestURI和pathPattern末尾的斜杠

38

CVE-2020-1957 : Shiro < 1.5.2权限绕过

漏洞复现

24

漏洞分析

Shiro部分

还是一样定位到getChain方法

25

跟进PathMatchingFilterChainResolver#getPathWithinApplication

26

跟进WebUtils#getPathWithinApplication

27

跟进WebUtils#getRequestUri

28

这里获取到了我们访问的uri,跟进WebUtils#ecodeAndCleanUriString

29

这里的semicolonIndex为0,字符串截取从0到0,即为空,回到WebUtils#getRequestUri,跟进WebUtils#normalize

30

继续跟进

31

因为这里的normalized就是前面path的值,也就是为空,这里加上/,所以normalized现在值为/

32

这里返回值为/,接着就和上一个洞一样进入while循环遍历pattern来匹配,没有匹配到/是被过滤了的,最后因为pattern已经遍历完成功绕过Shiro的检测

33

Spring部分

还是一样从DispatcherServlet#getHandler并且经过一次循环后开始

34

一路跟到UrlPathHelper#getRequestUri

35

跟进UrlPathHelper#decodeAndCleanUriString

36

这里的removeSemicolonContent方法会反斜杠为分割,将分号后的内容进行删除

1
2
;/admin/1 => /admin/1
;aaa/admin/1 => /admin/1

所以返回的是/admin/1

37

这样也就能正确匹配路由了,就导致了权限绕过

漏洞修复

将1.5.2的代码与当前版本进行diff,可以发现对应的patch如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static String getRequestUri(HttpServletRequest request) {
String uri =
(String)request.getAttribute("javax.servlet.include.request_uri");
if (uri == null) {
uri = valueOrEmpty(request.getContextPath()) + "/" +
valueOrEmpty(request.getServletPath()) + valueOrEmpty(request.getPathInfo());
}
return normalize(decodeAndCleanUriString(request, uri));
}
private static String valueOrEmpty(String input) {
return input == null ? "" : input;
}

它将 HttpServletRequest#getRequestUri 获取路由修改为了通过
ContextPath 、 ServletPath 、 PathInfo 三者拼接的方式获取路由,由于 ServletPath 能够正确的处理分号,通过这种方式来获取对应的路由能够成功修复此漏洞。

CVE-2020-13933 : Shiro<1.6.0权限绕过

漏洞复现

39

漏洞分析

Shiro部分

Shiro对URL部分的处理:

1
2
3
4
public static String getPathWithinApplication(HttpServletRequest request) {
return normalize(removeSemicolon(getServletPath(request) +
getPathInfo(request)));
}

Shiro使用下面两者拼接的方式获取URL:

1
getServletPath(request) + getPathInfo(request)

getServletPath默认会将URI进行Urldecode,如下:

40

获取到URL后会使用removeSemicolon方法进行去除分号处理

1
2
3
4
private static String removeSemicolon(String uri) {
int semicolonIndex = uri.indexOf(59);
return semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri;
}

返回值为 /admin/,获取到URI后,会判断URI末尾是否包含 / :

1
2
3
4
if (requestURI != null && !"/".equals(requestURI) && requestURI.endsWith("/"))
{
requestURI = requestURI.substring(0, requestURI.length() - 1);
}

在这里会将末尾的斜杠去掉,得到/admin,这样就不能和pattern(/admin/*)匹配成功,所以就绕过了Shiro的校验。

41

Spring部分

Spring通过 getPathWithinServletMapping 方法获取路由:

1
2
3
4
public String getPathWithinServletMapping(HttpServletRequest request) {
String pathWithinApp = this.getPathWithinApplication(request);
String servletPath = this.getServletPath(request);
String sanitizedPathWithinApp = this.getSanitizedPath(pathWithinApp);

在 getPathWithinApplication 方法中会使用 getRequestUri 来获取对应路由:

1
2
3
4
5
6
7
8
public String getRequestUri(HttpServletRequest request) {
String uri =
(String)request.getAttribute("javax.servlet.include.request_uri");
if (uri == null) {
uri = request.getRequestURI();
}
return this.decodeAndCleanUriString(request, uri);
}

在这里获取到的URI是没经过decode的URL,随后会进入到 decodeAndCleanUriString方法格式化URL:

1
2
3
4
5
6
7
 private String decodeAndCleanUriString(HttpServletRequest request, String uri)
{
uri = this.removeSemicolonContent(uri);
uri = this.decodeRequestString(request, uri);
uri = this.getSanitizedPath(uri);
return uri;
}

在该方法中,首先会调用 removeSemicolonContent 对分号进行截断,此时因为分号是urlencode后 的,所以没有被匹配到,URI无任何变化。在 decodeRequestString 方法中,该方法会对URI进行 UrlDecode,所以最终获取到的路由为 /admin/;page ,该路由能够匹配上 /admin/{name} ,所以能够正常返回⻚面。

漏洞修复

增加了一个filter

1
2
3
4
private static final List<String> SEMICOLON =
Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));
private static final List<String> BACKSLASH =
Collections.unmodifiableList(Arrays.asList("\\", "%5c", "%5C"));

该filter会对URL中的分号进行拦截。