环境搭建
根据 Spring Boot 整合 Shiro ,两种方式全总结!。我配置的权限如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}
........
"/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
为例,一步步分析整个流程中的请求过程。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25protected 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(), 得到:1
2
3
4
5private 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()处理得到的路径进行标准化处理. 标准话的处理包括:
- 替换反斜线
- 替换
//
为/
- 替换
/./
为/
- 替换
/../
为/
都是一些很常见的标准化方法.
1 | private static String normalize(String path, boolean replaceBackSlash) { |
经过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, 如此就成功地访问了后台请求.
最后,我们来数理一下整个请求过程:
- 客户端请求URL:
/xxxx/..;/admin/index
- shrio 内部处理得到校验URL为
/xxxx/..,
校验通过 - 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是不一样的.
增加一个路由1
2
3
4"/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并没有关系. 我们进行一个简单的测试:
1 | "/index") ( |
完全没有使用shiro, 大家也可以测试下.所以这个问题其实在shiro 1.5.2 上面也同样是可以的.
上面的测试只是一种最简单的情况, 只有shiro配置了一个全局的权限校验, 就有可能存在绕过的问题, 如果程序进一步在URL上面配置了权限校验,即使绕过了ShiroFilterChainDefinition, 但是还是无法绕过注解上面的防御.如下所示:
1 |
|
总结
讲到这里,差不多有关这个漏洞的所有问题都说完了.其实本文章还涉及到一些其他的知识.比如:
- requesturi 和 servlet的区别
- springmvc的请求处理流程
这些都可以写一篇文章来进行说明了.整体来说,这个漏的利用方式还是很简单的,我测试了目前大部分使用shiro的应用基本上都存在绕过的问题, 但是这个漏洞能够找成多大的危害呢? 就目前看来危害还是有限的,因为即使绕过了shiro的权限校验,但是一般情况下这些接口/请求都需要对应用户的权限,所以绕过了shiro登录到后台系统只是以一种没有用户身份的方式登录到后台系统, 后台校验此时获取当前用户信息,发现为空.此时整体系统就会出错,或者重新跳转到登录页面,重新登录.这里就不作说明了