作者:Spoock
博客:https://blog.spoock.com/2020/05/09/cve-2020-1957/
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]
根据 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, 如此就成功地访问了后台请求.
最后,我们来数理一下整个请求过程:
/xxxx/..;/admin/index
/xxxx/..,
校验通过/xxxx/..;/admin/index
, 最终请求 /admin/index
, 成功访问了后台请求.对应与修复的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"; } }
讲到这里,差不多有关这个漏洞的所有问题都说完了.其实本文章还涉及到一些其他的知识.比如:
这些都可以写一篇文章来进行说明了.整体来说,这个漏的利用方式还是很简单的,我测试了目前大部分使用shiro的应用基本上都存在绕过的问题, 但是这个漏洞能够找成多大的危害呢? 就目前看来危害还是有限的,因为即使绕过了shiro的权限校验,但是一般情况下这些接口/请求都需要对应用户的权限,所以绕过了shiro登录到后台系统只是以一种没有用户身份的方式登录到后台系统, 后台校验此时获取当前用户信息,发现为空.此时整体系统就会出错,或者重新跳转到登录页面,重新登录.这里就不作说明了
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1196/