淺談 Springboot 中的文件上傳
引言
在JavaWeb應用中,任意文件上傳一直是關注的重點,攻擊者通過上傳惡意jsp文件,可以獲取服務器權限。但是在Springboot框架對JSP解析存在一定的限制。
Spring官方原文如下,大概意思是jsp對內嵌的容器的支持不太友好,推薦使用thymeleaf這類的模版引擎進行渲染。

那么針對Springboot應用,即使存在任意文件上傳缺陷,按照傳統的思路直接上傳jsp文件,也是無法達到理想的效果的。
下面通過查看其具體的實現方式來看看有沒有相關的利用思路,同時在日常項目開發中應該注意些什么。
Springboot文件上傳的實現
首先看看在Springboot中如何實現文件上傳功能,在網上找了個教程,Controller的代碼如下,Spring會自動解析multipart/form-data請求,將multipart中的對象封裝到MultipartRequest對象中:
@RequestMapping(value={"/uploadFile"},method={RequestMethod.POST})
public String uploadFile(MultipartFile file,String type,HttpServletResponse
response) throws
Exception{
String UPLOADED_FOLDER="/resource/upload/";
if(!file.isEmpty()){
String
path = UPLOADED_FOLDER + file.getOriginalFilename();
File
targetFile = new
File(path);
FileUtils.inputStreamToFile(file.getInputStream(),targetFile);
......
......
}
}
大致是通過getOriginalFilename()方法獲取文件名,然后使用File對象創建對應的文件。接下來看看Springboot是如何解析multipart請求并封裝OriinalFilename的。
SpringBoot在
MultipartAutoConfiguration自動裝配了MultipartResolver來對multipart請求進行解析:
@Configuration
@ConditionalOnClass({ Servlet.class,StandardServletMultipartResolver.class,
MultipartConfigElement.class})
@ConditionalOnProperty(prefix \= "spring.http.multipart", name= "enabled", matchIfMissing =true)
@EnableConfigurationProperties(MultipartProperties.class) public classMultipartAutoConfiguration { private final MultipartPropertiesmultipartProperties; public MultipartAutoConfiguration(MultipartPropertiesmultipartProperties) { this.multipartProperties =multipartProperties;
}
@Bean
@ConditionalOnMissingBeanpublic MultipartConfigElement multipartConfigElement() { returnthis.multipartProperties.createMultipartConfig();
}
@Bean(name \=DispatcherServlet.MULTIPART\_RESOLVER\_BEAN\_NAME)
@ConditionalOnMissingBean(MultipartResolver.class) publicStandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolvermultipartResolver \= new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());return multipartResolver;
}
}
可以看到其默認裝配的解析器是org.springframework.web.multipart.support。StandardServletMultipartResolver。查看對應的實現。
StandardServletMultipart Resolver的處理方式
對應的spring-web組件版本為5.3.3。使用StandardServletMultipartResolver解析multipart請求的關鍵過程如下:
關鍵multipart請求的解析方法parseRequest:
private void parseRequest(HttpServletRequestrequest)
{
try
{
Collection<Part>parts = request.getParts();
this.multipartParameterNames = newLinkedHashSet(parts.size());
MultiValueMap<String,MultipartFile> files = new LinkedMultiValueMap(parts.size());
for (Part part : parts)
{
String headerValue =part.getHeader("Content-Disposition");
ContentDisposition disposition =ContentDisposition.parse(headerValue);
String filename =disposition.getFilename();
if (filename != null)
{
if((filename.startsWith("=?")) && (filename.endsWith("?="))){
filename =MimeDelegate.decode(filename);
}
files.add(part.getName(), newStandardMultipartFile(part, filename));
}
else
{
this.multipartParameterNames.add(part.getName());
}
}
setMultipartFiles(files);
}
catch (Throwable ex)
{
handleParseFailure(ex);
}
}
主要的解析方法在
org.springframework.http.ContentDisposition的parse方法,在這里對相關的http內容進行了處理,獲取文件名的關鍵內容如下。
如果傳入的multipart請求無法直接使用filename=解析出文件名,Spring還會使用content-disposition解析一次(使用filename*=解析文件名):
public static ContentDisposition parse(StringcontentDisposition)
{
......
for (int i = 1; i < parts.size();i++)
{
String part =(String)parts.get(i);
int eqIndex =part.indexOf('=');
if (eqIndex!= -1)
{
String attribute =part.substring(0, eqIndex);
String value =(part.startsWith("\"", eqIndex + 1)) &&(part.endsWith("\"")) ? part.substring(eqIndex + 2,part.length() - 1) : part.substring(eqIndex + 1);
if(attribute.equals("name"))
{
name = value;
}
else if(attribute.equals("filename*"))
{
int idx1 =value.indexOf('\'');
int idx2 = value.indexOf('\'',idx1 + 1);
if ((idx1 != -1) &&(idx2 != -1))
{
charset =Charset.forName(value.substring(0, idx1).trim());
Assert.isTrue((StandardCharsets.UTF_8.equals(charset)) ||(StandardCharsets.ISO_8859_1.equals(charset)), "Charset should be UTF-8 orISO-8859-1");
filename =decodeFilename(value.substring(idx2 + 1), charset);
}
else
{
filename =decodeFilename(value, StandardCharsets.US_ASCII);
}
}
else if((attribute.equals("filename")) && (filename ==null))
{
filename = value;
}
這里發現一個點,整個過程沒有對類似…/的路徑進行檢查/過濾,獲取文件名后會實例化StandardMultipartFile方便后續程序調用:
private static classStandardMultipartFile
implements MultipartFile,Serializable
{
......
public StringgetOriginalFilename()
{
return this.filename;
}
......
實例化方法同樣也沒有對原始上傳的filename進行檢查/過濾
相關接口可以通過getOriginalFilename()方法獲得對應的上傳文件名,然后進行文件創建。
未做安全處理的文件上傳
由于獲取的fileName未進行安全處理,在使用File創建文件時,若路徑處path寫入…/…/穿越符號,是可以跨目錄新建文件的:
File file = new File("path")

那么也就是說即使Springboot對jsp存在一定的支持限制,在特定情況下那么可以嘗試上傳定時任務進行權限獲取。
上傳文件名為…/…/…/…/…/…/…/…/…/var/spool/cron/root,結合前面百度到的demo成功反彈shell:

進行了后綴安全檢查的文件上傳
到這里針對Springboot任意文件上傳的缺陷利用已經有一些眉目了。這里發現一個有趣的點。
針對任意文件上傳,在進行業務開發的時候,常常會對后綴進行相關的白名單檢查,如果上傳非法后綴,那么拒絕對應的業務請求,例如如下代碼:
if(!file.isEmpty()){
String Filename =
file.getOriginalFilename();
String suffix =
originalFilename.substring(Filename.lastIndexOf("."));
if(!".xlsx".equals(suffix)&&!".xls".equals(suffix)){
throw
new Exception("非法請求,請導入excel文件");
}
byte[] bytes =
file.getBytes();
String path =ULOADED_FOLDER +
Filename;
}
在進行文件上傳時進行了后綴檢查,如果不是xlsx或者xls后綴的話,拒絕請求。因為/etc/cron.d/目錄下的文件可以任意后綴命名,那么此時可以上傳文件名為“…/…/…/…/…/…/etc/cron.d/test.xls”繞過對應的安全檢查:


上傳成功后,本地監聽端口等待定時任務執行,成功反彈 shell,獲取服務器權限:

其他版本的spring-web組件也是大同小異,例如4版本會通過extractFilename()方法進行multipart請求的處理再封裝,同樣沒有處理目錄穿越符的問題:
private String extractFilename(StringcontentDisposition, String key)
{
if (contentDisposition == null){
return null;
}
int startIndex =contentDisposition.indexOf(key);
if (startIndex == -1) {
return null;
}
String filename =contentDisposition.substring(startIndex + key.length());
if(filename.startsWith("\""))
{
int endIndex =filename.indexOf("\"", 1);
if (endIndex != -1) {
return filename.substring(1,endIndex);
}
}
else
{
int endIndex =filename.indexOf(";");
if (endIndex != -1) {
return filename.substring(0,endIndex);
}
}
return filename;
}
綜上,關于Springboot的文件上傳可以通過結合目錄遍歷的方式嘗試利用,當然也需要滿足一定的利用條件,例如對跨目錄具有寫權限、未重命名文件名等。
同樣的SpringMVC也加載了默認的解析器,一般是CommonsMultipartResolver,查看其對應的處理方式進行對比。
CommonsMultipartResolver的處理方式
解析部分就不細看了,直接查看getOriginalFilename()
https://github.com/spring-projects/spring-... 95-121行
可以看到其針對linux和windows的情況對multipart請求中的原始文件名進行了截斷處理,防止了…/…/帶來的目錄穿越風險:

結語
上述的問題已經報告給了
security@pivotal.io,官方回復如下:

因為某些原因暫不打算在
StandardServletMultipartResolver上做更多的處理了。
建議根據owasp提供的建議,在實現上傳業務時進行更多的安全檢查。
https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
原創:tkswifty SecIN技術平臺
原文鏈接:https://mp.weixin.qq.com/s/wPgdnyv57qBwkHV...