绕过文件上传限制利用客户端路径遍历
在我之前的博客文章中,我演示了如何利用JSON文件作为客户端路径遍历(CSPT)的小工具来执行跨站请求伪造(CSRF)。那个例子很简单,因为没有强制执行文件上传限制。然而,现实世界的应用程序通常会对文件上传施加限制以确保安全。
在这篇文章中,我们将探讨如何绕过其中一些机制来实现相同的目标。我们将涵盖常见的文件验证方法以及如何颠覆它们。
约束条件
在大多数情况下,小工具文件将在前端使用JSON.parse进行解析。这意味着我们的文件必须是JSON.parse的有效输入。
如果我们查看V8的实现,一个有效的JSON输入可以是:
- 字符串
- 数字
- true
- false
- null
- 数组
- 对象
解析器会跳过起始的空白字符,例如:
此外,JSON对象(键或值)中的控制字符和双引号会破坏JSON结构,必须进行转义。我们的小工具文件必须遵循这些限制才能被解析为JSON。
不同的应用程序使用设计用于检测文件的MIME类型、文件结构或魔术字节的库或工具来验证文件。通过创造性地制作满足这些条件的文件,我们可以欺骗这些验证并绕过限制。
让我们探讨如何绕过各种文件上传机制,以维护用于CSPT的有效JSON负载,同时满足文件格式要求,例如PDF或图像。
绕过PDF检查以上传JSON文件
许多上传机制中的基本检查涉及验证文件的MIME类型。这通常通过Content-Type头或检查文件本身来完成。然而,这些检查通常可以通过操纵文件的结构或头来绕过。
绕过mmmagic验证
mmmagic库通常用于Node.js应用程序中,基于Magic数据库检测文件类型。可以使用以下代码验证PDF文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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魔术字节。它使用此处定义的Magic检测规则。然而,根据PDF规范,此魔术数字不需要位于文件的最开头。
我们可以在JSON对象的前1024字节内包装一个PDF头。这将是一个有效的JSON文件,被该库视为PDF。这使我们能够欺骗库接受上传为有效的PDF,同时仍然允许浏览器将其解析为JSON。以下是一个示例:
1
|
{ "id" : "../CSPT_PAYLOAD", "%PDF": "1.4" }
|
只要%PDF头出现在前1024字节内,mmmagic库就会接受此文件作为PDF,但它仍然可以在客户端解析为JSON。
绕过pdflib验证
pdflib库需要的不仅仅是%PDF头。它可以用于验证整体PDF结构。
1
2
3
4
5
6
7
8
9
10
11
12
|
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');
}
}
|
技术:
要绕过此限制,我们可以创建一个有效的PDF(对于pdflib),同时仍然符合CSPT所需的JSON结构。
技巧是将PDF对象定义之间的%0A(换行符)字符替换为空格%20。这允许文件被识别为pdflib的有效PDF,但仍然可以解释为JSON。xref表不需要修复,因为我们的目标不是显示PDF,而是通过上传验证。
以下是一个示例:
1
|
{"_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 "}
|
虽然此PDF在最近的PDF查看器中不会渲染,但pdflib可以读取它并通过文件上传检查。
绕过文件命令验证
在某些环境中,使用文件命令或基于文件的库来检测文件类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
async function checkFileCommand(binaryFile) {
//Write a temporary file
const tmpobj = tmp.fileSync();
fs.writeSync(tmpobj.fd, binaryFile);
fs.closeSync(tmpobj.fd);
// Exec file command
output = execFileSync('file', ["-b", "--mime-type", tmpobj.name])
const isValid = (output.toString() === 'application/pdf\n')
if (!isValid) {
throw new Error(`content - type: File is not a PDF : ${output}`);
}
tmpobj.removeCallback();
}
|
技术:
与mmmagic的区别在于,在检查魔术字节之前,它尝试将文件解析为JSON。如果成功,该文件被视为JSON,并且不会执行其他检查。因此,我们不能使用与mmmagic相同的技巧。然而,文件命令对其可以处理的文件大小有已知限制。这是文件命令手册的摘录。
1
2
3
4
5
6
7
8
9
10
11
12
|
-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)。
例如,我们可以创建如下文件:
1
2
3
4
|
{
"_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 <..许多空格..> "
}
|
上传时,文件命令将无法解析此大型JSON结构,导致其回退到正常文件检测并将文件视为PDF。
使用WEBP格式绕过图像上传文件类型限制
图像上传通常使用像file-type这样的库来验证文件格式。以下代码尝试确保上传的文件是图像。
1
2
3
4
5
6
7
8
9
10
11
|
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);
}
};
|
技术:
有时,这些库检查预定义偏移处的特定魔术数字。在此示例中,file-type检查魔术字节是否出现在偏移8处:
https://github.com/sindresorhus/file-type/blob/v19.6.0/core.js#L358C1-L363C1
1
2
3
4
5
6
|
if (this.checkString('WEBP', {offset: 8})) {
return {
ext: 'webp',
mime: 'image/webp',
};
}
|
由于我们控制起始字节,我们可以构建一个有效的JSON文件。我们可以制作一个JSON对象,将魔术字节(WEBP)放置在正确的偏移处,允许文件通过验证作为图像,同时仍然是有效的JSON对象。以下是一个示例:
1
|
{"aaa":"WEBP","_id":"../../../../CSPT?"}
|
此文件将通过file-type的图像检查,同时仍然包含可用于CSPT的JSON数据。
结论
绕过文件上传限制并不是新事物,但我们想分享一些过去几年中在实施文件上传限制时上传JSON小工具的方法。我们使用它们来执行CSPT2CSRF或任何其他漏洞利用(XSS等),但它们也可以应用于其他上下文。不要犹豫,深入研究第三方源代码以了解其工作原理。
所有这些示例和文件都已包含在我们的CSPTPlayground中。该游乐场不仅包括CSPT2CSRF,还包括其他示例,例如JSONP小工具或开放重定向。这是基于Isira Adithya(@isira_adithya)和Justin Gardner(@Rhynorater)收到的反馈构建的。非常感谢!
更多信息
如果您想了解更多关于我们其他研究的信息,请查看我们的博客,在X(@doyensec)上关注我们,或随时通过info@doyensec.com与我们联系,以获取有关我们如何帮助您的组织“构建安全”的更多信息。