从CVE-2020-1957浅看Filter设计细节

  • A+

关于CVE-2020-1957

漏洞详情

  前段时间Shiro发了个新的漏洞:

图片.png
  漏洞信息大概是,当Spring框架的动态Controller与Shiro结合使用时,通过构造特殊的请求可以绕过相关的权限认证。
  查看官方commithttps://github.com/apache/shiro/commit/9762f97926ba99ac0d958e088cae3be8b657948d,在commit中寻找相关漏洞信息:

图片.png
  可以看到,path-match-bug-fix主要描述的是Spring web在匹配url接口的时候会容错后面额外的/,而Shiro会无法匹配从而导致绕过权限控制。

漏洞复现

  简单进行下复现:
  使用springboot+Shiro进行漏洞验证,Shiro版本为1.2.4:
xml
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.4</version>
</dependency>

  简单的进行权限配置,/hello接口需要防止未授权访问,需要在登陆后才可以访问:
java
ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//指定 SecurityManager
bean.setSecurityManager(securityManager());
//登录页面
bean.setLoginUrl("/login");
//登录成功页面
bean.setSuccessUrl("/index");
//访问未获授权路径时跳转的页面
bean.setUnauthorizedUrl("/unauthorizedurl");
//配置路径拦截规则,注意,要有序
Map<String, String> map = new LinkedHashMap<>();
map.put("/doLogin", "anon");
map.put("/hello", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}

  在controller中进行路径映射,正常访问/hello接口时在页面打印hello:
java
@GetMapping("/hello")
public String hello() {
return "hello";
}

  具体效果如下:直接访问/hello接口返回登陆页面(未授权访问):

图片.png
  访问/hello/成功绕过权限控制,达到未授权访问的效果:

图片.png
  因为这里是使用单个接口匹配的,但是如果使用全量匹配的话就比较鸡肋了:
java
map.put("/**","authc");

  刚刚的方式就没法绕过权限控制了:

图片.png

Shiro中URI接口获取的具体实现

  乍一看上面这种利用方式在Shiro框架中还是比较鸡肋的,因为一般实际配置的时候,不论是未授权还是垂直越权,一般都会根据目录进行全量匹配:
java
map.put("/**","authc");
map.put("/admin/**","authc,perms[admin:view]");

  Shiro的三大核心组件是Subject、SecurityManager和Realms。 Shiro相关的鉴权也是通过类似Filter Chain(过滤器链)方式实现了,内部提供了一个PathMatchingFilterChainResolver进行路径匹配。
  权限控制不外乎就是对每一个接口(通俗来说就是我们的URI/URL)进行业务梳理,然后判断当前URI/URL是否具有相应的业务权限。所以这里简单的看一下Shiro是怎么进行路径获取的。
  在shiro/web/src/main/java/org/apache/shiro/web/filter/PathMatchingFilter.java可以看到,在pathsMatch方法中其是通过将request对象传入到getPathWithinApplication方法进行获取的:

图片.png
  查看该方法的定义,主要是通过调用WebUtils工具类的getPathWithinApplication方法:
java
import org.apache.shiro.web.util.WebUtils;
......
/**
* Returns the context path within the application based on the specified <code>request</code>.
* <p/>
* This implementation merely delegates to
* {@link WebUtils#getPathWithinApplication(javax.servlet.http.HttpServletRequest) WebUtils.getPathWithinApplication(request)},
* but can be overridden by subclasses for custom logic.
*
* @param request the incoming <code>ServletRequest</code>
* @return the context path within the application.
*/
protected String getPathWithinApplication(ServletRequest request) {
return WebUtils.getPathWithinApplication(WebUtils.toHttp(request));
}
......

  继续跟进WebUtils.getPathWithinApplication(request)的具体实现:

图片.png
  getContextPath()应该是得到项目的虚拟路径,获取接口URI主要还是在getRequestUri()方法上。进一步查看getRequestUri()方法是怎么获取URI接口的:

图片.png
  首先是通过request.getAttribute(INCLUDE_REQUEST_URI_
ATTRIBUTE)进行获取,INCLUDE_REQUEST_URI_ATTRIBUTE是一个全局变量:
java
public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";

  这个应该是拦截页面中include方式(类似
)的请求,并取得其请求的URL。
  主要的业务接口获取代码如下:
java
uri = valueOrEmpty(request.getContextPath()) + "/" +
valueOrEmpty(request.getServletPath()) +
valueOrEmpty(request.getPathInfo());

  这里在并没有使用request.getRequestURL()和request.getRequestURI()这两个不安全方法(之前社区有一篇文章《过滤器设计缺陷导致权限绕过》讲过这两个方法在没有规范化的情况下存在权限绕过的问题)。主要是通过request对象的获取结果进行拼接,然后得到请求的URI接口,最后在normalize和decodeAndCleanUriString进行URI规范化处理。
  decodeAndCleanUriString方法主要是解决URL编码以及分隔符;的处理:

图片.png
  normalize方法主要是对../这类的非标准化内容进行处理:
```java
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);

}

&emsp;&emsp;到这里还是没有对结尾额外的/进行对应的处理,主要处理额外的/在最后路径解析的PathMatchingFilterChainResolver.java这里:
![图片.png](/img/sin/M00/00/19/wKg0C16AgpKAI5AwAAAVr3IBang228.png)
java
private static final String DEFAULT_PATH_SEPARATOR = "/";
```

图片.png
  这里整个Shiro简单的一个接口URI获取流程就出来了:
  1.通过request.getServletPath()和request.getPathInfo()方法获取接口URI;
  2.对获取到的接口URI进行规范化处理,例如URL解码,去除../,分隔符;等内容;
  3.最后在路径解析时候去除掉接口URI后面额外的/;

Filter设计细节

绕过权限控制

  前面提到当Spring框架的动态Controller与Shiro结合使用时,通过在接口后尝试追加额外的/可以绕过相关的权限认证这种方式在Shiro场景下比较鸡肋。
  因为Shiro部分鉴权也是通过filter实现的,通过简单的分析ShiroURI接口获取的过程,其实还是基于servlet的request对象的方法进行操作,类比其实在我们自定义的filter中也是存在的,并且场景比较广泛。
  一般情况下,通常是获取到当前URI/URL,然后跟需要鉴权的接口进行比对,结合endsWith()方法,设置对应的校验名单。
  例如下面的过滤器实现,所有.do、.action结尾的接口均需要做登陆检查,防止未授权访问,同样的我们获取接口URI的方法也是基于servlet的request对象的方法进行操作:
java
String uri = request.getServletPath()+(request.getPathInfo() == null ? "" : request.getPathInfo());
if(uri.endsWith(".do")||uri.endsWith(".action")) {
//检测当前用户是否登陆
User user =(User) request.getSession().getAttribute("user");
if(user==null|| "".equals(user)) {
errorResponse(response, paramN, "未授权访问");
return;
}
}

  这里为了防止如下的一些bypass场景,使用了 request.getServletPath()+(request.getPathInfo() == null ? "" : request.getPathInfo());进行路径获取。
* 在URI引入参数分隔符;,进行切割URI绕过限制,例如/system;Bypass/UserSearch.do;Bypass
* 在URI引入类似/login(白名单接口,可以通过测试得出,一般登陆都是不需要权限校验的)/../的样式,伪造白名单接口
* 对URI进行URL编码/多重URL编码,尝试绕过。

  但是这里跟之前的Shiro一样,没有对额外的/进行处理,前面也提到了,特定情况下,Spring web在匹配url接口的时候会容错后面额外的/。

  以Spring MVC为例,配置过滤器拦截所有接口:
xml
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

  按照上面的说法,以下两种访问方式效果都是一样的:
/admin/info.do?param=value
/admin/info.do/?param=value
  引用上面的自定义filter,对所有.do、.action结尾的接口均需要做登陆检查,防止未授权访问,request.getServletPath()和request.getPathInfo方法如果在接口URI后追加额外的/,还是可以获取到的额外的/的:

图片.png
  根据上面的filter代码,理想情况下非登陆访问/admin/info.do,会提示未授权访问:

图片.png
  尝试在接口后追加额外的/,成功绕过权限校验:

图片.png
  也就是说在Spring Web的动态Controller场景下,尝试对应的URL接口后加入/,可能可以绕过权限控制这个缺陷是比较通用的。
  从CVE-2020-1957的缺陷类比到实际开发中的自定义filter,还是比较值得思考的,尤其是在使用一些startWith和endwith方法进行接口限定鉴权时,如何对filter获取的URI路径进行规范化还是比较值得思考的一个问题。Shiro中的normalize方法也是值得借鉴使用的。

相关推荐: 深入分析一个有趣的SSRF漏洞

关于SSRF漏洞的文章,读者也许已经读过很多了,例如,有的SSRF漏洞甚至能够升级为RCE漏洞——尽管这是大部分攻击者的终极目标,但本文介绍的重点,却是SSRF经常被忽略的影响:对于应用程序逻辑的影响。 在这篇文章中,我们将为您讲述一个无需身份验证的SSRF漏…