Spring Boot 中关于 %2e 的 Trick

公众号程序猿DD

共 8694字,需浏览 18分钟

 ·

2021-05-29 09:37

作者 | Ruilin

来源 | http://rui0.cn/archives/1643
分享一个Spring Boot中关于%2e的小Trick。
先说结论,当Spring Boot版本在小于等于2.3.0.RELEASE的情况下,alwaysUseFullPath为默认值false,这会使得其获取ServletPath,所以在路由匹配时相当于会进行路径标准化包括对%2e解码以及处理跨目录,这可能导致身份验证绕过。
而反过来由于高版本将alwaysUseFullPath自动配置成了true从而开启全路径,又可能导致一些安全问题。
这里我们来通过一个例子看一下这个Trick,并分析它的原因。
首先我们先来设置Sprin Boot版本
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.0.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
编写一个Controller
@RestController
public class httpbinController {
    @RequestMapping(value = "no-auth", method = RequestMethod.GET)
    public String noAuth() {
        return "no-auth";
    }
 
    @RequestMapping(value = "auth", method = RequestMethod.GET)
    public String auth() {
        return "auth";
    }
}
接下来配置对应的Interceptor来实现对除no-auth以外的路由的拦截
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(handlerInterceptor())
                //配置拦截规则
                .addPathPatterns("/**");
    }
 
    @Bean
    public HandlerInterceptor handlerInterceptor() {
        return new PermissionInterceptor();
    }
}

