Spring Boot 中關於 %2e 的 坑,希望你不要遇到

作者 | 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來進行路由判斷。通常你可以看到如 startsWith contains  這樣的判斷方式,顯然這是不安全的,我們繞過方式由很多比如 .. ..; 等,但其實在用 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自動配置的內容發生了變化。

往期推薦



面試官:作爲架構師,請你談談Saas 應用如何搭建?

SpringBoot中微服務技術中進程間通信原理

Spring Boot 接入支付寶支付案例教程!

Spring Boot 中的Http接口調用只知道 RestTemplate?來試下 Retrofit !

Java中關於線程同步,你會用到的4個類

數據庫消耗 CPU 最高的 sql 語句如何定位?

怎麼樣通過Nginx實現限流?

5個好用的開源網絡監控工具

SpringBoot實現登錄攔截的原理

keepalived實現Nginx雙機熱備

一行代碼讓你擺脫U盤完成局域網文件傳輸

Docker簡易搭建 ElasticSearch 集羣


本文分享自微信公衆號 - 俠夢的開發筆記(xmdevnote)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章