加载中...

CVE-2020-13933 漏洞分析


0x10 漏洞背景

Apache Shiro 是一个强大且易用的 Java 安全框架,执行身份验证、授权、密码和会话管理,通常会和 Spring 等框架一起搭配使用来开发 web 应用(请求在到达 Spring 之前,会先经过 Shiro 认证身份)。

因为某个特定测试场景,Shiro 被白帽子发现了 CVE-2020-11989 身份校验绕过漏洞,漏洞原因在于 Shiro 处理 url 的逻辑与 spring 存在差异。

对此官方发布了 1.5.3 版本进行修复,但是因为修复补丁存在缺陷, 再次导致了 CVE-2020-13933 身份校验绕过漏洞,官方不得不再次发布了 1.6.0 修复版本。

0x20 漏洞靶场

0x21 靶场搭建

环境要求:

  • JDK: 1.8
  • spring-boot: 2.3.3
  • apache-shiro: 1.5.3 (< 1.6.0)

靶场源码(Maven): https://github.com/EXP-Docs/CVE-2020-13933

靶场关键代码说明:

  • ShiroConfig.java: 权限配置, 当请求 /res/* 资源时, 302 跳转到登陆页面进行身份认证
  • NameController.java
      □ /res/{name}: 请求名为 name 的的资源(触发身份认证)
      □ /res/: 不请求任何资源(不触发身份认证)

此靶场已收录到 Vulmon 漏洞情报搜索引擎

0x22 靶场验证

不在请求路由中指定资源名称时,不触发身份验证,也无资源返回: http://127.0.0.1:8080/res/

在请求路由中指定资源名称时,302 跳转到身份验证页面: http://127.0.0.1:8080/res/poc

构造特定 PoC 请求指定资源时,不触发身份验证,并返回资源: http://127.0.0.1:8080/res/%3bpoc%3b; 的 URL 编码)

0x30 漏洞分析

0x31 前置漏洞回顾

通过漏洞背景可知, CVE-2020-13933 是 CVE-2020-11989 的修复补丁不当产生的,因此有必要了解 CVE-2020-11989 的漏洞原因、以及官方补丁的改动位置。

通过查阅 CVE-2020-11989 现有的漏洞分析报告, 可知 Shiro 为了处理请求路由中形如 ;jsessionid=C7E5ED51DCE6B918FD88AE5575C589B6 的参数,会截断 ; 后的内容。

对于 Shiro < 1.5.3 的版本,因为相关逻辑缺陷,若刻意构造路由类似 /;/site/res/name 的请求,会因为匹配错误导致请求路由截断成 /,此时因为没有指定要请求的资源路由,故不会触发 Shiro 的身份认证逻辑。

因为不需要认证身份,请求 /;/site/res/name 被直接送到 Spring 获取资源,经过 Spring 格式化后(截取根目录后的资源路径),请求路由截断成 /res/name,最终可以成功获取到资源页面,导致 Shiro 权限绕过。

为了修复这个漏洞, Shiro 发布了 1.5.3 版本,可以对比下官方修复的代码位置:

【修改前】 Shiro 1.5.2:

public static String getPathWithinApplication(HttpServletRequest request) {
    String contextPath = getContextPath(request);
    String requestUri = getRequestUri(request);
    if (StringUtils.startsWithIgnoreCase(requestUri, contextPath)) {
        // Normal case: URI contains context path.
        String path = requestUri.substring(contextPath.length());
        return (StringUtils.hasText(path) ? path : "/");
    } else {
        // Special case: rather unusual.
        return requestUri;
    }
}

【修改后】 Shiro 1.5.3:

public static String getPathWithinApplication(HttpServletRequest request) {
    return normalize(removeSemicolon(getServletPath(request) + getPathInfo(request)));
}

0x32 当前漏洞分析

由于官方更新 Shiro 1.5.3 版本后,身份绕过漏洞依旧存在,说明这次代码修复的逻辑仍有不完善的地方。

为了分析漏洞成因,可以先搭建上述的靶场,然后在 Shiro 1.5.3 的官方修复代码打断点 DEBUG。

不难发现,修复代码位置在 Shiro 处理请求路由的入口处 WebUtils.java。为了便于分析,这里对代码做了逐行拆解:

// 输入请求路由: http://127.0.0.1:8080/res/%3bpoc

public static String getPathWithinApplication(HttpServletRequest request) {
    String servletPath = getServletPath(request);   // servletPath = "/res/;poc" (URL 已解码)
    String pathInfo = getPathInfo(request);         // pathInfo = ""
    String path = servletPath + pathInfo;           // path = "/res/;poc"
    path = removeSemicolon(path);                   // path = "/res/"  (截断 ; 后面的内容)
    path = normalize(path);                         // path = "/res/"  (标准化 URL: 处理双斜杠、路径穿越)
    return path;                                    // path = "/res/"
//  return normalize(removeSemicolon(getServletPath(request) + getPathInfo(request)));
}

很明显,该 PoC 通过 Shiro 处理后, 请求路由 /res/%3bpoc 会变成 /res/, 根据前面靶场的验证结果,因为此路由没有指定资源名称,因此不会触发身份认证,请求会直接送到 Spring 的相关逻辑处理以获取资源。

此时再 DEBUG 看看 Spring 的处理请求路由的入口代码 UrlPathHelper.java 是如何做的:

// 输入请求路由: http://127.0.0.1:8080/res/%3bpoc

private String decodeAndCleanUriString(HttpServletRequest request, String uri) {    // uri = "/res/%3bpoc" (URL 未解码)
    uri = removeSemicolonContent(uri);          // uri = "/res/%3bpoc"  (截断 ; 后面的内容)
    uri = decodeRequestString(request, uri);    // uri = "/res/;poc"    (URL 字符解码)
    uri = getSanitizedPath(uri);                // uri = "/res/;poc"    (标准化 URL: 处理双斜杠)
    return uri;
}

其实在这里已经可以发现,因为 Shiro 和 Spring 对 URL 的处理逻辑顺序不一致,导致两者得到的路由不同。

不妨根据这两段代码的逻辑,横向对比输入不同 URL 的情况下最终结果有什么差异。

  1. 当输入为 http://127.0.0.1:8080/res/%3bpoc 时:
Shiro 逻辑 路由 Spring 逻辑 路由
输入 /res/%3bpoc 输入 /res/%3bpoc
URL 解码 /res/;poc 截断 ; /res/%3bpoc
截断 ; /res/ URL 解码 /res/;poc
URL 标准化 /res/ URL 标准化 /res/;poc
资源名 无资源名 资源名 ;poc
结果 不触发验证 结果 返回资源页面
  1. 当输入为 http://127.0.0.1:8080/res/;poc 时:
Shiro 逻辑 路由 Spring 逻辑 路由
输入 /res/;poc 输入 /res/;poc
URL 解码 /res/;poc 截断 ; /res/
截断 ; /res/ URL 解码 /res/
URL 标准化 /res/ URL 标准化 /res/
资源名 无资源名 资源名 无资源名
结果 不触发验证 结果 无资源返回
  1. 当输入为 http://127.0.0.1:8080/res/poc 时:
Shiro 逻辑 路由 Spring 逻辑 路由
输入 /res/poc 输入 /res/poc
URL 解码 /res/poc 截断 ; /res/poc
截断 ; /res/poc URL 解码 /res/poc
URL 标准化 /res/poc URL 标准化 /res/poc
资源名 poc 资源名 poc
结果 302 跳转身份认证 结果 返回资源页面
  1. 当输入为 http://127.0.0.1:8080/res/ 时:
Shiro 逻辑 路由 Spring 逻辑 路由
输入 /res/ 输入 /res/
URL 解码 /res/ 截断 ; /res/
截断 ; /res/ URL 解码 /res/
URL 标准化 /res/ URL 标准化 /res/
资源名 无资源名 资源名 无资源名
结果 不触发验证 结果 无资源返回

0x33 分析结论

显然除了第 1 种输入,其它 3 种输入的最终输出碰巧都是一致的,而第 1 种输入正是此漏洞的 PoC 。

造成这种情况的根本原因在于 Shiro 和 Spring 在【URL 截断】和【URL 解码】的操作顺序不一致。

回顾 URL 的处理过程,总结下来就是:

  • URL 进入到 Shiro 时,因为先做 URL 编码 %3b => ; ,使得路由中的资源名被截断删除
  • Shiro 误以为请求路由中无资源名,故该次请求不触发身份认证逻辑
  • 因不需要身份认证,请求路由被直接转发到 Spring
  • Spring 从请求路由中正确截取资源名称,并返回资源页面
  • 最终导致 Shiro 身份认证被绕过

由于 Shiro 的身份认证是通过判断 URL 匹配来实现的,只要能找到 Shiro 处理 URL 逻辑与 Spring 框架处理 URL 逻辑不一致的地方,都有可能造成身份认证绕过。

0x40 漏洞利用

对于这个漏洞,PoC 亦是 EXP ,只需要把 %3b 放在请求资源路由的最前面,即可绕过认证实现资源访问。

例如: http://127.0.0.1:8080/res/%3b{资源名}res 属于站点根目录, 不属于资源路由的一部分)

当资源路由为后台路径时,攻击者即可绕过认证访问到后台资源。

0x50 漏洞修复

从漏洞分析过程可知,只需要调整 Shiro 处理逻辑顺序,使其与 Spring 保持一致即可修复此漏洞。

但是可能考虑到相同的漏洞已经出现连续了 2 次,所以官方使用了一种更灵活的修复方式:

不难发现官方补丁 Shiro 1.6.0 并没有修复 Shiro 1.5.3 中有关 WebUtils 的问题代码。

取而代之的是引入了责任链模式,增加了针对 URL 的预处理过滤器 InvalidRequestFilter.java,使得 URL 在到进入 Shiro 处理逻辑之前,就通过过滤器对分号、反斜杠和非 ASCII 字符预先进行过滤。尔后若这段修复代码再出现类似漏洞 ,要修复则简单得多。

0x60 挖掘思路

前面已经说过,由于 Shiro 的权限校验是通过判断 url 匹配来实现的,因此一般的挖掘思路是: 如果能找到 Shiro 处理 URL 逻辑与 web 框架处理 URL 逻辑不一致的地方,那么就有可能利用它进行权限绕过。

但说到底,此漏洞是某个白帽子在做渗透测试试题时偶尔发现的,否则除非是深谙 Shiro 和相关 web 框架的源码、知道两者处理 URL 逻辑的不一致,不然要在正常情况下发现这个漏洞几无可能。

在不熟悉底层代码逻辑的前提下,可以留意所关注框架的 CVE 修复补丁,通过确认修复内容,推理修复逻辑是否完整合理,也不失是一种捷径。这个漏洞也是这样被发现的,可以说这个漏洞的发现,偶然中带着必然。

0x70 参考资料


文章作者: EXP
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 EXP !
  目录