• 聊聊PDF.JS中存在的CVE-2024-4367漏洞
  • 发布于 2个月前
  • 279 热度
    0 评论
引言
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对象来完成的:
// 如果可以,将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的内容呢?

字体矩阵登场
fontMatrix的默认值是[0.001, 0, 0, 0.001, 0, 0],但通常由字体本身在其嵌入的元数据中设置为自定义矩阵。具体做法因字体格式而异。这是一个Type1[1]解析器的例子:
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键时只读取一个数字数组。看来我们确实限于数字。

然而,事实证明,这个矩阵有不止一个潜在的来源。显然,也可以在字体外部指定自定义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
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体中!
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')]
结果正如预期:

你可以在这里[2]找到一个概念验证PDF文件(在下面的《受影响版本》部分有更新)。为了演示JavaScript运行的上下文,警告将显示window.origin的值。有趣的是,这不是你在URL栏中看到的file://路径(如果你下载了文件)。相反,PDF.js在resource://pdf.js的源下运行。这阻止了访问本地文件,但在其他方面稍微有些特权。例如,即使没有适当地限制JavaScript代码,也可以调用文件下载(通过对话框),甚至“下载”任意的file://URL。此外,打开的PDF文件的真实路径存储在window.PDFViewerApplication.url中,允许攻击者监视人们打开PDF文件,了解他们何时打开文件以及他们正在做什么,还可以了解文件在他们的机器上的位置。

在嵌入PDF.js的应用程序中,影响可能更为严重。如果没有适当的缓解措施(见下文),这基本上为攻击者提供了一个XSS[3] 原语,用于包含PDF查看器的域。根据应用程序的不同,这可能导致数据泄露、以受害者的名义执行恶意操作,甚至可能完全接管账户。在没有正确沙箱JavaScript代码的Electron应用程序中,这个漏洞甚至会导致本地代码执行(!)。我们至少发现了一个流行的Electron应用程序就是这种情况。

缓解措施
在Codean Labs,我们意识到跟踪这样的依赖项及其相关风险是困难的。我们很高兴能为你承担这个负担。我们以高效、全面和人性化的方式执行应用程序安全评估,让你可以专注于开发。点击这里[4] 了解更多。

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

此外,一个简单的解决方法是将PDF.js设置isEvalSupported设置为false。这将禁用易受攻击的代码路径。如果你有一个严格的内容安全策略[6](禁用了eval和Function构造函数的使用),那么这个漏洞也是无法到达的。

受影响版本
Rob Wu[7]的分析(在下面重复,经许可)表明,易受攻击的代码路径自PDF.js的第一个版本以来就存在,但由于2016年和2017年发布的几个版本中的一个错别字,它并不可到达。需要注意的是,2017年及之前标记为未受影响的版本仍然容易受到不同的漏洞攻击(CVE-2018-5158[8]),这意味着它们不安全。
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[9],以适用于所有受影响的版本,包括v1.4.20及以下。确保使用这个最新版本来测试你的PDF.js实例是否受到影响(考虑到其他缓解措施)。原始的纯文本但不够通用的PoC可以在这里[10]找到。

时间线
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


本文译自:https://codeanlabs.com/blog/research/cve-2024-4367-arbitrary-js-execution-in-pdf-js/
用户评论