Shiro 权限绕过漏洞分析(CVE--1957)

作者:Spoock

博客:https://blog.spoock.com/2020/05/09/cve-2020-1957/

本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:paper@seebug.org

环境搭建

根据 Spring Boot 整合 Shiro ,两种方式全总结!。我配置的权限如下所示:

@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("/admin/**", "authc");

bean.setFilterChainDefinitionMap(map);

return bean;

}

........

@RequestMapping("/admin/index")

public String test() {

return "This is admin index page";

}

会对admin所有的页面都会进行权限校验。测试结果如下:

访问index

访问admin/index

漏洞分析

绕过演示

在shiro的1.5.1及其之前的版本都可以完美地绕过权限检验,如下所示;

绕过原理分析

我们需要分析我们请求的URL在整个项目的传入传递过程。在使用了shiro的项目中,是我们请求的URL(URL1),进过shiro权限检验(URL2), 最后到springboot项目找到路由来处理(URL3)

漏洞的出现就在URL1,URL2和URL3 有可能不是同一个URL,这就导致我们能绕过shiro的校验,直接访问后端需要首选的URL。本例中的漏洞就是因为这个原因产生的。

http://localhost:8080/xxxx/..;/admin/index 为例,一步步分析整个流程中的请求过程。

protected String getPathWithinApplication(ServletRequest request) {

return WebUtils.getPathWithinApplication(WebUtils.toHttp(request));

}

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;

}

}

public static String getRequestUri(HttpServletRequest request) {

String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE);

if (uri == null) {

uri = request.getRequestURI();

}

return normalize(decodeAndCleanUriString(request, uri));

}

此时的URL还是我们传入的原始URL: /xxxx/..;/admin/index

接着,程序会进入到decodeAndCleanUriString(), 得到:

private static String decodeAndCleanUriString(HttpServletRequest request, String uri) {

uri = decodeRequestString(request, uri);

int semicolonIndex = uri.indexOf(';');

return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri);

}

decodeAndCleanUriString 以 ;截断后面的请求,所以此时返回的就是 /xxxx/...然后程序调用normalize() 对decodeAndCleanUriString()处理得到的路径进行标准化处理. 标准话的处理包括:

  • 替换反斜线
  • 替换 ///
  • 替换 /.//
  • 替换 /..//

都是一些很常见的标准化方法.

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);

}

经过getPathWithinApplication()函数的处理,最终shiro 需要校验的URL 就是 /xxxx/... 最终会进入到 org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver 中的 getChain()方法会URL校验. 关键的校验方法如下:

由于 /xxxx/.. 并不会匹配到 /admin/**, 所以shiro权限校验就会通过.

最终我们的原始请求 /xxxx/..;/admin/index 就会进入到 springboot中. springboot对于每一个进入的request请求也会有自己的处理方式,找到自己所对应的mapping. 具体的匹配方式是在:org.springframework.web.util.UrlPathHelper 中的 getPathWithinServletMapping()

getPathWithinServletMapping() 在一般情况下返回的就是 servletPath, 所以本例中返回的就是 /admin/index.最终到了/admin/index 对应的requestMapping, 如此就成功地访问了后台请求.

最后,我们来数理一下整个请求过程:

  1. 客户端请求URL: /xxxx/..;/admin/index
  2. shrio 内部处理得到校验URL为 /xxxx/..,校验通过
  3. springboot 处理 /xxxx/..;/admin/index , 最终请求 /admin/index, 成功访问了后台请求.

commmit分析

对应与修复的commit是: Add tests for WebUtils

其中关键的修复代码如下;

对比与1.5.1的版本获取request.getRequestURI(), 在此基础上,对其进行标准化,分析, 由于 getRequestURI是直接返回请求URL,导致了可以被绕过.

在1.5.2的版本中是由contextPath()+ servletPath()+ pathinfo()组合而成. 以 /xxxx/..;/admin/index为例, ,修正后的URL是:

经过修改后.shiro处理的URL就是 /admin/index, 发现需要进行权限校验,因此不就会放行.

其他

偶然发现 这样也可以绕过shiro的权限校验, 但是这种情况和上面的情况是不一样的. 上面的情况是shiro校验的URL和最终进入到springboot中需要处理的URL是不一样的.

增加一个路由

@RequestMapping("/admin")

public String test2() {

return "This is the default admi controller";

}

在这种情况下,可以访问到/admin这样的路由. 但仅此而已, 并不访问访问更多/admin下方更多的路由. 接下来分析这种原因.按照前面的一贯分析, 我们同样可以知道 在 org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver() 中的getChain()是可以通过检验的. 因为 /admin.index 不属于/admin/**

在springboot中需要通过request找到对应的handler进行处理. springboot是在 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping 这个函数中,通过 lookupPath找到对应的handler.

通过上述的截图也可以看出, springboot获取的也是 /admin.index 这个URL. 但是可以成功地找到handler来处理.所以本质上 这个 /admin.index路由可以绕过 shiro 是springboot内部通过URL找到handler的一个机制.与shiro并没有关系. 我们进行一个简单的测试:

@RequestMapping("/index")

public String index() {

return "This is homepage";

}

完全没有使用shiro, 大家也可以测试下.所以这个问题其实在shiro 1.5.2 上面也同样是可以的.

上面的测试只是一种最简单的情况, 只有shiro配置了一个全局的权限校验, 就有可能存在绕过的问题, 如果程序进一步在URL上面配置了权限校验,即使绕过了ShiroFilterChainDefinition, 但是还是无法绕过注解上面的防御.如下所示:

@Bean

public ShiroFilterChainDefinition shiroFilterChainDefinition() {

DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();

//哪些请求可以匿名访问

chain.addPathDefinition("/user/login", "anon");

chain.addPathDefinition("/page/401", "anon");

chain.addPathDefinition("/page/403", "anon");

chain.addPathDefinition("/t5/hello", "anon");

chain.addPathDefinition("/t5/guest", "anon");

//除了以上的请求外,其它请求都需要登录

chain.addPathDefinition("/**", "authc");

return chain;

}

@RestController

@RequestMapping("/t5")

public class Test5Controller {

@RequiresUser

@GetMapping("/user")

public String user() {

return "@RequiresUser";

}

}

总结

讲到这里,差不多有关这个漏洞的所有问题都说完了.其实本文章还涉及到一些其他的知识.比如:

  1. requesturi 和 servlet的区别
  2. springmvc的请求处理流程

这些都可以写一篇文章来进行说明了.整体来说,这个漏的利用方式还是很简单的,我测试了目前大部分使用shiro的应用基本上都存在绕过的问题, 但是这个漏洞能够找成多大的危害呢? 就目前看来危害还是有限的,因为即使绕过了shiro的权限校验,但是一般情况下这些接口/请求都需要对应用户的权限,所以绕过了shiro登录到后台系统只是以一种没有用户身份的方式登录到后台系统, 后台校验此时获取当前用户信息,发现为空.此时整体系统就会出错,或者重新跳转到登录页面,重新登录.这里就不作说明了

以上是 Shiro 权限绕过漏洞分析(CVE--1957) 的全部内容, 来源链接: utcz.com/p/199586.html

回到顶部