Apache Shiro CVE-2020-1957&CVE-2020-11989身份认证绕过漏洞分析

0x00 漏洞信息

1
2
3
4
5
VUL = ["CVE-2020-1957&CVE-2020-11989"]
VUL_NAME = ["Apahce Shiro权限绕过漏洞"]
TYPE = ["Permissions Bypass"]
DESCRIPTION = '''2020年3月23号,Shiro开发者Brian Demers在用户社区发表帖子,提醒shiro用户进行安全更新,本次更新进行了三个修复,其中就包括了对编号为CVE-2020-1957的Shrio授权绕过漏洞的修复。SHIRO-682的修复了spring框架下uri = uri + ‘/’ 绕过Shiro防护的问题。然后下面的描述则清晰得描述了造成改错误的原因。在Spring web项目中,请求URI /resource/menus和/resource/menus/ 都可以访问到服务器的资源。但在Shiro中的URL路径表达式pathPattern可以正确匹配/resource/menus,但不能正确匹配/resource/menus/,导致过滤链无法正确匹配,从而绕过Shiro的防护机制。'''
IMPACT = ["Apahce Shiro < 1.5.3"]

0x01 漏洞复现

环境搭建:

首先搭建漏洞环境。看了网上方法,利用Spring Boot + shiro搭建。

先看了大家都参考的博客 https://segmentfault.com/a/1190000019440231 ,发现自己其实创建Spring项目就看不懂,所以先向郭老师请教了下,远程协助我建了项目,又发了我一篇文章 https://developer.aliyun.com/article/695233 ,总算搞懂怎么创建了,感谢郭老师的耐心:heart:。

创建项目,new-project,进入界面,选择Spring Initializr,这里服务填写阿里的下载更快点 https://start.aliyun.com/ ,点击next

image-20200713145444783

填写、选择项目元数据,next

image-20200713145727086

选择Spring Web依赖,点击next,选择项目目录点击finish即创建完成

image-20200713150325825

等待下载完成,在pom.xml添加Shiro依赖,这里shiro版本我选的1.4.0,只要在1.5.2以下都可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>

创建完成后,发现项目下自动生成三个文件:src/main/java-ShiroApplication.class程序入口;src/main/resources-配置文件-application.properties;src/test/java-ShiroApplicationTests.class测试程序

image-20200713151229419

接下来配置shiro过滤规则

创建Realm,实现认证:

MyRealm.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.debug.shiro.bean;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class MyRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
if (!"javaboy".equals(username)) {
throw new UnknownAccountException("账户不存在!");
}
return new SimpleAuthenticationInfo(username, "123", getName());
}
}

配置shiro:

ShiroConfig.class
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
package com.debug.shiro.config;

import com.debug.shiro.bean.MyRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
MyRealm myRealm() {
return new MyRealm();
}

@Bean
org.apache.shiro.mgt.SecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(myRealm());
return manager;
}

@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("/doLogin", "anon");
map.put("/guest/*", "anon");
map.put("/admin/*", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
}

配置LoginController:

LoginController.class
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
package com.debug.shiro.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class LoginController {
@PostMapping("/doLogin")
public void doLogin(String username, String password) {
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new UsernamePasswordToken(username, password));
System.out.println("登录成功!");
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("登录失败!");
}
}

@RequestMapping("/guest/*")
public String guest(){
return "guest page";
}

@RequestMapping("/admin/index")
public String admin(){
return "admin page";
}

@GetMapping("/login")
public String login() {
return "please login!";
}

}

可以开始运行

image-20200713152726548

访问 http://localhost:8080/login

image-20200713152826010

至此,环境搭建成功。

漏洞复现:

CVE-2020-1957

访问 http://localhost:8080/admin/index ,可以看到被拦截,需要登录

image-20200713154205020

访问 http://localhost:8080/admin/index/ ,直接进入admin界面,无需登录

image-20200713154233782

CVE-2020-11989

访问 http://localhost:8080/;/admin/index ,绕过登录进入admin界面

image-20200713154421239

0x02 漏洞分析

拦截器基础知识

在debug分析前,先学习下Shiro拦截器的规则: https://www.jianshu.com/p/3671b97aab3d

