CVE-2024-4367 – PDF.js 中的任意 JavaScript 执行

2,405 阅读10分钟

pdfjs_header_mid.png

这篇文章详细介绍了Codean Labs 发现的 PDF.js 漏洞CVE-2024-4367。PDF.js是一个基于 JavaScript 的 PDF 查看器,由 Mozilla 维护。此漏洞允许攻击者在打开恶意 PDF 文件时立即执行任意 JavaScript 代码。这会影响所有 Firefox 用户(<126),因为 Firefox 使用 PDF.js 来显示 PDF 文件,但也会严重影响许多(间接)使用 PDF.js 实现预览功能的基于 Web 和 Electron 的应用程序。

如果您是以任何方式处理 PDF 文件的基于 JavaScript/Typescript 的应用程序的开发人员,我们建议您检查您是否没有(间接)使用易受攻击的 PDF.js 版本。请参阅本文末尾的缓解措施详细信息。

介绍

PDF.js 有两种常见用例。首先,它是 Firefox 的内置 PDF 查看器。如果您使用 Firefox,并且曾经下载或浏览过 PDF 文件,那么您就会看到它的实际作用。其次,它被捆绑到名为的 Node 模块中pdfjs-dist,根据 NPM 的数据,每周下载量约为 270 万次。以这种形式,网站可以使用它来提供嵌入式 PDF 预览功能。从 Git 托管平台到笔记应用程序,一切都使用它。您现在想到的那个很可能使用 PDF.js。

PDF 格式非常复杂。由于支持各种媒体类型、复杂的字体渲染甚至基本的脚本,PDF 阅读器是漏洞研究人员的常见目标。由于解析逻辑量如此之大,必然会出现一些错误,PDF.js 也不例外。然而,它的独特之处在于它是用 JavaScript 编写的,而不是 C 或 C++。这意味着不存在内存损坏问题,但正如我们将看到的,它本身也存在一系列风险。

字形渲染

您可能会惊讶地发现,这个错误与 PDF 格式 (JavaScript!) 的脚本功能无关。相反,这是字体渲染代码中特定部分的疏忽。

PDF 中的字体有多种不同的格式,其中一些比其他格式更晦涩难懂(至少对我们来说是这样)。对于 TrueType 等现代格式,PDF.js 主要依靠浏览器自己的字体渲染器。在其他情况下,它必须手动将字体描述转换为页面上的曲线。为了优化性能,每个字形都预编译了一个路径生成器Function函数。如果支持,可以通过创建一个 JavaScript对象来完成,该对象具有一个主体(jsBuf),其中包含构成路径的指令:

// 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("")
  ));
}

从攻击者的角度来看,这真的很有趣:如果我们能够以某种方式控制这些cmds进入Function身体并插入我们自己的代码,它将在呈现这样的字形时立即执行。

好吧,让我们看看这个命令列表是如何生成的。顺着逻辑回到类中,我们CompiledFont找到了方法compileGlyph(...)。此方法cmds使用一些常规命令(savetransform和)初始化数组,并交给方法填充实际的渲染命令:scale``restore``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();

此时,我们可以审核字体解析代码以及字形可以生成的各种命令和参数,例如quadraticCurveTobezierCurveTo,但所有这些似乎都很无害,无法控制除数字以外的任何内容。然而,更有趣的是transform我们上面看到的命令:

{ cmd: "transform", args: fontMatrix.slice() },

这个fontMatrix数组被复制(使用.slice())并插入到对象主体中Function,以逗号连接。代码显然假设它是一个数字数组,但情况总是如此吗?这个数组中的任何字符串都将按字面意思插入,而没有任何引号将其括起来。因此,这充其量会破坏 JavaScript 语法,最坏的情况是允许任意代码执行。但我们甚至可以控制到fontMatrix那种程度的内容吗?

输入 FontMatrix

的值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
>>
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

dict上述代码中的引用指的是对象。Font因此,我们应该能够定义一个自定义FontMatrix数组,如下所示:

1 0 obj
<<
  /Type /Font
  /Subtype /Type1
  /FontDescriptor 2 0 R
  /BaseFont /FooBarFont
  /FontMatrix [1 2 3 4 5 6]   % <-----
