Spring Use Apache Shiro Servlet权限认证绕过漏洞分析

微信扫一扫,分享到朋友圈

Spring Use Apache Shiro Servlet权限认证绕过漏洞分析

简介

在Spring Boot中使用Apache Shiro进行身份验证、权限控制时,可以精心构造恶意的URL,利用Apache Shiro和Spring Boot对URL处理的差异化,可以绕过Apache Shiro对Spring Boot中的Servlet的安全权限认证,越权并实现Servlet未授权访问。

近期涉及到的主要有CVE-2020-1957、CVE-2020-11989、CVE-2020-13933三个漏洞编号。

环境要点:

  • 参考threedr3am师傅 Spring-Shiro集合项目 代码

  • Spring Boot 1.5.22.RELEASE

  • Apache Shiro 1.5.x,根据编号变化

  • Java(TM) SE Runtime Environment (build 1.8.0_112-b16)

CVE-2020-1957

影响Apache Shiro 1.5.1以前版本

配置

pom.xml 具体配置:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.22.RELEASE</version>
<relativePath/>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cve-2020-1957</artifactId>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>7</source>
<target>7</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<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.5.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.1</version>
</dependency>
</dependencies>
</project>

Shiro版本为 1.5.1

在概念层, Shiro 架构包含三个主要的理念: Subject SecurityManager Realm

Spring Boot 整合 Shiro 的核心逻辑和代码如下:

Realm.java

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

Shiro 中的 Realm 提供待验证数据的验证方式。

SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作。

比如此处代码就通过重写 doGetAuthorizationInfo 方法,并以账户名 rai4over 和密码 123456 为标准对登录进行了身份认证。

ShiroConfig.java

@Configuration
public class ShiroConfig {
@Bean
MyRealm myRealm() {
return new MyRealm();
}
@Bean
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("/login", "anon");
map.put("/xxxxx/**", "anon");
map.put("/aaaaa/**", "anon");
map.put("/admin", "authc");
map.put("/admin.*", "authc");
map.put("/admin/**", "authc");
map.put("/**", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
}

Shiro 配置类,创建 SecurityManager ,并为 SecurityManager 提供并设置 Realm 。在 shiroFilterFactoryBean 中设置具体的拦截器规则,admin及其路径下的url设置权限为 authc ,需要经过登录认证后才能访问;其他的 login xxxxx 等URL则设置权限为 anon ,可以无需权限认证进行匿名访问。

TestController.java

@RestController
public class TestController {
@RequestMapping(value = "/login")
public String login(String username, String password) {
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new UsernamePasswordToken(username, password));
return "登录成功!";
} catch (AuthenticationException e) {
e.printStackTrace();
return "登录失败!";
}
}
@RequestMapping(value = "/admin", method = RequestMethod.GET)
public String admin() {
return "admin secret bypass and unauthorized access";
}
@RequestMapping(value = "/xxxxx", method = RequestMethod.GET)
public String xxxxx() {
return "xxxxx";
}
}

Spring Boot Controller ,包含和配置类对应的路由 admin xxxxx 等的响应方式。

复现

/xxxxx 无需认证访问内容

/admin 直接访问会因为权限校验而失败,跳转到 /login 登录

使用POC /xxxxx/..;/admin ,越权访问 /admin 这个Servlet,并且内容成功返回

分析

我们发送的恶意 /xxxxx/..;/admin 请求首先经过 Shiro 进行处理

org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain

shiro 中的 PathMatchingFilterChainResolver 类对传入的 URL 进行解析,并和已经配置的过滤器规则进行匹配进行判断。

org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getPathWithinApplication

实现自定义请求到应用程序路径的解析行为,参数为 ServletRequest 对象,包含请求的上下文信息:

org.apache.shiro.web.util.WebUtils#getPathWithinApplication

getPathWithinApplication 检测并返回路径。

org.apache.shiro.web.util.WebUtils#getRequestUri

从请求上下文对象中获取具体的 URI ,也就是 /xxxxx/..;/admin ,然后传入 decodeAndCleanUriString

org.apache.shiro.web.util.WebUtils#decodeAndCleanUriString

; 后面进行截断,此时的uri为 /xxxxx/.. ,返回并作为参数传入 normalize

org.apache.shiro.web.util.WebUtils#normalize(java.lang.String)

继续跟进

org.apache.shiro.web.util.WebUtils#normalize(java.lang.String, boolean)

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

对URI进行了规范化操作,比如循环替换反斜线、对多个下划线进行多余替换等操作,URI结果仍为 /xxxxx/.. ,并返回到上层的 getChain 进行具体权限判断。

/org/apache/shiro/shiro-web/1.5.1/shiro-web-1.5.1-sources.jar!/org/apache/shiro/web/filter/mgt/PathMatchingFilterChainResolver.java:123

for 循环中进行判断权限,遍历的对象是 filterChainManager.getChainNames()

org.apache.shiro.web.filter.mgt.DefaultFilterChainManager#getChainNames

返回和过滤器配置的一样的集合,具体为:

查看通过校验时的情况

/xxxxx/.. /xxxxx/** 进行匹配时,是能够成功匹配的。

因此请求 /xxxxx/..;/admin ,在shiro中经过处理变为 /xxxxx/.. ,与过滤器 /xxxxx/** 规则进行匹配通过校验,成功转向后方的 Spring Boot

恶意请求 /xxxxx/..;/admin 通过Shiro的校验后,传递到Spring Boot中进行解析,根据 Controller 设置的路由选择对应 Servlet

org.springframework.web.util.UrlPathHelper#getPathWithinServletMapping

开始获取请求对应的 Servlet 路径。

org.springframework.web.util.UrlPathHelper#getServletPath

从请求上下文对象中获取 javax.servlet.include.servlet_path 属性的结果为 null ,进入 if 分支。

javax.servlet.http.HttpServletRequestWrapper#getServletPath

Spring Boot 此处开始使用 JDK 从请求上下文对象中获取 Servlet

org.apache.catalina.connector.Request#getServletPath

经过 JDK 解析从 Mapping 中得到 Servlet 结果为 /admin

/Users/rai4over/.m2/repository/org/springframework/spring-web/4.3.25.RELEASE/spring-web-4.3.25.RELEASE-sources.jar!/org/springframework/web/util/UrlPathHelper.java:231

最后返回给 Spring Boot ,形成了对 /admin 这个 Servlet 的未授权访问,最终再返回给攻击者。

CVE-2020-11989

影响Apache Shiro 1.5.3以前版本

配置

pom.xml 具体配置:

    <dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.2</version>
</dependency>

shiro版本变为 1.5.2

ShiroConfig.java 中的 shiroFilterFactoryBean 需要修改过滤器规则

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

/aaaaa/ 可以匿名访问, /admin/ 需要权限认证,并且不存在 /** 兜底拦截设置。

TestController.java 中修改路由

@RequestMapping(value = "/admin/{id}", method = RequestMethod.GET)
public String admin(@PathVariable(name = "id") String id) {
return "admin secret bypass and unauthorized access and parameter = " + id;
}

复现

/ 进行两次URL编码,POC为 http://127.0.0.1:9999/admin/aaa%252Faaa

成功未授权访问 /admin/ 内容

分析

org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain

经过 getPathWithinApplication 得到 requestURI /admin/aaa/aaa ,然后到 for 循环里使用 pathMatches 函数对过滤器权限进行遍历和判断。

shiro-core-1.5.2-sources.jar!/org/apache/shiro/util/AntPathMatcher.java:155

此时的调用栈为:

doMatch:155, AntPathMatcher (org.apache.shiro.util)
match:90, AntPathMatcher (org.apache.shiro.util)
matches:86, AntPathMatcher (org.apache.shiro.util)
pathMatches:152, PathMatchingFilterChainResolver (org.apache.shiro.web.filter.mgt)
getChain:123, PathMatchingFilterChainResolver (org.apache.shiro.web.filter.mgt)
getExecutionChain:415, AbstractShiroFilter (org.apache.shiro.web.servlet)
executeChain:448, AbstractShiroFilter (org.apache.shiro.web.servlet)
call:365, AbstractShiroFilter$1 (org.apache.shiro.web.servlet)
doCall:90, SubjectCallable (org.apache.shiro.subject.support)
call:83, SubjectCallable (org.apache.shiro.subject.support)
execute:387, DelegatingSubject (org.apache.shiro.subject.support)
doFilterInternal:362, AbstractShiroFilter (org.apache.shiro.web.servlet)
doFilter:125, OncePerRequestFilter (org.apache.shiro.web.servlet)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:99, RequestContextFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:109, HttpPutFormContentFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, HiddenHttpMethodFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:197, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:199, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:493, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:137, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:87, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:798, Http11Processor (org.apache.coyote.http11)
process:66, AbstractProcessorLight (org.apache.coyote)
process:808, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1498, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

/admin/aaa/aaa 中包含两个 / ,因此条件进入分支返回false,这样 /admin/aaa/aaa 就不会匹配上 /admin/* 规则,就不会进行进行权限验证,然后将请求转发给后端的Spring Boot。

org.springframework.web.util.UrlPathHelper#getPathWithinServletMapping

可以看到Spring Boot中解析为 /admin/aaa%2Faaa aaa%2Faaa 被当做字符串,刚好匹配后端路由规则 /admin/{id} ,所以页面显示的时候 id 的值为 /aaa%2Faaa

CVE-2020-13933

影响Apache Shiro 1.6.0以前版本

配置

pom.xml具体配置:

    <dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>

shiro版本变为 1.5.3

ShiroConfig.java 中的 shiroFilterFactoryBean 需要修改过滤器规则

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

与CVE-2020-11989相同, /admin/ 需要权限认证,,并且不存在 /** 兜底拦截设置。

TestController.java 不变

@RequestMapping(value = "/admin/{id}", method = RequestMethod.GET)
public String admin(@PathVariable(name = "id") String id) {
return "admin secret bypass and unauthorized access and parameter = " + id;
}

复现

利用 ; 和URL编码,POC为 http://127.0.0.1:9999/admin/%3bpage

分析

org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain

请求经过ULR解码后为 /admin/;page ,经过 getPathWithinApplication 处理会截断 ; 得到 requestURI /admin ,然后到 for 循环里使用 pathMatches 函数对过滤器权限进行遍历和判断。

在Shiro拦截器规则中 /admin 无法匹配规则 /admin/* ,成功绕过并访问Spring后端的 /admin/{id}

org.springframework.web.util.UrlPathHelper#getPathWithinServletMapping

可以看到Spring Boot中解析为 /admin/;page ;page 被当做字符串,刚好匹配后端路由规则 /admin/{id} ,所以页面显示的时候 id 的值为 ;page

参考

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

https://www.cnblogs.com/ph4nt0mer/p/13535999.html

本文作者:Rai4over

微信扫一扫,分享到朋友圈

Spring Use Apache Shiro Servlet权限认证绕过漏洞分析

文思海辉金融连续七年稳居银行业CRM解决方案子市场榜首

上一篇

韩国探月计划稳步推进 2022年发射首颗绕月轨道探测器

下一篇

你也可能喜欢

Spring Use Apache Shiro Servlet权限认证绕过漏洞分析

长按储存图像,分享给朋友