CVE-2020-13933 | Shiro权限绕过漏洞

CVE-2020-13933 | Shiro权限绕过漏洞

image-20220406210511216

这次调试查看Shiro权限绕过漏洞可以说是在于开始对Java的部署和调试开始得心应手起来了,相比这个漏洞而言,感觉这次学习让我对Java分析思路的了解是更加重要的,跟了这个漏洞之后再去学习其它的Shiro-CVE和Fastjson还有JSON应该就简单多了。

关于漏洞

CVE-2020-13933: Apache Shiro 权限绕过漏洞分析

漏洞影响

  • Apache Shiro < 1.6.0

第二次搭Shiro了,但是搭环境的时候也还是遇到了一些坑,只能说Java的环境太难了好吧。。。。

环境搭建: 项目搭建传送门

示例源码: 环境源码传送门

注意需要修改一下org.syclover.srpingbootshiro.LoginController#admin函数:

    @GetMapping("/admin/{name}")
    public String admin() {
        return "admin page";
    }

需要知道

下面开始进入框架分析之前我们需要先明白几个概念:

image-20220405234617612

我们的Shiro过滤器是在Spring映射加载资源之前就进行一些过滤判断操作的了

所以我们接下来将整个流程划分一下:

image-20220406000320697

Image

Subject:

主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;

SecurityManager:

安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;

Realm:

域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

image-20220405153031055

PathMatchingFilter: 提供了基于Ant风格的请求路径匹配功能及拦截器参数解析的功能。

OncePerRequestFilter: 过滤器基类,该基类保证每个请求在任何servlet容器上仅执行一次,另外提供enabled属性,表示是否开启该拦截器实例,默认enabled=true表示开启,如果不想让某个拦截器工作,可以设置为false即可。

AbstractShiroFilter: 是Shiro的入口,根据URL配置的filter,选择并执行相应的filter chain。

Shiro拦截器

Shiro框架通过拦截器功能来实现对用户访问权限的控制和拦截。Shiro中常见的拦截器有anon,authc等拦截器

anon为匿名拦截器,不需要登录就能访问,一般用于静态资源,或者移动端接口

authc为登录拦截器,需要登录认证才能访问的资源

用户可以在Shiro.ini编写匹配URL配置,将会拦截匹配的URL,并执行响应的拦截器。从而实现对URL的访问控制,URL路径表达式通常为ANT格式。如下配置,访问 /index.html 主页的时候,Shiro将不会对其进行登录判断,anon拦截器不需要登录就能进行访问。而对于 /user/xiaoming 等 /user/xiaogang 等接口,authc拦截器将会对其进行登录判断,有登录认证才能访问资源。

