【java安全】Spring权限绕过
简单分析总结一下Spring权限绕过的一些方法
spring权限绕过
前言
经过长达20年的发展,spring框架已经进入6.0时代,目前国内在使用的版本主要还是4.0和5.0版本
Spring Boot是一个Spring的组件集合,它帮我们预组装了Springl的一系列组件,通过Spring Boot我们可以在极
短的时间内搭建一套基于Spring框架的WEB系统。
Spring Boot与spring框架的版本对应关系如下:
Spring Boot版本 | Spring版本 | |
---|---|---|
3.x | 6.x | |
2.x | 5.x | |
1.x | 4.x |
环境
不同版本的Spring框架在路由处理上存在一定差异,本文相关源码采用Spring Boot2.2.0.RELEASE版本
对应Spring Framework的版本为5.2.0.RELEASE,WEB容器采用默认的tomcat
@RestController
public class AdminController {
@RequestMapping("/admin")
public String admin(){
return "You are admin";
}
}
前置知识
在进行分析之前,需要了解一下spring中如何对路由进行处理
从网上获取到一张原理图
首先所有请求会进入Dispatcher Servlet,由其进行分发,其大致处理逻辑是根据请求信息遍历HandlerMappinng,找到对应请求的HandlerMapping交由其进行处理
换句话说:DispatcherServlet会调用对应HandlerMapping来解析URL,完成Controller的匹配,接着执行业务操作,最后将数据交给ViewResolver处理视图映射
Spring MVC 框架中有多种不同的 HandlerMapping 实现,每种实现都有不同的作用和使用场景
- BeanNameUrlHandlerMapping 是最简单的HandlerMapping实现,它将 URL 请求的路径直接映射到 Bean 的名称上。
- SimpleUrlHandlerMapping 将 URL 请求的路径与相应的处理程序进行映射。
- ControllerClassNameHandlerMapping 将 URL 请求的路径映射到 Controller 类名上。
- DefaultAnnotationHandlerMapping 将 URL 请求的路径映射到标注了 @RequestMapping 注解的方法上
- RequestMappingHandlerMapping 用户通过RequestMapping注解实现的接口
- ....
分析
我们的测试代码是通过RequestMapping注解编写的一个接口(大部分接口都是这样编写),因此经过Mapping的遍历后会匹配到RequestMappingHandlerMapping进行处理
深入分析getHandler
方法,看看是如何匹配成功的
可以看到先是调用了getHandlerInternal
方法,该方法调用完成后即匹配到了org.example.controller.AdminController#admin()
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
Object handler = this.getHandlerInternal(request);
if (handler == null) {
handler = this.getDefaultHandler();
}
if (handler == null) {
return null;
} else {
if (handler instanceof String) {
String handlerName = (String)handler;
handler = this.obtainApplicationContext().getBean(handlerName);
}
HandlerExecutionChain executionChain = this.getHandlerExecutionChain(handler, request);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Mapped to " + handler);
} else if (this.logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
this.logger.debug("Mapped to " + executionChain.getHandler());
}
if (this.hasCorsConfigurationSource(handler)) {
CorsConfiguration config = this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null;
CorsConfiguration handlerConfig = this.getCorsConfiguration(handler, request);
config = config != null ? config.combine(handlerConfig) : handlerConfig;
executionChain = this.getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}
}
深入查看getHandlerInternal
方法,可以发现其先获取了lookupPath
,接着通过lookupPath
和request
去查找对应的处理方法
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = this.getUrlPathHelper().getLookupPathForRequest(request);
request.setAttribute(LOOKUP_PATH, lookupPath);
this.mappingRegistry.acquireReadLock();
HandlerMethod var4;
try {
HandlerMethod handlerMethod = this.lookupHandlerMethod(lookupPath, request);
var4 = handlerMethod != null ? handlerMethod.createWithResolvedBean() : null;
} finally {
this.mappingRegistry.releaseReadLock();
}
return var4;
}
深入分析getLookupPathForRequest
方法,可以看到一个alwaysUseFullPath
,顾名思义就是“全路径查找”,该配置在Spring Boot版本小于等于2.3.0 RELEASE默认为false,根据这个开关决定调用getPathWithinApplication
或者getPathWithinServletMapping
,rest为空时也会调用getPathWithinApplication
方法,因此后面会重点分析该方法(因为大部分程序的配置都会走到这个函数里面处理路径)
public String getLookupPathForRequest(HttpServletRequest request) {
if (this.alwaysUseFullPath) {
return this.getPathWithinApplication(request);
} else {
String rest = this.getPathWithinServletMapping(request);
return !"".equals(rest) ? rest : this.getPathWithinApplication(request);
}
}
查询得到这两个方法的区别:
-
getPathWithinApplication
:- 这个方法是 Spring Framework 中的方法,用于获取请求 URL 相对于应用程序的路径。
- 它返回的是请求 URL 相对于应用程序根路径的路径部分,不包括应用程序的上下文路径。
- 例如,如果你的应用程序部署在
/myapp
上下文路径下,而请求的 URL 是/myapp/users/123
,那么getPathWithinApplication
返回的将是/users/123
。
-
getPathWithinServletMapping
:- 这个方法通常是在 Servlet API 或类似的框架中提供的,用于获取请求 URL 相对于 Servlet 映射的路径。
- 它返回的是请求 URL 相对于 Servlet 映射路径的路径部分,不包括上下文路径。
- 如果你有一个 Servlet 映射到
/users/*
,而请求的 URL 是/myapp/users/123
,那么getPathWithinServletMapping
返回的将是/123
。
总的来说,getPathWithinApplication
是在 Spring Framework 中用于获取相对于应用程序根路径的路径,而 getPathWithinServletMapping
则是在 Servlet API 中用于获取相对于 Servlet 映射路径的路径。
因为测试环境Spring Boot版本小于等于2.3.0,所以继续跟进getPathWithinServletMapping
方法,其首先调用了getPathWithinApplication
public String getPathWithinServletMapping(HttpServletRequest request) {
String pathWithinApp = this.getPathWithinApplication(request);
String servletPath = this.getServletPath(request);
String sanitizedPathWithinApp = this.getSanitizedPath(pathWithinApp);
String path;
if (servletPath.contains(sanitizedPathWithinApp)) {
path = this.getRemainingPath(sanitizedPathWithinApp, servletPath, false);
} else {
path = this.getRemainingPath(pathWithinApp, servletPath, false);
}
if (path != null) {
return path;
} else {
String pathInfo = request.getPathInfo();
if (pathInfo != null) {
return pathInfo;
} else {
if (!this.urlDecode) {
path = this.getRemainingPath(this.decodeInternal(request, pathWithinApp), servletPath, false);
if (path != null) {
return pathWithinApp;
}
}
return servletPath;
}
}
}
继续跟进getPathWithinApplication
,跟前面分析一致,用于获取相对于应用程序根路径的路径,但是有一些处理,contextPath
是获取应用上下文路径
public String getPathWithinApplication(HttpServletRequest request) {
String contextPath = this.getContextPath(request);
String requestUri = this.getRequestUri(request);
String path = this.getRemainingPath(requestUri, contextPath, true);
if (path != null) {
return StringUtils.hasText(path) ? path : "/";
} else {
return requestUri;
}
}
继续深入getRequestUri
方法,发现其对路径进行了处理decodeAndCleanUriString
public String getRequestUri(HttpServletRequest request) {
String uri = (String)request.getAttribute("javax.servlet.include.request_uri");
if (uri == null) {
uri = request.getRequestURI();
}
return this.decodeAndCleanUriString(request, uri);
}
深入decodeAndCleanUriString
方法,发现三个处理的方法
private String decodeAndCleanUriString(HttpServletRequest request, String uri) {
uri = this.removeSemicolonContent(uri);
uri = this.decodeRequestString(request, uri);
uri = this.getSanitizedPath(uri);
return uri;
}
removeSemicolonContent:用于移除请求 URI 中的分号内容,如URI:/;cxzdsadw/admin,处理后即为//admin
decodeRequestString:URL解码
getSanitizedPath:循环移除"//",如//admin处理后即为/admin
再继续分析getPathWithinServletMapping
方法剩余的代码,绝大部分情况下获取的三个路径是相等的,也就是直接返回,当存在目录穿越时,通常会返回servletPath
其中
getServletPath
方法最终会调用web容器的getServletPath
,在不同的容器中该方法的返回值有差异以tomcat为例,当我们输入/api/..;/admin时,该方法将返回常规化后的url,即/admin, getPathWithinServletMapping正常情况下会返回空字符串
分析完路径的处理过程,接着分析如何通过路径查找到对应的处理过程org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod
发现只有springfox.documentation.spring.web.PropertySourcedRequestMappingHandlerMapping
重写了该方法,并且该方法内是通过类似hashmap形式去查找匹配的,url匹配很严格必须一一对应,因此在绕过类似swagger这种基于配置生成的接口是很难绕过的
绝大部分场景都是走的默认的lookupHandlerMethod
方法,该方法在没有匹配到处理方法时会使用最佳匹配的算法,寻找最佳匹配的处理器,比如"/admin/"会匹配上"/admin"
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<AbstractHandlerMethodMapping<T>.Match> matches = new ArrayList();
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
this.addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
this.addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Comparator<AbstractHandlerMethodMapping<T>.Match> comparator = new MatchComparator(this.getMappingComparator(request));
matches.sort(comparator);
AbstractHandlerMethodMapping<T>.Match bestMatch = (Match)matches.get(0);
if (matches.size() > 1) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(matches.size() + " matching mappings: " + matches);
}
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
AbstractHandlerMethodMapping<T>.Match secondBestMatch = (Match)matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException("Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
this.handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
} else {
return this.handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
漏洞利用与修复
在漏洞利用上,主要使用的方法是url路径格式化的绕过、智能匹配算法导致的绕过
首先添加一个Interceptor
进行拦截
正常访问,被拦截
使用路径格式化绕过案例1:"//admin"
使用路径格式化绕过案例2: "/;xxx/admin"
使用路径格式化绕过案例3: "/%61%64%6d%69%6e"
使用路径格式化绕过案例4:"/aa/../admin"
漏洞原理与修复
在tomcat容器下,通过getRequestURI/getRequestURL获取到的url是请求的原始url,即未经过任何处理和转换的url, 通过前面的源码分析,Spring框架会自动清除url中的特殊字符并进行url解码,因此在Spring框架下如使用相关的漏洞 方法做鉴权,则很容易出现权限绕过漏洞。
因此,推荐使用较安全的方法一getServletPath获取请求的url,该方法获取到的url是经过tomcat'常规化处理后的,即做了和Spring框架类似的清理与转换,使得其对url的解释结果一致,从而避免因处理不一致导致的权限绕过问题。 例如,访问/;//api/../admin路径时,getRequestURI和getServletPath获取到的url分别如下图所示:
不严格的黑名单导致的权限绕过
漏洞源码
再使用前面的方法发现无法绕过,因为这里用了getServletPath
方法获取路径
借助Spring路径智能匹配机制去绕过
尾部增加.xxx绕过(Spring Boot版本小于等于1.5.22.RELEASE时useSuffixPatternMatch默认为true)
原理及修复
由于spring框架存在最佳路由匹配算法,导致访问/admin/时也能访问到路由定义为/admin的接口,从安全的角度,在任何时候都不建议使用黑名单防护,当然在万不得已的时候建议能够更加严格,如将equals方法改为contains方法
总结
Spring作为一款主流的、优秀的JAVA开发框架,其功能非常强大,在开发人员使用不当时虽然可能存在较多的安全漏洞,但这严格意义上并不属于框架本身的安全问题,属于开发人员的逻辑不严谨从而导致Spring框架与WEB容器之间产生解析差异导致的逻辑漏洞。站在安全的角度,建议在任何时候都不要优先通过黑名单的形式进行访问策略控制。
参考
black_memory师傅的视频分享