>>
endobj

尝试执行此操作时,最初看起来似乎不起作用,因为transform生成的Function主体中的操作仍使用默认矩阵。但是,发生这种情况是因为字体文件本身正在覆盖该值。幸运的是,当使用没有内部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();

漏洞利用及影响

现在,插入任意 JavaScript 代码只需正确处理语法即可。这是一个触发警报的经典示例,首先关闭函数c.transform(...),然后使用尾随括号:

/FontMatrix [1 2 3 4 5 (0); alert('foobar')]

结果正如预期:

CVE-2024-4367 的利用

CVE-2024-4367 的利用

您可以在此处找到概念验证 PDF 文件(已更新,请参阅下面的受影响版本部分)。为了演示 JavaScript 运行的上下文,警报将显示 的值window.origin。有趣的是,这不是file://您在 URL 栏中看到的路径(如果您已经下载了文件)。相反,PDF.js 在原点 下运行resource://pdf.js。这可以阻止访问本地文件,但在其他方面略有特权。例如,可以调用文件下载(通过对话框),甚至可以“下载”任意file://URL。此外,打开的 PDF 文件的真实路径存储在 中window.PDFViewerApplication.url,这允许攻击者监视打开 PDF 文件的人,不仅可以了解他们何时打开文件以及他们对文件的操作,还可以了解文件在他们的机器上的位置。

在嵌入 PDF.js 的应用程序中,影响可能更严重。如果没有采取任何缓解措施(见下文),这实际上会为攻击者提供包含 PDF 查看器的域的XSS原语。根据应用程序的不同,这可能会导致数据泄露、以受害者的名义执行恶意操作,甚至完全接管帐户。在没有正确沙盒 JavaScript 代码的 Electron 应用程序上,此漏洞甚至会导致本机代码执行(!)。我们发现至少有一款流行的 Electron 应用程序存在这种情况

减轻

在 Codean Labs,我们意识到跟踪此类依赖项及其相关风险非常困难。我们很高兴为您分担这一负担。我们以高效、彻底和人性化的方式执行应用程序安全评估,让您专注于开发。单击此处了解更多信息。

针对此漏洞的最佳缓解措施是将 PDF.js 更新至 4.2.67 或更高版本。大多数包装器库react-pdf 也发布了修补版本。由于一些更高级别的 PDF 相关库静态嵌入了 PDF.js,我们建议递归检查node_modules文件夹中调用的文件pdf.js以确保安全。PDF.js 的无头用例(例如,在服务器端从 PDF 中获取统计数据和数据)似乎不受影响,但我们没有彻底测试这一点。也建议更新。

此外,一个简单的解决方法是将 PDF.js 设置设为isEvalSupportedfalse这将禁用易受攻击的代码路径。如果您有严格的内容安全策略eval(禁用和构造函数的使用Function),则该漏洞也无法利用。

受影响的版本

Rob Wu的分析(经许可复制于下方)表明,自 PDF.js 首次发布以来,易受攻击的代码路径就一直存在,但由于拼写错误,在 2016 年和 2017 年发布的几个版本中无法访问。值得注意的是,2017 年及之前标记为不受影响的版本仍然容易受到另一个漏洞(CVE-2018-5158)的攻击,这意味着它们不安全使用。

  • 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 首次公开发布)

Rob 还更新了概念验证 PDF ,使其适用于所有受影响的版本,包括 v1.4.20 及以下版本。请务必使用此最新版本来测试您的 PDF.js 实例是否受到影响(同时考虑其他缓解措施)。原始的纯文本但不太通用的 PoC 可在此处找到。

时间线

  • 2024-04-26 – 向 Mozilla 披露漏洞
  • 2024-04-29 – PDF.js v4.2.67 发布到 NPM,修复该问题
  • 2024-05-14 – Firefox 126、Firefox ESR 115.11 和 Thunderbird 115.11 发布,包括 PDF.js 的修复版本
  • 2024-05-20 – 发布此博文
  • 2024-05-22 – 添加了详细的版本信息并更新了 PoC,由 Rob Wu 提供

原文:codeanlabs.com/blog/resear…