Apache Shiro是一个安全框架,在Java开发中使用非常广,可以很方便地做权限管理。一个权限绕过漏洞,危害不是很大,因为一般情况下绕过Shiro还有具体的操作权限验证,很少发生绕过Shiro获取很大权限这种事。最有名的Shiro反序列化漏洞打算后续分析
下载官方示例:https://github.com/lenve/javaboy-code-samples
使用其中的shiro/shiro-basic项目,修改org\javaboy\shirobasic\ShiroConfig.java
代码,修改配置,设置登录url为/login
,设置成功后跳转的url是/index
,设置登录失败的url为/unauthorizedurl
,对于/admin
下的所有路径都进行权限验证
xxxxxxxxxx
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;
}
添加一个路由,在org\javaboy\shirobasic\LoginController.java
中增加如下代码
xxxxxxxxxx
"/index") (
public String index(){
return "This is index page";
}
"/admin/index") (
public String test() {
return "This is admin index page";
}
启动项目,访问/index,没有被拦截
访问/admin/index,不允许访问,跳到登录页
输入http://localhost:8080/xxx/..;/admin/index
成功绕过
从大体上理解,其实让Shiro和SpringBoot获取到的URL不同,就可以做到绕过
从shiro获取uri的入口分析:org\apache\shiro\web\util\WebUtils.java
,可以看到,通过getRequestUri
函数获得的uri并不是真正的
xxxxxxxxxx
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;
}
}
跟入getRequestUri
函数,可以看到在return normalize(decodeAndCleanUriString(request, uri));
之前,uri还是正确的
xxxxxxxxxx
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));
}
重点来看normalize
和decodeAndCleanUriString
方法。不难看出,decodeAndCleanUriString
方法进行了字符串截断,最后得到的uri是/xxx/..
xxxxxxxxxx
private static String decodeAndCleanUriString(HttpServletRequest request, String uri) {
uri = decodeRequestString(request, uri);
int semicolonIndex = uri.indexOf(';');
return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri);
}
其中normalize
方法并没有什么特殊的地方,一个标准库,替换反斜杠,去除双斜杠,处理/../
和/./
等特殊情况,而获取到的uri会在org\apache\shiro\web\filter\mgt\PathMatchingFilterChainResolver.java
类的getChain
方法做校验,而/xxx/..
并不会匹配到/admin/**
xxxxxxxxxx
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
FilterChainManager filterChainManager = getFilterChainManager();
if (!filterChainManager.hasChains()) {
return null;
}
String requestURI = getPathWithinApplication(request);
//the 'chain names' in this implementation are actually path patterns defined by the user. We just use them
//as the chain name for the FilterChainManager's requirements
for (String pathPattern : filterChainManager.getChainNames()) {
// If the path does match, then pass on to the subclass implementation for specific checks:
if (pathMatches(pathPattern, requestURI)) {
if (log.isTraceEnabled()) {
log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. " +
"Utilizing corresponding filter chain...");
}
return filterChainManager.proxy(originalChain, pathPattern);
}
}
return null;
}
到这里就绕过了Shiro,但还要让SpringBoot解析成对应的URL漏洞才会生效。找到SpringBoot解析URL的类org\springframework\web\util\UrlPathHelper.java
的getPathWithinServletMapping
方法,断点调试后发现返回的是/admin/index
,成功触发漏洞。HttpServletRequest.getServletPath()
是标准方法,并不存在漏洞,所以本质上还是shiro的校验问题
官方补丁:https://github.com/apache/shiro/commit/3708d7907016bf2fa12691dff6ff0def1249b8ce
以上文的payload/xxx/...;/admin/index
,这里uri会变成//admin/index
xxxxxxxxxx
public static String getRequestUri(HttpServletRequest request) {
String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE);
if (uri == null) {
uri = request.getRequestURI();
uri = valueOrEmpty(request.getContextPath()) + "/" +
valueOrEmpty(request.getServletPath()) +
valueOrEmpty(request.getPathInfo());
}
return normalize(decodeAndCleanUriString(request, uri));
}