@Component
public class PermissionInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler)
 throws Exception 
{
        String uri = request.getRequestURI();
        uri = uri.replaceAll("//""/");
        System.out.println("RequestURI: "+uri);
        if (uri.contains("..") || uri.contains("./") ) {
            return false;
        }
        if (uri.startsWith("/no-auth")){
            return true;
        }
        return false;
    }
}
由上面代码可以知道它使用了getRequestURI来进行路由判断。通常你可以看到如startsWithcontains 这样的判断方式,显然这是不安全的,我们绕过方式由很多比如....;等,但其实在用startsWith来判断白名单时构造都离不开跨目录的符号.. 那么像上述代码这种情况又如何来绕过呢?答案就是%2e发起请求如下
$ curl -v "http://127.0.0.1:8080/no-auth/%2e%2e/auth"
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /no-auth/%2e%2e/auth HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 4
< Date: Wed, 14 Apr 2021 13:22:03 GMT
<
* Connection #0 to host 127.0.0.1 left intact
auth
* Closing connection 0
RequestURI输出为
RequestURI: /no-auth/%2e%2e/auth
可以看到我们通过%2e%2e绕过了PermissionInterceptor的判断,同时匹配路由成功,很显然应用在进行路由匹配时会进行路径标准化包括对%2e解码以及处理跨目录即如果存在/../则返回上一级目录。
我们再来切换Spring Boot版本再来看下
<version>2.3.1.RELEASE</version>
发起请求,当然也是过了拦截,但没有匹配路由成功,返回404
$ curl -v "http://127.0.0.1:8080/no-auth/%2e%2e/auth"
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /no-auth/%2e%2e/auth HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 404
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Content-Length: 0
< Date: Wed, 14 Apr 2021 13:17:26 GMT
<
* Connection #0 to host 127.0.0.1 left intact
* Closing connection 0
RequestURI输出为
RequestURI: /no-auth/%2e%2e/auth
可以得出结论当Spring Boot版本在小于等于2.3.0.RELEASE的情况下,其在路由匹配时会进行路径标准化包括对%2e解码以及处理跨目录,这可能导致身份验证绕过。那么又为什么会这样?
在SpringMVC进行路由匹配时会从DispatcherServlet开始,然后到HandlerMapping中去获取Handler,在这个时候就会进行对应path的匹配。
我们来跟进代码看这个关键的地方org.springframework.web.util.UrlPathHelper#getLookupPathForRequest(javax.servlet.http.HttpServletRequest) 这里就出现有趣的现象,在2.3.0.RELEASE中alwaysUseFullPath为默认值false
img
而在2.3.1.RELEASE中alwaysUseFullPath被设置成了true
img
这也就导致了不同的结果,一个走向了getPathWithinApplication而另一个走向了getPathWithinServletMapping 在getPathWithinServletMapping 中会获取ServletPath,ServletPath会对uri标准化包括先解码然后处理跨目录等,这个很多讲Tomcat uri差异的文章都提过了,就不多说了。而getPathWithinApplication中主要是先获取RequestURI然后解码但之后没有再次处理跨目录,所以保留了..因此无法准确匹配到路由。到这里我们可以看到这两者的不同,也解释了最终出现绕过情况的原因。
那么Trick的具体描述就成了当Spring Boot版本在小于等于2.3.0.RELEASE的情况下,alwaysUseFullPath为默认值false,这会使得其获取ServletPath,所以在路由匹配时相当于会进行路径标准化包括对%2e解码以及处理跨目录,这可能导致身份验证绕过。
而这和Shiro的CVE-2020-17523中的一个姿势形成了呼应,只要高版本Spring Boot就可以了不用非要手动设置alwaysUseFullPath
$ curl -v http://127.0.0.1:8080/admin/%2e
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /admin/%2e HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 10
< Date: Wed, 14 Apr 2021 13:48:33 GMT
<
* Connection #0 to host 127.0.0.1 left intact
admin page* Closing connection 0
因为混合使用Spring Framework依赖时用户需要明确配置而Spring Boot会自动配置Spring Framework,所以在使用Spring Boot的时候官方提供了shiro-spring-boot-web-starter依赖来支持UrlPathHelper(https://shiro.apache.org/spring-boot.html)这样可以解决这类问题,上面这种姿势也就不存在了。对于非Spring Boot应用你可以通过这种方式(https://shiro.apache.org/spring-framework.html#web-applications)来配置UrlPathHelper。感兴趣的可以再看看说不定有额外收获。
话说回来,可是为什么在高版本中alwaysUseFullPath会被设置成true呢?这就要追溯到org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#configurePathMatch 在spring-boot-autoconfigure-2.3.0.RELEASE中

在spring-boot-autoconfigure-2.3.1.RELEASE中

为什么要这样设置?我们查看git log这里给出了答案。

https://github.com/spring-projects/spring-boot/commit/a12a3054c9c5dded034ee72faac20e578b5503af
当Servlet映射为”/”时,官方认为这样配置是更有效率的,因为需要请求路径的处理较少。
配置servlet.path可以通过如下,但通常不会这样配置除非有特殊需求。
spring.mvc.servlet.path=/test/
所以最后,当Spring Boot版本在小于等于2.3.0.RELEASE的情况下,alwaysUseFullPath为默认值false,这会使得其获取ServletPath,所以在路由匹配时相当于会进行路径标准化包括对%2e解码以及处理跨目录,这可能导致身份验证绕过。而高版本为了提高效率对alwaysUseFullPath自动配置成了true从而开启全路径,这又造就了Shiro的CVE-2020-17523中在配置不当情况下的一个利用姿势,如果代码中没有提供对此类参数的判断支持,那么就可能会存在安全隐患。其根本原因是Spring Boot自动配置的内容发生了变化。

往期推荐

重磅!Spring Boot 2.5.0火热发布,还学得动吗?

常用正则表达式最强整理(速查手册)

Spring Security 5.5发布,正式实装OAuth2.0的第五种授权模式

Spring越来越强,而我们越来越快餐!离开了Spring,居然API都写不出来了!

字节跳动涉代码抄袭被诉陪22.74亿,连错误的函数都搬?


如果你喜欢本文,欢迎关注我,订阅更多精彩内容
关注我回复「加群」,加入Spring技术交流群
浏览 51
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报