简介

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

环境

  • Java(TM) SE Runtime Environment (build 1.8.0_112-b16)
  • Apache Shiro 1.5.1
  • Spring Boot 1.5.22.RELEASE

项目代码可以通过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架构包含三个主要的理念:SubjectSecurityManagerRealm

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,需要经过登录认证后才能访问;其他的loginxxxxx等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 BootController,包含和配置类对应的路由adminxxxxx等的响应方式。

复现

xxxxx无需认证访问内容

admin访问就跳转到login登录

/xxxxx/..;/admin越权访问admin内容成功

分析

Shiro 处理

我们发送的恶意/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)

Spring 处理

恶意请求/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的未授权访问,最终再返回给攻击者。

修复

https://github.com/apache/shiro/commit/3708d7907016bf2fa12691dff6ff0def1249b8ce#diff-98f7bc5c0391389e56531f8b3754081aR139

修改了requestURI的获取方式,经过更准确的解析获取。

参考

https://github.com/threedr3am/learnjavabug

http://www.51gjie.com/javaweb/1138.html

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