Shiro拦截器有11种,这里只简单看下anon和authc,anon不需要认证可以直接访问,对应上述的/guest/*和/dologin,authc需要认证才能访问,对应/admin/*。

image-20200713161043499

文章中介绍了两种拦截器配置方法:INI文件文件配置和代码配置,复现例子中用的是代码配置。

拦截器规则通配符如下:

1
2
3
?:匹配一个字符
*:匹配零个或多个字符
**:匹配零个或多个路径

CVE-2020-1957复现例子中用到的是/admin/*,只匹配了admin下的文件,不会匹配多个路径,可以复现成功,如果是/admin/**则复现失败,有一定的局限性;而CVE-2020-11989无论是/admin/*还是/admin/**都可以复现成功。

CVE-2020-1957分析

根据commit信息 https://github.com/apache/shiro/commit/3708d7907016bf2fa12691dff6ff0def1249b8ce ,对WebUtils#getPathWithinApplication等进行了修复

image-20200714131644955

所以我们在org.apache.shiro.web.util.WebUtils#getPathWithinApplication:48行打断点,开始debug模式,然后访问 http://127.0.0.1:8080/admin/index/ 开始分析

F8执行到String requestUri = getRequestUri(request) 然后继续执行

image-20200714132604290

一直F8直到获取URI,getRequestUri(request) 返回的uri是我们输入的请求:/admin/index/

image-20200714102608525

继续F8进入getPathWithinApplication(HttpServletRequest request),返回/admin/index/

image-20200714102956166

继续F8进入PathMatchingFilterChainResolver#getChain,拿到了刚刚getPathWithinApplication获取的请求URI-/admin/index/,将请求的URI链匹配配置的过滤规则链

image-20200713223910948

执行到while条件时,F7进入pathMatches

image-20200714133200908

pathMatches收到两个参数,pattern和path,一个是设置的规则链,一个是请求的路径,return pathMatcher.matches(pattern, path)我们F7进入pathMatcher.matches

image-20200714133408631

此时进入到了org.apache.shiro.util.AntPathMatcher#matches,继续深入,会调用doMatch

image-20200714133928022

分析doMatch,主要是用来匹配规则链和请求

image-20200714134656016

继续执行,一直while循环到/admin/*和/admin/index/进行比较再来看下。分解路径时,是以”/“来进行分解,/admin/index/分解后的是{“admin”,”index”},这里是和/admin/index分解后的结果一样,继续往下进入循环,第一个字符串相比较都是admin肯定一样,继续循环比较第二个*和index,调用matchStrings方法

image-20200714151630406

F7看下matchStrings,比较*和index。循环比较每个字符,当字符为*,说明规则包含*,退出比较,返回true,匹配成功

image-20200714152537290

继续回到doMatch,循环结束,此时返回pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) : !path.endsWith(this.pathSeparator),pattern.endsWith(this.pathSeparator) ,规则链/admin/*不是以分隔符/结尾,所以返回!path.endsWith(this.pathSeparator),/admin/index/是以分隔符/结尾,所以为true,最终返回false,没有匹配到规则链。假设本来请求的是/admin/index,不以/结尾,这里最终会返回true,成功匹配到规则链

image-20200714153059523

回到getChain,因为返回结果为false即没有匹配成功,所以while会继续循环,直到迭代完成,返回null。这里假如是请求/admin/index,匹配到了,会执行 filterChainManager.proxy(originalChain, pathPattern),也就会进行代理。但是这里直接返回null没有执行proxy从而绕过了鉴权。

image-20200714153812174

接下来继续分析如何获取到/admin/index的资源

F8到getMatchingPattern调用org.springframework.util.AntPathMatcher#match,然后doMatch

image-20200714205353393

深入分析doMatch,可以看到跟之前类似,都是用/直接分解路径,然后依次比较,由于一个是以/结尾 一个不是,所以最终返回了false

image-20200714205939671

getMatchingPattern(“/admin/index”, “/admin/index/“)返回/admin/index/

image-20200714211044556

getMatchingPatterns返回matches={“/admin/index/“}

image-20200714211754168

继续F8,返回到AbstractHandlerMethodMapping#addMatchingMappings,match=”/admin/index/“,因为match不为NULL,认为匹配成功了,直接获取/admin/index的资源

image-20200714215504718

最后梳理下,首先鉴权的时候,匹配规则链doMatch用/进行分隔,/admin/index/和/admin/index效果一样,因为一个以/结尾,一个不以/结尾返回false绕过了鉴权;请求资源的时候,匹配规则链时同样存在问题,getMatchingPattern返回的match不为NULL就判断匹配成功,获取到/admin/index的资源。

CVE-2020-11989分析

更上面同样的断点,访问 http://127.0.0.1:8080/;/admin/index ,开始debug

调用getRequestUri(request),进入该方法

image-20200714234621778

获取返回值时,调用decodeAndCleanUriString(request, uri),分析该方法如图,该方法对含有;的URI进行了处理,当有;时,获取;前的字符串,此时返回/

image-20200714235245592

经过normaliz返回/

image-20200714235743342

此时请求URI由”/;/admin/index”变成了”/“,然后再去匹配chain规则链就跟之前一样,不能匹配从而绕过鉴权

image-20200715000016874

接下来分析是如何获取/admin/index的资源

F8直到org.springframework.web.util.UrlPathHelper#getRequestUri

image-20200715000707589

分析 this.decodeAndCleanUriString

image-20200715001035983

调用removeSemicolonContent,返回//admin/index

image-20200715001526885

继续执行,调用 this.getSanitizedPath

image-20200715001735002

经过getSanitizedPath,将//处理成/,最终获取的是/admin/index

image-20200715002010255

总结下,首先请求/;/admin/index要先进行鉴权,经过org.apache.shiro.web.util.WebUtils#decodeAndCleanUriString(request, uri)处理成/,绕过了授权,然后请求时再经过org.springframework.web.util.UrlPathHelper#decodeAndCleanUriString获取/admin/index的资源

参考链接:

Spring Boot 2.x基础教程:快速入门
Spring Boot 整合 Shiro ,两种方式全总结!
SpringBoot:集成Shiro之拦截器配置
Shiro权限绕过漏洞分析(CVE-2020-1957)
Apache Shiro权限绕过漏洞分析(CVE-2020-11989)