在大多数情况下,小工具文件通常使用 JSON.parse
在前端解析。这意味着上传的文件必须是JSON.parse
的有效输入。如果查看V8(https://github.com/v8/v8/blob/refs/tags/13.2.67/src/json/json-parser.cc)的实现, 一个有效的JSON输入可以是:字符串、数字、true
、false
、null
、数组
、对象
。解析器会跳过起始的空白字符,例如: ' '
、't'
、'r'
、'n'
。
此外, JSON对象(键或值)中的控制字符和双引号会破坏JSON结构, 必须进行转义。只有严格遵循这些限制条件, 才能被正确解析为JSON。不同的应用程序使用库或工具来验证文件, 这些库或工具旨在检测文件的MIME类型、文件结构或Magic Bytes。通过巧妙的制作符合这些条件的文件, 可以做到欺骗其安全验证并绕过限制。下面的过程将以探索绕过PDF和图像文件上传限制。
许多上传机制中的基本检查涉及验证文件的MIME类型。这通常通过检查Content-Type头或文件本身来完成。然而, 这些检查通常可以通过操控文件的结构或文件头来绕过。
(1)绕过mmmagic验证
mmmagic
库通常用于Node.js应用程序中, 利用Magic库检测文件类型, 可以使用以下代码验证PDF文件:
async function checkMMMagic(binaryFile) {
var magic = new Magic(mmm.MAGIC_MIME_TYPE);
const detectAsync = (binaryFile) => {
return new Promise((resolve, reject) => {
magic.detect.call(magic, binaryFile, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
const result = await detectAsync(binaryFile);
const isValid = (result === 'application/pdf')
if (!isValid) {
throw new Error('mmmagic: File is not a PDF : ' + result);
}
}
%PDF
特征字节, 根据PDF文件格式规范, 该特征不需要位于文件的最开头。此处可以在JSON对象的前1024字节内包装一个PDF头, 这将是一个有效的JSON文件, 但库会将其视为一个PDF文件。这给了攻击者一个欺骗的机会, 使其接受JSON文件为一个有效的PDF文件, 同时仍允许浏览器将其解析为JSON, 以下是一个例子:{ "id" : "../CSPT_PAYLOAD", "%PDF": "1.4" }
%PDF
头出现在前1024字节内, mmmagic
库就会将此文件当作PDF文件接收, 并且它仍然可以在客户端被解析为JSON。pdflib库不仅要求文件包含%PDF
头部, 还会用来验证整个PDF结构的有效性。
async function checkPdfLib(binaryFile) {
let pdfDoc = null
try {
pdfDoc = await PDFDocument.load(binaryFile);
} catch (error) {
throw new Error('pdflib: Not a valid PDF')
}
if (pdfDoc.getPageCount() == 0) {
throw new Error('pdflib: PDF doesn't have a page');
}
}
为了绕过这一验证,可以创建一个对于 pdflib 来说有效的 PDF 文件,同时仍然符合 CSPT 所需的 JSON 结构。技巧在于将 PDF 对象定义之间的 %0A
(换行符)替换为空格 %20
。这样,文件可以被 pdflib 识别为有效的 PDF,但仍然可以被解释为 JSON。xref 表不需要修复,因为最终目标不是浏览PDF,而是通过上传验证。
以下是一个例子:
{"_id":"../../../../CSPT?","bypass":"%PDF-1.3 1 0 obj << /Pages 2 0 R /Type /Catalog >> endobj 2 0 obj << /Count 1 /Kids [ 3 0 R ] /Type /Pages >> endobj 3 0 obj << /Contents 4 0 R /MediaBox [ 0 0 200 200 ] /Parent 2 0 R /Resources << /Font << /F1 5 0 R >> >> /Type /Page >> endobj 4 0 obj << /Length 50 >> stream BT /F1 10 Tf 20 100 Td (CSPT) Tj ET endstream endobj 5 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj xref 0 6 0000000000 65535 f 0000000009 00000 n 0000000062 00000 n 0000000133 00000 n 0000000277 00000 n 0000000370 00000 n trailer << /Size 6 /Root 1 0 R >> startxref 447 %%EOF "}
在某些环境中,file
命令或基于 file
的库被用来检测文件类型。
async function checkFileCommand(binaryFile) {
const tmpobj = tmp.fileSync();
fs.writeSync(tmpobj.fd, binaryFile);
fs.closeSync(tmpobj.fd);
output = execFileSync('file', ["-b", "--mime-type", tmpobj.name])
const isValid = (output.toString() === 'application/pdfn')
if (!isValid) {
throw new Error(`content - type: File is not a PDF : ${output}`);
}
tmpobj.removeCallback();
}
mmmagic
的不同之处在于,file
命令在检查magic字节之前,会尝试将文件解析为 JSON。如果成功,文件会被认为是 JSON,之后就不会执行其他类型检查。因此,不能像在 mmmagic
中一样使用相同的技巧。然而,file
命令有一个已知的文件大小处理限制。下面是 man file
文件中的摘录:-P, --parameter name=value
Set various parameter limits.
Name Default Explanation
bytes 1048576 max number of bytes to read from file
elf_notes 256 max ELF notes processed
elf_phnum 2048 max ELF program sections processed
elf_shnum 32768 max ELF sections processed
encoding 65536 max number of bytes for encoding evaluation
indir 50 recursion limit for indirect magic
name 60 use count limit for name/use magic
regex 8192 length limit for regex searches
这里可以利用这个限制,通过填充空白字符(如空格或制表符)将文件扩展,直到它超过解析限制。一旦文件超过限制,file_is_json
函数将失败,文件将被分类为其他类型(例如 PDF)。
看一个例子, 这里可以像这样创建一个文件:
{
"_id": "../../../../CSPT?",
"bypass": "%PDF-1.3 1 0 obj << /Pages 2 0 R /Type /Catalog >> endobj 2 0 obj << /Count 1 /Kids [ 3 0 R ] /Type /Pages >> endobj 3 0 obj << /Contents 4 0 R /MediaBox [ 0 0 200 200 ] /Parent 2 0 R /Resources << /Font << /F1 5 0 R >> >> /Type /Page >> endobj 4 0 obj << /Length 50 >> stream BT /F1 10 Tf 20 100 Td (CSPT) Tj ET endstream endobj 5 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj xref 0 6 0000000000 65535 f 0000000009 00000 n 0000000062 00000 n 0000000133 00000 n 0000000277 00000 n 0000000370 00000 n trailer << /Size 6 /Root 1 0 R >> startxref 447 %%EOF <..A LOT OF SPACES..> "
}
当上传时,file
命令将无法解析这个大的 JSON 结构,导致它回退到常规的文件检测机制,将文件当作 PDF 处理。
图像上传常常使用像 file-type
这样的库来验证文件格式。以下代码试图确保上传的文件是图像:
const checkFileType = async (binary) => {
const { fileTypeFromBuffer } = await fileType();
const type = await fileTypeFromBuffer(binary);
const result = type.mime;
const isValid = result.startsWith('image/');
if (!isValid) {
throw new Error('file-type: File is not an image : ' + result);
}
};
有时,这些库会在预定义的偏移量处检查特定的magic字节。在这个例子中,file-type
会检查偏移量 8 处是否存在magic节:
https://github.com/sindresorhus/file-type/blob/v19.6.0/core.js#L358C1-L363C1
if (this.checkString('WEBP', {offset: 8})) {
return {
ext: 'webp',
mime: 'image/webp',
};
}
{"aaa":"WEBP","_id":"../../../../CSPT?"}
file-type
的图像检查,同时仍然包含可以用于 CSPT 的 JSON 数据。原文始发于微信公众号(二进制空间安全):突破PDF和图像文件上传限制并利用的几种思路
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论