12

shiro权限绕过漏洞分析(cve-2020-1957)

 3 years ago
source link: https://blog.spoock.com/2020/05/09/cve-2020-1957/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

shiro权限绕过漏洞分析(cve-2020-1957)

2020-05-09

根据 Spring Boot 整合 Shiro ,两种方式全总结!。我配置的权限如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean
ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager());
bean.setLoginUrl("/login");
bean.setSuccessUrl("/index");
bean.setUnauthorizedUrl("/unauthorizedurl");
Map<String, String> map = new LinkedHashMap<>();
map.put("/admin/**", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}

........
@RequestMapping("/admin/index")
public String test() {
return "This is admin index page";
}

会对admin所有的页面都会进行权限校验。测试结果如下:
访问index

1.png

访问admin/index

2.png

在shiro的1.5.1及其之前的版本都可以完美地绕过权限检验,如下所示;

3.png

绕过原理分析

我们需要分析我们请求的URL在整个项目的传入传递过程。在使用了shiro的项目中,是我们请求的URL(URL1),进过shiro权限检验(URL2), 最后到springboot项目找到路由来处理(URL3)

漏洞的出现就在URL1,URL2和URL3 有可能不是同一个URL,这就导致我们能绕过shiro的校验,直接访问后端需要首选的URL。本例中的漏洞就是因为这个原因产生的。

http://localhost:8080/xxxx/..;/admin/index 为例,一步步分析整个流程中的请求过程。

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
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;
}
}


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));
}
4.png

此时的URL还是我们传入的原始URL: /xxxx/..;/admin/index

接着,程序会进入到decodeAndCleanUriString(), 得到:

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);
}
5.png

decodeAndCleanUriString 以 ;截断后面的请求,所以此时返回的就是 /xxxx/...然后程序调用normalize() 对decodeAndCleanUriString()处理得到的路径进行标准化处理. 标准话的处理包括:

  • 替换反斜线
  • 替换 ///
  • 替换 /.//
  • 替换 /..//

都是一些很常见的标准化方法.

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
private static String normalize(String path, boolean replaceBackSlash) {

if (path == null)
return null;

// Create a place for the normalized path
String normalized = path;

if (replaceBackSlash && normalized.indexOf('\\') >= 0)
normalized = normalized.replace('\\', '/');

if (normalized.equals("/."))
return "/";

// Add a leading "/" if necessary
if (!normalized.startsWith("/"))
normalized = "/" + normalized;

// Resolve occurrences of "//" in the normalized path
while (true) {
int index = normalized.indexOf("//");
if (index < 0)
break;
normalized = normalized.substring(0, index) +
normalized.substring(index + 1);
}

// Resolve occurrences of "/./" in the normalized path
while (true) {
int index = normalized.indexOf("/./");
if (index < 0)
break;
normalized = normalized.substring(0, index) +
normalized.substring(index + 2);
}

// Resolve occurrences of "/../" in the normalized path
while (true) {
int index = normalized.indexOf("/../");
if (index < 0)
break;
if (index == 0)
return (null); // Trying to go outside our context
int index2 = normalized.lastIndexOf('/', index - 1);
normalized = normalized.substring(0, index2) +
normalized.substring(index + 3);
}

// Return the normalized path that we have completed
return (normalized);

}

经过getPathWithinApplication()函数的处理,最终shiro 需要校验的URL 就是 /xxxx/... 最终会进入到 org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver 中的 getChain()方法会URL校验. 关键的校验方法如下:

6.png

