漏洞描述
Apache Shiro before 1.7.0, when using Apache Shiro with Spring, a specially crafted HTTP request may cause an authentication bypass.
If you are NOT using Shiro’s Spring Boot Starter (shiro-spring-boot-web-starter
), you must configure add the ShiroRequestMappingConfig
autoconfiguration to your applicationor configure the equivalent manually. [1]
漏洞条件
-
shiro < 1.7.0
-
springboot > 2.3.0 RELEASE
-
要使用resful风格的路径
漏洞复现
环境
基础配置
shiro: 1.6.0
spring-boot: 2.7.4
shiro配置:
@Bean
ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setLoginUrl("/login");
bean.setSuccessUrl("/loginSuccess");
bean.setUnauthorizedUrl("/unauthorized");
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
//url --> filter1,filter2....
map.put("/print", "authc, perms[printer:print]");
map.put("/query", "authc, perms[printer:query]");
map.put("/admin/*", "authc, roles[admin]"); //不可以是"/admin/**"
map.put("/login","authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
controller:
@GetMapping("/admin/{param}")
public String adminInfo(@PathVariable String param) {
if(param == null){
return "you are admin";
}
return "Admin Info: " + param;
}
测试
payload
GET /admin/. HTTP/1.1
Host: localhost:9090
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
DNT: 1
Sec-GPC: 1
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=0, i
结果:
漏洞分析
漏洞入口:PathMatchingFilterChainResolver.getChain()
源码:
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
FilterChainManager filterChainManager = getFilterChainManager();
if (!filterChainManager.hasChains()) {
return null;
}
//漏洞点
String requestURI = getPathWithinApplication(request);
//去除末尾的 “/”
if(requestURI != null && !DEFAULT_PATH_SEPARATOR.equals(requestURI)
&& requestURI.endsWith(DEFAULT_PATH_SEPARATOR)) {
requestURI = requestURI.substring(0, requestURI.length() - 1);
}
for (String pathPattern : filterChainManager.getChainNames()) {
//去除末尾的 “/”
if (pathPattern != null && !DEFAULT_PATH_SEPARATOR.equals(pathPattern)
&& pathPattern.endsWith(DEFAULT_PATH_SEPARATOR)) {
pathPattern = pathPattern.substring(0, pathPattern.length() - 1);
}
// If the path does match, then pass on to the subclass implementation for specific checks:
if (pathMatches(pathPattern, requestURI)) {
if (log.isTraceEnabled()) {
log.trace("省略");
}
return filterChainManager.proxy(originalChain, pathPattern);
}
}
return null;
}
进入漏洞点
末尾多出来的路径不明白的可以看同专辑中的CVE-2020-13933
结果毫无疑问,/amdin
匹配到/**
但是"/admin"这个路径对InvalidRequestFilter的核心算法而言是合法的,所以会被shiro放行:
//InvalidRequestFilter::
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
String uri = WebUtils.toHttp(request).getRequestURI();
return !containsSemicolon(uri)
&& !containsBackslash(uri)
&& !containsNonAsciiCharacters(uri);
}
于是来到spring的路径匹配入口:AbstractHandlerMethodMapping.getHandlerInternal(...)
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = initLookupPath(request);
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
进入initLookupPath(request):
userPathPatterns() 默认为true;
返回:/admin/.
由于spring没有像shiro那样对像 带有/.
和/..
的路径进行规范化,导致最终匹配到了controller方法:
【注意】:点号是必须的:
结果测试 spring”/admin/“是匹配 “/admin”的但是不匹配“/admin/{param}”
补充
为什么要 springboot > 2.3.0 :
[2]
主要是因为springboot <= 2.3.0 RELEASE 路径匹配机制与后面版本有所不同。
以下源码全是springboot 2.3.0 RELEASE
函数入口:AbstractHandlerMethodMapping#getHandlerInternal
调试:
uri: /admin/.
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
public String getLookupPathForRequest(HttpServletRequest request) {
// Always use full path within current servlet context?
//分支1
if (this.alwaysUseFullPath) {
return getPathWithinApplication(request);
}
// Else, use path within current servlet mapping if applicable
//分支2
//一般进入这个分支
String rest = getPathWithinServletMapping(request);
if (!"".equals(rest)) {
return rest;
}
else {
return getPathWithinApplication(request);
}
}
-
第一个分支是正常的:getPathWithinApplication
public String getPathWithinApplication(HttpServletRequest request) { String contextPath = getContextPath(request); String requestUri = getRequestUri(request); //去除匹配到的部分,返回剩余部分,忽略大小写,从而返回相对路径 String path = getRemainingPath(requestUri, contextPath, true); if (path != null) { // Normal case: URI contains context path. return (StringUtils.hasText(path) ? path : "/"); } else { return requestUri; } }
public String getRequestUri(HttpServletRequest request) { String uri = (String) request.getAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE); if (uri == null) { uri = request.getRequestURI(); } return decodeAndCleanUriString(request, uri); }
private String decodeAndCleanUriString(HttpServletRequest request, String uri) { //先去除分号, uri = removeSemicolonContent(uri); //再解码 uri = decodeRequestString(request, uri); //然后将`//`替换成`/` uri = getSanitizedPath(uri); return uri; }
-
但是第二个分支会出问题:
getServletPath()底层调用request.getServletPath(),其会对
/..
和/.
进行规范化getRemainingPath(...)是去除匹配的部分,返回其余的部分
当然如果只走第一分支,那该漏洞在springboot <= 2.3.0 RELEASE 也能利用,这需要相关配置使得this.alwaysUseFullPath
返回true.
无论是哪个分支对上一个漏洞CVE-2020-13933都没有影响
漏洞修复
springboot > 2.3.0 RELEASE:
protected String initLookupPath(HttpServletRequest request) {
if (usesPathPatterns()) {
request.removeAttribute(UrlPathHelper.PATH_ATTRIBUTE);
RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(request);
String lookupPath = requestPath.pathWithinApplication().value();
return UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath);
}
else {
//进入这个分支需要配置才会生效
//底层与spring 2.3.0 RELEASE一样
return getUrlPathHelper().resolveAndCacheLookupPath(request);
}
}
public String resolveAndCacheLookupPath(HttpServletRequest request) {
//与spring <= 2.3.0 RELEASE一样
String lookupPath = getLookupPathForRequest(request);
request.setAttribute(PATH_ATTRIBUTE, lookupPath);
return lookupPath;
}
public String getLookupPathForRequest(HttpServletRequest request) {
// Always use full path within current servlet context?
//分支1
if (this.alwaysUseFullPath) {
return getPathWithinApplication(request);
}
// Else, use path within current servlet mapping if applicable
//分支2
//一般进入这个分支
String rest = getPathWithinServletMapping(request);
if (!"".equals(rest)) {
return rest;
}
else {
return getPathWithinApplication(request);
}
}
修复:增加了一个类,ShiroUrlPathHelper.java
很明显,将路径匹配规则,改成shiro的WebUtils.getPathWithinApplication(request);
如此shiro与spring获取到的路径就完全一致了。
【注意】:需要额外配置才能生效(也就是走initLookupPath()
中的else分支):
Reference
[1] Security Reports | Apache Shiro
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)