Apache Commons Fileupload Dos漏洞分析
Apache Commons Fileupload是一個用於處理檔案上傳的庫。本文就分析Apache Commons Fileupload的兩個老洞,包括CVE2014-0050和CVE-2016-3092。兩個漏洞都是由同一個地方導致的,都是由於對boundary的處理的邏輯不夠嚴謹造成的。CVE2014-0050是由於對boundary的處理沒有校驗出現無限迴圈而導致的Dos漏洞;CVE-2016-3092則是賦值操作存在問題,程式會不斷的以boundary的長度來開闢記憶體空間進而導致記憶體的耗盡。CVE-2014-0050是在1.3.1之前的版本,CVE-2016-3092出現在1.3.2之前的版本,
環境搭建
搭建一個簡單的SpringMVC的專案,使用gradle匯入Apache Commons Fileupload的版本1.3版本,如下:
dependencies { /** *omit other dependencies */ compile group: 'commons-fileupload', name: 'commons-fileupload', version: '1.3' }
在路由中新增一個對於檔案上傳處理的路由,如下:
@RequestMapping(value="/fileupload") publicString uploadFileHandler(HttpServletRequest request) { boolean flag = false; //判斷是否是檔案上傳請求 if(ServletFileUpload.isMultipartContent(request)){ // 建立檔案上傳處理器 DiskFileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload upload = new ServletFileUpload(factory); //限制單個上傳檔案的大小 upload.setFileSizeMax(1L<<24); try { List<FileItem> list = upload.parseRequest(request); for (FileItem item : list) { // 普通表單項 if (item.isFormField()) { String name = item.getFieldName(); String value = item.getString("UTF-8"); System.out.println(name + " : " + value); } else {// 檔案表單項 // 檔名 String fileName = item.getName(); // 生成唯一檔名 fileName = UUID.randomUUID().toString() + "#" + fileName; // 獲取上傳路徑:專案目錄下的upload資料夾(先建立upload資料夾) String basePath = request.getServletContext().getRealPath("/upload"); // 建立檔案物件 File file = new File(basePath, fileName); // 寫檔案(儲存) item.write(file); // 刪除臨時檔案 item.delete(); } } } catch (FileUploadException e) { System.out.println("上傳檔案過大"); } catch (IOException e) { System.out.println("檔案讀取出現問題"); } catch (Exception e) { e.printStackTrace(); } } return flag? "success":"error"; }
配合一個簡單的檔案上傳的頁面,如下:
<form method="post" action="/fileupload" enctype="multipart/form-data"> 選擇一個檔案: <input type="file" name="uploadFile" /> <br/><br/> <input type="submit" value="上傳" /> </form>
至此,整個漏洞的環境搭建完畢。
CVE2014-0050漏洞分析
在通過CVE2014-0050的 ofollow,noindex">修復commit 分析發現,其中的修復關鍵地方是對 org.apache.commons.fileupload.MultipartStream.java
進行了如下的修改:

增加程式碼的含義是對 this.boundaryLength
進行了限制,如果超過了 bufSize
則丟擲異常。 bufSize
的長度的定義是 DEFAULT_BUFSIZE = 4096
,預設是是4096的長度。分析下 org.apache.commons.fileupload.MultipartStream.java::MultipartStream()
程式碼如下:
MultipartStream(InputStream input, byte[] boundary, int bufSize, ProgressNotifier pNotifier) { this.input = input; this.bufSize = bufSize; this.buffer = new byte[bufSize]; this.notifier = pNotifier; // We prepend CR/LF to the boundary to chop trailing CR/LF from // body-data tokens. this.boundary = new byte[boundary.length + BOUNDARY_PREFIX.length]; this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length; this.keepRegion = this.boundary.length; System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0, BOUNDARY_PREFIX.length); System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length, boundary.length); head = 0; tail = 0; }
其中的 this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
就是 boundary
的長度加上預設的 BOUNDARY_PREFIX.length
(值為4)。最終程式執行至 org.apache.commons.fileupload.MultipartStream::makeAvailable()
,如下:
private int makeAvailable() throws IOException { if (pos != -1) { return 0; } // Move the data to the beginning of the buffer. total += tail - head - pad; System.arraycopy(buffer, tail - pad, buffer, 0, pad); // Refill buffer with new data. head = 0; tail = pad; for (;;) { int bytesRead = input.read(buffer, tail, bufSize - tail); if (bytesRead == -1) { // The last pad amount is left in the buffer. // Boundary can't be in there so signal an error // condition. final String msg = "Stream ended unexpectedly"; throw new MalformedStreamException(msg); } if (notifier != null) { notifier.noteBytesRead(bytesRead); } tail += bytesRead; findSeparator(); int av = available(); if (av > 0 || pos != -1) { return av; } } }
根據 input.read(buffer, tail, bufSize - tail)
返回的結果決定是否退出。但是當我們的自定義的boundary的長度超過了4096之後, int bytesRead = input.read(buffer, tail, bufSize - tail)
中的 bytesRead
永遠不會返回 -1
,最終的結果如下所示:

漏洞的修復就是通過增加對boundary的判斷實現了,禁止boundary的長度過長,否則就會丟擲異常。如下:

CVE-2016-3092漏洞分析
首先分析一下的 commit資訊 ,如下所示:

將賦值操作放到了判斷boundary的長度之後,同時通過 this.bufSize = Math.max(bufSize, boundaryLength*2);
增加了 bufSize
的長度。之所以進行這樣調整的原因是在於進行判斷之前就會進行大量地賦值操作,將資料放到記憶體當中,增加了系統的開銷,這個漏洞如果要觸發就需要傳送大量地請求,設定boundary恰好滿足條件,即為4090。這樣就會大量地消耗系統的記憶體。
根據 Apache Commons Fileupload 1.3.1 DOS(CVE-2016-3092) 的提示,構造一個boundary大小為1000000位元組的資料包,迴圈傳送500次請求對1.3.1的版本進行測試就會出現記憶體大量消耗的問題;
總結
看似簡單的程式碼很有可能會存在安全問題,這也告誡我們不能隨意地將使用者的輸入放入到資料庫中進行查詢或者是放置到記憶體中。