作者: Badcode@知道創宇404實驗室
時間: 2018/08/14
英文版:http://www.bjnorthway.com/991/

漏洞簡介

2018年04月05日,Pivotal公布了Spring MVC存在一個目錄穿越漏洞(CVE-2018-1271)。Spring Framework版本5.0到5.0.4,4.3到4.3.14以及較舊的不受支持的版本允許應用程序配置Spring MVC以提供靜態資源(例如CSS,JS,圖像)。當Spring MVC的靜態資源存放在Windows系統上時,攻擊可以通過構造特殊URL導致目錄遍歷漏洞。

漏洞影響

  • Spring Framework 5.0 to 5.0.4.
  • Spring Framework 4.3 to 4.3.14
  • 已不支持的舊版本仍然受影響

漏洞利用條件

  • Server運行于Windows系統上
  • 要使用file協議打開資源文件目錄

漏洞復現

復現環境

環境搭建

1.下載 spring-mvc-showcase

git clone https://github.com/spring-projects/spring-mvc-showcase.git

修改pom.xml,使用Spring Framework 5.0.0。

2.修改 Spring MVC 靜態資源配置,可參考官方文檔

通過官方文檔可知有兩種方式配置,可自行選擇配置。此處通過重寫WebMvcConfigurer中的addResourceHandlers方法來添加新的資源文件路徑。在org.springframework.samples.mvc.config.WebMvcConfig添加以下代碼即可,使用file://協議指定resources為靜態文件目錄。

 registry.addResourceHandler("/resources/**").addResourceLocations("file:./src/main/resources/","/resources/");

3.使用 jetty 啟動項目

mvn jetty:run

至此復現環境搭建完畢。

復現過程及結果

訪問以下鏈接

http://localhost:8080/spring-mvc-showcase/resources/%255c%255c..%255c/..%255c/..%255c/..%255c/..%255c/..%255c/..%255c/..%255c/..%255c/windows/win.ini

可以看到成功讀取到win.ini的內容了。

漏洞分析

當外部要訪問靜態資源時,會調用org.springframework.web.servlet.resource.ResourceHttpRequestHandler:handleRequest來處理,在這里下斷點調試。

跟進org.springframework.web.servlet.resource.ResourceHttpRequestHandler:getResource()

request中保存的路徑是/spring-mvc-showcase/resources/%255c%255c..%255c/..%255c/..%255c/..%255c/..%255c/..%255c/..%255c/..%255c/..%255c/windows/win.ini。在request.getAttribute()函數取值時會進行 url decode操作,此時path的值為%5c%5c..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/windows/win.ini。接下來會對path進行兩次校驗,將pathpath解碼之后的值分別使用isInvalidPath函數檢查。看下這個函數

path包含有..的時候,會調用cleanPath函數對path處理。跟進

這個函數的作用是把包含..的這種相對路徑轉換成絕對路徑。例如/foo/bar/../經過cleanPath處理后就會變成/foo/

cleanPath的問題在于String[] pathArray = delimitedListToStringArray(pathToUse, "/");這個是允許空元素存在的,也就是說cleanPath會把//當成一個目錄,而操作系統是不會把//當成一個目錄的。借用一張Orange大佬的圖。

繼續回到流程上,上面說到會對path進行兩次校驗,第一次調用isInvalidPathpath的值是%5c%5c..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/windows/win.ini,因為path/分割之后沒有元素等于..,所以path經過cleanPath處理后的值不變,繼續之后的判斷,path里面也不包含../,所以最終返回false,也就是通過了校驗。

第二次調用isInvalidPath(URLDecoder.decode(path, "UTF-8")),此時參數值是\\..\/..\/..\/..\/..\/..\/..\/..\/..\/windows/win.ini,經過cleanPath處理后的值是//windows/win.ini,之后繼續判斷,path里面也不包含../,最終返回false,也通過了校驗。

通過兩次校驗之后,繼續向下執行。獲取一個Resource對象

path的值還是之前,getLocations()獲取到的就是之前在配置文件中配置的路徑file:./src/main/resources/,繼續跟進