[urls] /index.html = anon /user/** = authc

Shiro的URL路径表达式为Ant格式,路径通配符支持?* **

?:匹配一个字符 *:匹配零个或多个字符串 * :匹配路径中的零个或多个路径

其中表示匹配零个或多个字符串,/可以匹配/hello,但匹配不到/hello/,因为通配符无法匹配路径

假设/hello接口设置了authc拦截器,访问/hello将会被进行权限判断,如果请求的URL为/hello/,/*URL路径表达式将无法正确匹配,放行。然后进入到spring(Servlet)拦截器,spring中/hello形式和/hello/形式的URL访问的资源是一样的

更多的一些处理初始请求的过程可以看文章但是没有一点基础的小伙伴还是不建议看了(上面这些看不懂页问题不大)。


框架流程

花了差不多两天的时间才把这个Spring-shiro框架的资源申请访问流程弄清楚, 之前看了很久网上的大师傅们讲解这个漏洞分析的文章, 很多都是直接对内部的一些函数进行讲解, 但是对于这些函数在整个框架和访问流程中所处的位置都没有进行一个清晰的说明, 这就让我这个没走过Java框架的菜鸡看的很难受了, 看了半天都还是对漏洞一知半解, 所以我在这里花了点时间将这个框架的访问流程中涉及到这个漏洞的一些主要相关函数的运行流程写到流程图当中以免读者看我这里的文章遇到像我一样的问题。

但是因为这个过程我想写详细一点方便读者理解这个数据流的主要流向和过滤器的详细处理流程所以虽然我已经尽力删减了一些不必要的函数但是还是有很大一块, 想详细了解的读者可以结合我下面这个图的流程

image-20220406191743046

因为图太大所以到后面会糊所以我们再将图片分为多个部分一一截图

image-20220406135455075

image-20220406190254434

image-20220406191953374

image-20220406192030142

问题出现在下面:

image-20220406134412161

image-20220406134558698

shiro 过滤器链调用

理解几个词:

ApplicationFilterChain: 
发出请求后在Spring-Shiro项目执行之前先由它指定过滤器

OncePerRequestFilter:
过滤器基类,该基类保证每个请求在任何servlet容器上仅执行一次,另外提供enabled属性,表示是否开启该拦截器实例,默认enabled=true表示开启,如果不想让某个拦截器工作,可以设置为false即可。

经过ApplicationFilterChain,请求被分派到OncePerRequestFilter过滤器进行拦截,首先调用getAlreadyFilteredAttributeName给我们的自定义过滤器打上标记,并通过判断当前请求中是否有该标记来判断该拦截器是否已经调用过,接着判断是否标记该拦截器不进行工作.

如果都没有,那么调用doFilterInternal方法,这里调用的是其子类AbstractShiroFilter#doFilterInternal方法,解析requestresponse。接着根据请求利用SecurityManager创建WebSubject接口类型的实例,这里采用的是建造者模式。

这个拦截器是否工作的判断标志可以理解为我们当前项目的session, 如果这个session已经登录成功则最终让我们跳转到login页面的过滤器就不工作

嗯....shiro 过滤器链调用这一段不是问题所在, 如果看不懂的话也不要紧, 后面的调用国我们就不详细展开了

shiro url 处理

image-20220406192258133

调用org.apache.shiro.web.filter.PathMatchingFilter#getPathWithinApplication方法。

image-20220406001243644

不管是5.1.2还是5.1.3都先到这一步,但是它们执行的WebUtils.getPathWithinApplication(WebUtils.toHttp(request))有所差别

跟到WebUtils#getPathWithinApplication方法, 这里Shirourl的处理也是造成CVE-2020-11989的一个点。左边是修复之前的1.5.2版本,右边是当前版本1.5.3,采用getRequestUri,而getRequestUri里就进行了url的解码。

image-20220406001748767

1.5.2和1.5.3在下图中的左上角有jar包版本显示:

image-20220405203124511

image-20220405203235409

区别:

  • 1.5.2获取URL的org.apache.shiro.web.util.WebUtils#getPathWithinApplication采用的是getRequestUri方法同时里面会调用到decodeAndCleanUriString -> decodeRequestString进行再一次的解码。

  • 而5.1.3采用了标准的getServletPath(request) + getPathInfo(request)同时不再进行url解码。

    image-20220406213421124

  • 修复之后,采用了标准的url解析,不再对CVE-2020-11989使用的%2f也就是/解码,但是并不影响我们的绕过操作。

首先说明一下, 在Shiro的1.5.2和1.5.3都存在这个漏洞,它们执行后返回的结果都是一样的,都为/admin/,我们看一下1.5.3是怎么处理的:

1.5.3

  1. getServletPath方法得到的URL是请求加入应用后已经解码了一次的URL也就是/admin/;page,返回结果为/admin/;page

image-20220406002153359

  1. removeSemicolon,该方法查找;,并将;及其之后的部分删除得到/admin/

image-20220406002211865

  1. normalize,用于操作./所带来的影响。本次测试返回/admin/

image-20220406002244389

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

        private static String normalize(String path, boolean replaceBackSlash) {
            if (path == null) {
                return null;
            } else {
                String normalized = path;
                if (replaceBackSlash && path.indexOf(92) >= 0) {
                    normalized = path.replace('\\', '/');
                }
    
                if (normalized.equals("/.")) {
                    return "/";
                } else {
                    if (!normalized.startsWith("/")) {
                        normalized = "/" + normalized;
                    }
    
                    while(true) {
                        int index = normalized.indexOf("//");
                        if (index < 0) {
                            while(true) {
                                index = normalized.indexOf("/./");
                                if (index < 0) {
                                    while(true) {
                                        index = normalized.indexOf("/../");
                                        if (index < 0) {
                                            return normalized;
                                        }
    
                                        if (index == 0) {
                                            return null;
                                        }
    
                                        int index2 = normalized.lastIndexOf(47, index - 1);
                                        normalized = normalized.substring(0, index2) + normalized.substring(index + 3);
                                    }
                                }
    
                                normalized = normalized.substring(0, index) + normalized.substring(index + 2);
                            }
                        }
    
                        normalized = normalized.substring(0, index) + normalized.substring(index + 1);
                    }
                }
            }
        }
    
  2. 所以最终org.apache.shiro.web.filter.PathMatchingFilter#getPathWithinApplication返回了/admin/

返回的/admin/交给谁呢?

image-20220406192905489

接着到org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain

在这里继续执行下一步, 我们得到的/admin/赋值给requestURI变量

image-20220406002928828

    public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
        FilterChainManager filterChainManager = this.getFilterChainManager();
        if (!filterChainManager.hasChains()) {
            return null;
        } else {
            String requestURI = this.getPathWithinApplication(request);
            if (requestURI != null && !"/".equals(requestURI) && requestURI.endsWith("/")) {
                requestURI = requestURI.substring(0, requestURI.length() - 1);
            }

            Iterator var6 = filterChainManager.getChainNames().iterator();

            String pathPattern;
            do {
                if (!var6.hasNext()) {
                    return null;
                }

                pathPattern = (String)var6.next();
                if (pathPattern != null && !"/".equals(pathPattern) && pathPattern.endsWith("/")) {
                    pathPattern = pathPattern.substring(0, pathPattern.length() - 1);
                }
            } while(!this.pathMatches(pathPattern, requestURI));

            if (log.isTraceEnabled()) {
                log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + Encode.forHtml(requestURI) + "].  Utilizing corresponding filter chain...");
            }

            return filterChainManager.proxy(originalChain, pathPattern);
        }
    }

进入if判断因为/admin/不是/并且最后一个字符为/所以为true执行里面的语句

image-20220406003141861
requestURI = requestURI.substring(0, requestURI.length() - 1);

这个if判断的作用其实就是检查访问的资源是否为根目录资源(url为http://host:port/)

如果不是访问根目录并且最后又带有/结尾的话那就把结尾的/给去掉

经过以上的getPathWithinApplication和if判断语句的处理后得到的requestURI

处理结果示例:

/home/images	=>	/home.images
/home/images/	=>	/home/images
/home/imag/es/	=>	/home/imag/es

接着处理后的requestURI交给this.getFilterChainManager()处理

org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getFilterChainManager:

    public FilterChainManager getFilterChainManager() {
        return this.filterChainManager;
    }

org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#filterChainManager:

    private FilterChainManager filterChainManager;

这个filterChainManager就是我们上面的流程图当中的Filter1,Filter2...的管理者,我们当前断点条件下使用到的这个过滤器管理者实际上就是用于管理我们在org.syclover.srpingbootshiro.LoginController当中自定义的一些接口, 而其中的getChainNames()函数就用于获取这个管理员所管理的全部过滤器接口格式的(这个格式已经被转化为Ant格式而不是原本我们在定义时所写的{name}这种格式)

(但是其实我不是很理解为什么/login接口没有在这里)

image-20220406005420194

image-20220406005836851

image-20220406005756621

但是filterChainManager只是一个接口,用于构造过滤器,里面的函数等着我们自己构造特定功能的过滤器时根据需要去实现

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.apache.shiro.web.filter.mgt;

import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import org.apache.shiro.config.ConfigurationException;

public interface FilterChainManager {
    Map<String, Filter> getFilters();

    NamedFilterList getChain(String var1);

    boolean hasChains();

    Set<String> getChainNames();

    FilterChain proxy(FilterChain var1, String var2);

    void addFilter(String var1, Filter var2);

    void addFilter(String var1, Filter var2, boolean var3);

    void createChain(String var1, String var2);

    void addToChain(String var1, String var2);

    void addToChain(String var1, String var2, String var3) throws ConfigurationException;
}

后面一直执行do...while语句, 我们先不看语句内容而是来看看最后可能会有什么结果:

  1. return null;

    如果return的话就表示没有我们自定义的过滤器是匹配当前用户访问资源的,所以就不会使用我们自定义的过滤器而是使用默认的过滤器
    

    在这里我们自定义的过滤器会进行身份验证但是默认的过滤器就不会进行身份验证也就导致越权问题了

  2. return filterChainManager.proxy(originalChain, pathPattern)

    originalChain : 执行函数传入的第三个变量FilterChain originalChain
    pathPattern	  : 我们用户自定义的在当前filterChainManager管理下的过滤器器
    

    filterChainManager.proxy函数即相当于是说使用这个符合匹配规则的过滤器对默认过滤器进行代理, 简单说就是使用我们需要身份验证的过滤

寻找条件

所以我们一已经知道了想要发生越权问题的点:

org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain执行return null

但是有哪些条件呢?

可以看到do...while语句

        Iterator var6 = filterChainManager.getChainNames().iterator();

        String pathPattern;		//doLogin
        do {
            if (!var6.hasNext()) {
                return null;
            }

            pathPattern = (String)var6.next();
            if (pathPattern != null && !"/".equals(pathPattern) && pathPattern.endsWith("/")) {
                pathPattern = pathPattern.substring(0, pathPattern.length() - 1);
            }
        } while(!this.pathMatches(pathPattern, requestURI));
        
注: 这里的pathPattern = "doLogin" 应该就是代码编写者自己写的,并没有什么特殊的,因为它会在while语句里面更新的

可以看到想要执行return null;的条件就是!var6.hasNext()

这个var6我们上面已经解释过,就是管理我们自定义的两个入口/admin/{name}/login 的过滤器管理者,而它的成员就是我们的两个过滤器,hasNext()函数很明显就是一个获取下一个过滤器的函数,在第一次执行的时候hasNext()就会得到"/doLogin",

image-20220406011838785

第二次执行就是得到"/admin/*"

到第三次自然就是为空, 这时!var6.hasNext()条件自然就成立了, 但是如果退出了我们的do...while循环后面就只剩下return filterChainManager.proxy(originalChain, pathPattern)这一个return语句了

所以我们必须保证:

  • 在执行!var6.hasNext()前一直满足条件!this.pathMatches(pathPattern, requestURI)

getChain返回null的条件

while循环体内的语句

先分析一下结构体里面的语句:

do{
    .....
pathPattern = (String)var6.next();
    if (pathPattern != null && !"/".equals(pathPattern) && pathPattern.endsWith("/")) {
        pathPattern = pathPattern.substring(0, pathPattern.length() - 1);
	}
}while(!this.pathMatches(pathPattern, requestURI));
  1. pathPattern = (String)var6.next();自然就是获取下一个匹配格式了
  2. if判断语句和上面一样, 判断是否去掉结尾的/

过滤器的匹配过程

再开始分析getChain返回null的条件之前我们先来了解一些过滤器的匹配过程,因为这个while条件中执行的语句实际上就是过滤器的匹配过程

this.pathMatches实际上的作用就相当于和我们的每个过滤器都匹配一次看看有没有哪个过滤器是合适当前访问的URL路径资源的,如果有的话那就退出do...while语句然后在后面使用这个满足匹配条件的过滤器代理取代掉默认的过滤器

这个匹配流程大体为:

  1. 第一步: 将原始的匹配格式转化为ANT格式然后我们记为pattern:

    变成ANT格式分别为:

    "/login"			=> 		pattern1 = "/login"
    "/admin/{name}"		=>		pattern2 = "/admin/*"
    

    image-20220405151829277

  2. 第二步: 通过/将pattern分割为数组,我们记为pattDirs

    这里的分割函数我们使用的是org.apache.shiro.util.StringUtils#tokenizeToStringArray(java.lang.String, java.lang.String)在这个案例环境中第一个参数也就是分隔符为/ ,第二个参数就是被分割的字符串pattern

    pattern1 = "/login"		=>		pattDirs1 = ["login"]
    pattern2 = "/admin/*"	=>		pattDirs2 = ["admin","*"]
    
  3. 将传入的URL路径也通过StringUtils#tokenizeToStringArray分割,并且分隔符也设为/,得到的分割数组记为pathDirs

    /admin/	=>		pathDirs = ["admin"]
    

    可能有人好奇命名传入URL路径的是/admin/%3bpage怎么就变成了/admin/?

    1. 呃,,,,,,该说不说如果你真有这个疑问的话那说明你没仔细看前面的内容了,建议回去看一下我们是怎么从org.apache.shiro.web.util.WebUtils#getPathWithinApplication走到这里的,但是也还是在这里再解释一下吧:

      答:

      1. 我们的URL路径为/admin/%3bpage但是会在request进入应用后进行一次URL解码变为/admin/;page
      2. 然后再经过removeSemicolon函数处理后去掉;和它后面的内容,变为/admin/然后交给normalize处理...出现的路径问题, 最后还是得到/admin/返回给PathMatchingFilterChainResolver#getChain执行这里讲解的过滤器匹配步骤

    image-20220406025425477

  4. 将获得的pattDirs逐个和pathDirs进行匹配

    URL获得:
    /admin/	=>		pathDirs = ["admin"]
    过滤器获得:
    pattern1 = "/login"		=>		pattDirs1 = ["login"]
    pattern2 = "/admin/*"	=>		pattDirs2 = ["admin","*"]
    

    这时候我们就已经获得了全部的需要进行匹配的内容了

    接下来就是通过层层的间接调用最后在org.apache.shiro.util.AntPathMatcher#doMatch逐个将过滤器的匹配数组pattDirsURL的匹配数组pathDirs按照一定的规则匹配

下面我们继续回到while(!this.pathMatches(pathPattern, requestURI));语句详细查看的调用的过程和执行的代码:

image-20220406193335263

this.pathMatches的代码:

下面这里是简单的调用而已, 真正的匹配函数在org.apache.shiro.util.AntPathMatcher#doMatch

org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#pathMatches
    protected boolean pathMatches(String pattern, String path) {
        PatternMatcher pathMatcher = this.getPathMatcher();
        return pathMatcher.matches(pattern, path);
    }
org.apache.shiro.util.AntPathMatcher#matches
    public boolean matches(String pattern, String source) {
        return this.match(pattern, source);
    }
org.apache.shiro.util.AntPathMatcher#match
    public boolean match(String pattern, String path) {
        return this.doMatch(pattern, path, true);
    }
org.apache.shiro.util.AntPathMatcher#doMatch
    太长了放下面吧

org.apache.shiro.util.AntPathMatcher#doMatch

    注:
	this.pathSeparator = "/"
    boolean fullMatch = true
  ------------------------------------------------------------------------------  
        protected boolean doMatch(String pattern, String path, boolean fullMatch) {
        if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
            return false;
        } else {
            String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator);
            String[] pathDirs = StringUtils.tokenizeToStringArray(path, this.pathSeparator);
            int pattIdxStart = 0;
            int pattIdxEnd = pattDirs.length - 1;
            int pathIdxStart = 0;

            int pathIdxEnd;
            String patDir;
            for(pathIdxEnd = pathDirs.length - 1; pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd; ++pathIdxStart) {
                patDir = pattDirs[pattIdxStart];
                if ("**".equals(patDir)) {
                    break;
                }

                if (!this.matchStrings(patDir, pathDirs[pathIdxStart])) {
                    return false;
                }

                ++pattIdxStart;
            }

            int patIdxTmp;
            if (pathIdxStart > pathIdxEnd) {
                if (pattIdxStart > pattIdxEnd) {
                    return pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) : !path.endsWith(this.pathSeparator);
                } else if (!fullMatch) {
                    return true;
                } else if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {
                    return true;
                } else {
                    for(patIdxTmp = pattIdxStart; patIdxTmp <= pattIdxEnd; ++patIdxTmp) {
                        if (!pattDirs[patIdxTmp].equals("**")) {
                            return false;
                        }
                    }

                    return true;
                }
            } else if (pattIdxStart > pattIdxEnd) {
                return false;
            } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
                return true;
            } else {
                while(pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
                    patDir = pattDirs[pattIdxEnd];
                    if (patDir.equals("**")) {
                        break;
                    }

                    if (!this.matchStrings(patDir, pathDirs[pathIdxEnd])) {
                        return false;
                    }

                    --pattIdxEnd;
                    --pathIdxEnd;
                }

                if (pathIdxStart > pathIdxEnd) {
                    for(patIdxTmp = pattIdxStart; patIdxTmp <= pattIdxEnd; ++patIdxTmp) {
                        if (!pattDirs[patIdxTmp].equals("**")) {
                            return false;
                        }
                    }

                    return true;
                } else {
				......还有很多循环判断,操作和上面基本一样, 代码太多就不放出来了, 在这里只执行到上面的return false语句

shiro url 匹配过滤器

嗯,,,,这里这部分首先是进行"/doLogin"的匹配判断但是这个不是终点所以我们在org.apache.shiro.util.AntPathMatcher#doMatch打一个断点直接快进到下一次执行这个函数的时候(也就是传入/admin/*进行匹配的时候)

org.apache.shiro.util.AntPathMatcher#doMatch。 这里把我们定义的ant风格的path和请求的path都以/进行拆分存入数组,pattDirspathDirs

image-20220406013833209

pattern最后一位是*,于是返回false

这是执行了第一次for循环(传入admin就行匹配)之后的状态:

pathIdxStart => 初始为0,后面每判断一个成员后就会+1
pathIdxEnd	 => 我们的url经过/分割后的数组成员个数-1
实际上pathDirs[pathIdxStart]就是我们下一个需要判断的目录名或文件名
而pathIdxEnd就用于判断是否以及匹配完了pathDirs的全部内容

image-20220406014318228

执行完第一遍之后我们可以看到因为存放我们的rl地址的分割数组pathDirs只有一个变量所以只能比较一次,第二次的时候pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd条件就为false了所以结束for循环

image-20220406014753777

在这里这个for循环我们需要注意的是判断**的if语句:

  • image-20220406014855429如果过滤器的匹配格式结果/分割后的数组中如果带有**的话就会直接停止for循环直接继续执行后面的匹配机制

但是实际上后面的内容几乎就在重复干了几遍刚才所做的匹配工作

我们在这里就只分析到return 的点, 后面还有一堆重复判断感兴趣的可以去看一下(说不定又能爆出一个点)

image-20220406020558290

我们说一下走过的几语句:

  1. 判断我们的FIlter匹配规则的分割数组pattDirs是不是已经全都匹配完了(这里并没有),因为我们的url分割后的数组只有["admin"]一个成员但是过滤器匹配规则分割后的数组有两个成员["admin" , "*"]

    if (pathIdxStart > pathIdxEnd) {
        return pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) : !path.endsWith(this.pathSeparator);
    }
    
  2. fullMatch是传入的最后一个参数, 表示的意思应该是全匹配,这里传入的是true

    else if (!fullMatch) {
    	return true;
    }	
    
  3. pattIdxStart == pattIdxEnd表示过滤器匹配的分割数组是否已经匹配到最后一个, 满足

    pattDirs[pattIdxStart].equals("*")表示在前一个条件的前提下匹配项为0(也就是以*结尾), 满足

    path.endsWith(this.pathSeparator)因为this.pathSeparator = "/" 所以意思是我们的url以/结尾, 我们不满足

    else if (
        pattIdxStart == pattIdxEnd &&
        pattDirs[pattIdxStart].equals("*") &&
        path.endsWith(this.pathSeparator)) 
    {
                        return true;
    }
    
  4. patIdxTmp = pattIdxStart 赋值语句

    patIdxTmp <= pattIdxEnd 判断当前过滤器匹配的数组是否以全部匹配完,如果patt[patIdxTmp]没越界那就成立, 我们这里是满足条件的,所以进入for循环并且当前的pattIdxStart = 1

    !pattDirs[patIdxTmp].equals("**") 判断当前的过滤器匹配项是否为"**"不是是的话就return false, 我们的patt[patIdxTmp] = "*" 所以执行return false

    else {
    	for(patIdxTmp = pattIdxStart; patIdxTmp <= pattIdxEnd; ++patIdxTmp) {
    		if (!pattDirs[patIdxTmp].equals("**")) {
    			return false;
    		}
    	}
    	return true;
    }
    

回到PathMatchingFilterChainResolver#getChain

image-20220406193827375

刚刚我们的return false就交给了开始的PathMatchingFilterChainResolver#getChain语句中了,我们再回来看一下

while语句:

        do {
            if (!var6.hasNext()) {
                return null;
            }

            pathPattern = (String)var6.next();
            if (pathPattern != null && !"/".equals(pathPattern) && pathPattern.endsWith("/")) {
                pathPattern = pathPattern.substring(0, pathPattern.length() - 1);
            }
        } while(!this.pathMatches(pathPattern, requestURI));

现在已经是第二个过滤器的检测了, 因为我们的全部过滤器都已经检测完了所以再次进入do结构中,

var6.hasNext()就是判断还有没有下一个过滤器, 很明显到这里就没有了无异我们直接return null

知道这意味着什么嘛:> ?!!!

这意味着我们现在使用的是默认的过滤器访问/admin下面的资源, 这代表什么:>?

这代表我们不需要身份验证!!!!

所以此时我们就已经完成了未授权的访问!!!

spring url 处理

到这一步我们已经通过使用%3b和ANT格式的匹配符*绕过了过滤器的匹配

现在我们使用默认不需要身份验证的过滤器访问/admin资源

这里的资源访问过程就已经不属于Shiro的Filter截断而是Serverlet.service阶段了,这个阶段的业务是通过Spring处理的,所以就是说我们现在就是将原请求URL /admin/%3bpage交给Spring,让它帮我们获取对应的资源然后返回给浏览器

但是问题是我们想要访问的是/admin/page 而不是 /admin/;page

那么我们接下来就看看Spring是怎么对/admin/%3bpage这个URL路径进行处理的,处理后又是怎么去记载资源的

spring 获取路径映射

实际上我们需要知道一点:

我们需要访问的资源不应该是/admin/page而是/admin/* 因为在我们这个程序里面/admin/page/admin/xxxx获得的资源都是一样的,最后都是会返回"admin page"

spring利用DispatcherServlet来分派请求,他的一个主要作用就是通过HandlerMapping,将请求映射到处理器。在处理过程中会调用getHandler方法。

在这里因为我们对/admin/{name}进行了路由代理的处理, 所以最终因为/admin/;page满足其ANT格式/admin/*所以执行这里的代理函数org.syclover.srpingbootshiro.LoginController#admin并返回"admin page"给浏览器

image-20220406193939050

image-20220406194421553

此外做了点小测试, 负责处理去掉URL的;和它后面内容的removeSemicolon函数

image-20220406195910445

​ 但是如果我们后面还有子目录的哈就不行了, 会返回错误页面, 但是我们获取进行过滤器匹配的还是/admin/

image-20220406200750973

image-20220406200413075

​ 这里应该就涉及到我们在上面的org.apache.shiro.util.AntPathMatcher#doMatch函数里面省略了的后面代码的操作了, 在这里就不展开说了, 感兴趣的小伙伴可以自己打断点调试一下也当做是检查一下理解这篇文章没有hhh

一点点小拓展

给一些Servlect的实现类

image-20220406043558708

下面是我在调试过程中查看在哪里返回响应包使用的一些关键的断点, 感兴趣的可以看一下

org.apache.shiro.web.servlet.AbstractShiroFilter#executeChain
javax.servlet.FilterChain#doFilter
org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
javax.servlet.Servlet#service
org.springframework.web.servlet.FrameworkServlet#service
org.springframework.web.servlet.FrameworkServlet#doGet
org.springframework.web.cors.CorsUtils#isPreFlightRequest
org.springframework.web.method.support.InvocableHandlerMethod#doInvoke
java.lang.reflect.Method#invoke
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#setResponseStatus
org.springframework.web.method.HandlerMethod#getResponseStatus
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle

最后在org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle函数里面执行下面代码后返回响应包

invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);

image-20220406201507174

嗯,,,太菜了,文中有挺多地方是自己的理解,所以可能会有错误的地方,大佬有发现有误的话希望指出。

参考文章:

https://cert.360.cn/report/detail?id=0a56bda5f00172dd642f2b436ed49cc7

https://www.cnblogs.com/backlion/p/14055278.html

posted @ 2022-04-25 12:49  h0cksr  阅读(215)  评论(0编辑  收藏  举报