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

admin 2024年5月21日20:04:02评论60 views字数 6107阅读20分21秒阅读模式
CVE-2024-4367 - PDF.js 中任意 JavaScript 执行

太长不看

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

如果您是一名开发 JavaScript/Typescript 应用程序处理 PDF 文件的开发人员,我们建议检查您是否(间接地)使用了 PDF.js 的易受攻击版本。有关缓解细节,请参阅本文末尾。

介绍

PDF.js有两个常见的用例。首先,它是Firefox浏览器内置的PDF查看器。如果你使用Firefox,并且曾经下载或浏览过PDF文件,你一定见过它的运行。其次,它被打包成一个名为pdfjs-dist的Node模块,根据NPM的数据显示,每周有大约270万次下载。在这种形式下,网站可以使用它来提供嵌入式的PDF预览功能。从Git托管平台到笔记应用程序,都在使用它。你现在想到的那个应用可能就是在使用PDF.js。PDF 格式以其复杂而闻名。支持各种媒体类型、复杂的字体渲染甚至基本的脚本编写,PDF 阅读器是漏洞研究人员的常见目标。由于存在大量的解析逻辑,难免会出现一些错误,PDF.js 也不例外。然而,它的独特之处在于它是用 JavaScript 编写的,而不是 C 或 C++。这意味着不存在内存损坏问题的机会,但正如我们将看到的,它也带来了自己的一套风险。

字形渲染

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

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

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

从攻击者的角度来看,这真的很有趣:如果我们能够以某种方式控制进入 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();

在这一点上,我们可以审计字体解析代码以及可以由字形生成的各种命令和参数,比如 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 解析器 — 用于其他几种字体格式 — 在这方面是类似的。总的来说,看起来我们确实受限于数字。

然而,事实证明,这个矩阵可能有不止一个潜在的起源。显然,还可以在 PDF 的元数据对象中指定一个自定义 FontMatrix 值,而不是在字体之外!仔细观察 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格式中,字体定义由几个对象组成。这些对象包括FontFontDescriptor和实际的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

尝试执行此操作时,最初看起来似乎不起作用,因为生成的 Function 主体中的 transform 操作仍然使用默认矩阵。然而,这是因为字体文件本身正在覆盖该值。幸运的是,当使用没有内部 FontMatrix 定义的 Type1 字体时,PDF 指定的值是权威的,因为 fontMatrix 值不会被覆盖。

现在我们可以从一个 PDF 对象中控制这个数组,我们拥有我们想要的所有灵活性,因为 PDF 支持的不仅仅是数字类型的基元。让我们尝试插入一个字符串类型的值,而不是一个数字(在 PDF 中,字符串是由括号括起来的):

/FontMatrix [1 2 3 4 5 (foobar)]

而且,它明显地插入了 Function 的body!

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 - PDF.js 中任意 JavaScript 执行CVE-2024-4367 的利用

你可以在这里找到一个概念验证的PDF文件。它可以很容易地使用常规文本编辑器进行修改。为了演示JavaScript运行的上下文,alert会显示window.origin的值。有趣的是,这并不是你在URL栏中看到的file://路径(如果你已经下载了文件)。相反,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 设置 isEvalSupported 设置为 false 。这将禁用易受攻击的代码路径。如果您有严格的内容安全策略(禁用使用 eval 和 Function 构造函数),则漏洞也无法触及。

时间表

  • 2024 年 4 月 26 日 - 漏洞向 Mozilla 披露 

  • 2024 年 4 月 29 日 - PDF.js v4.2.67 发布到 NPM,修复了问题。

  • 2024 年 5 月 14 日 - 发布了 Firefox 126、Firefox ESR 115.11 和 Thunderbird 115.11,包括修复版本的 PDF.js。

  • 2024 年 5 月 20 日-发布此博文

原文始发于微信公众号(独眼情报):CVE-2024-4367 – PDF.js 中任意 JavaScript 执行

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年5月21日20:04:02
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2024-4367 - PDF.js 中任意 JavaScript 执行https://cn-sec.com/archives/2763298.html

发表评论

匿名网友 填写信息