由于 /xxxx/.. 并不会匹配到 /admin/**, 所以shiro权限校验就会通过.

最终我们的原始请求 /xxxx/..;/admin/index 就会进入到 springboot中. springboot对于每一个进入的request请求也会有自己的处理方式,找到自己所对应的mapping. 具体的匹配方式是在:org.springframework.web.util.UrlPathHelper 中的 getPathWithinServletMapping()

7.png

getPathWithinServletMapping() 在一般情况下返回的就是 servletPath, 所以本例中返回的就是 /admin/index.最终到了/admin/index 对应的requestMapping, 如此就成功地访问了后台请求.

最后,我们来数理一下整个请求过程:

  1. 客户端请求URL: /xxxx/..;/admin/index
  2. shrio 内部处理得到校验URL为 /xxxx/..,校验通过
  3. springboot 处理 /xxxx/..;/admin/index , 最终请求 /admin/index, 成功访问了后台请求.

commmit分析

对应与修复的commit是: Add tests for WebUtils

其中关键的修复代码如下;

8.png

对比与1.5.1的版本获取request.getRequestURI(), 在此基础上,对其进行标准化,分析, 由于 getRequestURI是直接返回请求URL,导致了可以被绕过.

在1.5.2的版本中是由contextPath()+ servletPath()+ pathinfo()组合而成. 以 /xxxx/..;/admin/index为例, ,修正后的URL是:

9.png
10.png

经过修改后.shiro处理的URL就是 /admin/index, 发现需要进行权限校验,因此不就会放行.

11.png

偶然发现 这样也可以绕过shiro的权限校验, 但是这种情况和上面的情况是不一样的. 上面的情况是shiro校验的URL和最终进入到springboot中需要处理的URL是不一样的.

增加一个路由

1
2
3
4
@RequestMapping("/admin")
public String test2() {
return "This is the default admi controller";
}
12.png

在这种情况下,可以访问到/admin这样的路由. 但仅此而已, 并不访问访问更多/admin下方更多的路由. 接下来分析这种原因.按照前面的一贯分析, 我们同样可以知道 在 org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver() 中的getChain()是可以通过检验的. 因为 /admin.index 不属于/admin/**

13.png

在springboot中需要通过request找到对应的handler进行处理. springboot是在 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping 这个函数中,通过 lookupPath找到对应的handler.

14.png

通过上述的截图也可以看出, springboot获取的也是 /admin.index 这个URL. 但是可以成功地找到handler来处理.所以本质上 这个 /admin.index路由可以绕过 shiro 是springboot内部通过URL找到handler的一个机制.与shiro并没有关系. 我们进行一个简单的测试:

1
2
3
4
@RequestMapping("/index")
public String index() {
return "This is homepage";
}
15.png

完全没有使用shiro, 大家也可以测试下.所以这个问题其实在shiro 1.5.2 上面也同样是可以的.

上面的测试只是一种最简单的情况, 只有shiro配置了一个全局的权限校验, 就有可能存在绕过的问题, 如果程序进一步在URL上面配置了权限校验,即使绕过了ShiroFilterChainDefinition, 但是还是无法绕过注解上面的防御.如下所示:

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
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
//哪些请求可以匿名访问
chain.addPathDefinition("/user/login", "anon");
chain.addPathDefinition("/page/401", "anon");
chain.addPathDefinition("/page/403", "anon");
chain.addPathDefinition("/t5/hello", "anon");
chain.addPathDefinition("/t5/guest", "anon");

//除了以上的请求外,其它请求都需要登录
chain.addPathDefinition("/**", "authc");
return chain;
}


@RestController
@RequestMapping("/t5")
public class Test5Controller {
@RequiresUser
@GetMapping("/user")
public String user() {
return "@RequiresUser";
}
}

16.png

17.png

讲到这里,差不多有关这个漏洞的所有问题都说完了.其实本文章还涉及到一些其他的知识.比如:

  1. requesturi 和 servlet的区别
  2. springmvc的请求处理流程

这些都可以写一篇文章来进行说明了.整体来说,这个漏的利用方式还是很简单的,我测试了目前大部分使用shiro的应用基本上都存在绕过的问题, 但是这个漏洞能够找成多大的危害呢? 就目前看来危害还是有限的,因为即使绕过了shiro的权限校验,但是一般情况下这些接口/请求都需要对应用户的权限,所以绕过了shiro登录到后台系统只是以一种没有用户身份的方式登录到后台系统, 后台校验此时获取当前用户信息,发现为空.此时整体系统就会出错,或者重新跳转到登录页面,重新登录.这里就不作说明了


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK