CVE-2020-1957,Spring Boot中使用Apache Shiro进行身份验证、权限控制时,可以精心构造恶意的URL,利用Apache Shiro和Spring Boot对URL的处理的差异化,可以绕过Apache Shiro对Spring Boot中的Servlet的权限控制,越权并实现未授权访问。
项目代码可以通过threedr3am师傅项目进行魔改,加深理解
https://github.com/threedr3am/learnjavabug
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
架构包含三个主要的理念: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
登录
/xxxxx/..;/admin
越权访问admin
内容成功
我们发送的恶意/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
。
当前的调用栈为:
getChain:128, 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)
恶意请求/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
的未授权访问,最终再返回给攻击者。
修改了requestURI
的获取方式,经过更准确的解析获取。