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下的所有路径都进行权限验证
xxxxxxxxxxShiroFilterFactoryBean 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并不是真正的
xxxxxxxxxxpublic 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还是正确的
xxxxxxxxxxpublic 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/..
xxxxxxxxxxprivate 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/**
xxxxxxxxxxpublic 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
xxxxxxxxxxpublic 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));}