Shiro是一个功能强大且易于使用的 Java 安全框架,可执行身份验证、授权、加密和会话管理。在带有 Spring 动态控制器的 1.5.2 之前的 Apache Shiro 版本中,攻击者可以使用 ..;
绕过目录认证。
漏洞环境:使用 Spring 2.2.2 和 Shiro 1.5.1 启动应用程序,环境启动后,访问 http://your-ip:8080
查看主页。
本应用中 URL 权限的配置如下。
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/login.html", "authc"); // need to accept POSTs from the login form
chainDefinition.addPathDefinition("/logout", "logout");
chainDefinition.addPathDefinition("/admin/**", "authc");
return chainDefinition;
}
会对admin所有的页面都会进行权限校验。测试结果如下:
访问首页:


直接请求管理页面 /admin/
无法访问,将被重定向到登录页面。

访问admin时使用burp抓包:

绕过演示
构造恶意请求 /xxx/..;/admin/
绕过身份验证检查并访问管理页面,在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); //URL:"/xxxx/..;/admin/index"
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
, 成功访问了后台请求。
总的来说,这个漏洞还是比较简单的,虽然大部分shior都存在这个漏洞,但是实用性不大。