跟進ResourceResolver類的resolveResource

跟進PathResourceResolverresolveResourceInternal

進入到org.springframework.web.servlet.resource.PathResourceResolvergetResource()

此時的resourcePath就是之前的pathlocation就是之前getLocations()獲取到的值。繼續跟進this.getResource

調用location.createRelativ拼接得到文件的絕對路徑,返回一個UrlResource對象

返回到getResource函數

此時,resource是一個UrlResource對象,可以看到值是file:src/main/resources/%5c%5c..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/..%5c/windows/win.ini,之后調用exists()方法檢查該文件是否存在,調用isReadable()方法檢測該文件是否可讀。進去exists()方法

這里會調用isFileURLurl進行判斷,是否以file://協議來讀取文件,這也是為什么配置靜態目錄的時候要使用file://協議。

通過判斷之后,會調用this.getFile()來獲取這個文件對象,這個方法在org.springframework.util.ResourceUtils這個方法類里面,跟進

這里對是否為file://協議又判斷了一次,之后進行了一步最重要的操作new File(toURI(resourceUrl).getSchemeSpecificPart());,將resourceUrl轉換為URL對象,最后調用URI類的getSchemeSpecificPart()獲取到文件路徑,而在getSchemeSpecificPart()里面是有一次decode操作的,也就是在這里把%5c解碼成了\,跟進

最后返回到exists(),最終返回true,即文件存在

之后調用isReadable()方法檢測該文件是否可讀的時候,同樣會調用這個getFile,最終返回true,即文件可讀。

至此,對于resource的判斷都結束了。返回到org.springframework.web.servlet.resource.ResourceHttpRequestHandler:handleRequest(),獲取到通過校驗resource的之后,就開始準備response的內容了,包含獲取文件的類型(用于response的Content-type),文件的大小(用于response的Content-length)等等,最后調用this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);獲取文件的內容并返回給用戶。

跟進write()

跟進writeInternal,之后再跳到writeContent

跟進resource.getInputSream()

可以看到,這里使用openConnection創建一個URLConnection實例,也是在openConnection方法內,會自動decode,把%5c解碼成\,然后返回文件的InputStream對象,最終讀取內容返回給用戶。

注意事項

  1. 這個漏洞是可以在 Tomcat 下觸發的,因為payload的雙URL編碼的。

  2. 在Spring Framework 大于5.0.1的版本(我的測試環境5.0.4),雙URL編碼payload是不行的,單次URL編碼的payload的卻是可以的,這種情況下該漏洞就無法在Tomcat下觸發了,因為在默認情況下Tomcat遇到包含%2f(/)%5c(\)的URL直接http 400,在 jetty 下是可以觸發的。

至于為什么雙URL編碼不行,是因為org.springframework.web.servlet.resource.PathResourceResolvergetResource()多了一個encode操作。

如果是雙URL編碼payload的進來,在獲取path的時候解碼一次,經過一次isInvalidPath判斷,然后進入到PathResourceResolvergetResource(),也就是上圖的位置,這里又會重新編碼一次,又回到了雙編碼的情況了。最后在文件判斷是否存在exists()方法的時候,getSchemeSpecificPart()只能解碼一次,之后是無法讀取到文件的,也就是文件不存在。

所以這里要使用單次編碼才行。

補丁分析

看官方的補丁,是在ResourceHttpRequestHandlergetResource()里面把processPath重寫了

在進入isInvalidPath之前調用processPath函數對path處理,替換反斜線為斜線,刪掉多余斜線,從而導致在isInvalidPath里面校驗不通過。如果使用雙編碼方式的話,會經過isInvalidEncodedPath,里面會先對path解碼,然后調用processPath處理,最后經過isInvalidPath,同樣無法通過檢查。

漏洞修復

  • Spring Framework 5.*(5.0到5.0.4)版本,建議更新到5.0.5版本
  • Spring Framework 4.3.*(4.3到4.3.14)版本,建議更新到4.3.15版本
  • 不再受支持的舊版本,建議更新到4.3.15版本或5.0.5版本

參考鏈接


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/665/