PDF吃瓜群众的陷阱之CVE-2024-4367
PDF吃瓜群众的陷阱之CVE-2024-4367
0x1 概况:
PDF.js作为一个广泛使用的 PDF 渲染库,有时可能会因为处理字体的复杂性而出现许多安全问题,例如:
1. CVE-2013-5598:PDF.js 无法正确处理 IFRAME 元素的附加,读取任意文件或以 chrome 权限执行任意JS代码。
2. CVE-2015-2743:PDF.js 为内部 Workers 提供了过多权限,这可能允许远程攻击者利用同源策略绕过来执行任意代码。
3. CVE-2018-5158:未充分清理 PostScript 计算器函数,允许通过精心设计的 PDF 文件注入恶意 JavaScript。
4. CVE-2021-45086:PDF.js对参数pdf_name的操作会导致跨站点脚本。
可谓是细思极恐,并且最近的CVE-2024-4367也是一个关于PDF.js中的安全漏洞,接下来会着重进行分析,它涉及PDF字体的解析和处理,涉及在解析 PDF 文档中的字体时未能正确处理 fontMatrix,导致潜在的执行任意代码,严重影响了许多基于Web和Electron的应用,这些应用(间接)使用PDF.js来提供预览功能,CVE-2024-4367漏洞主要利用了以下几方面的缺陷:
1. 未正确验证 fontMatrix:
当从 PDF 字典中获取 fontMatrix 时,如果没有正确验证 fontMatrix 的内容和格式,可能会导致任意代码被注入。
2. 不安全的命令执行:
在构建和执行绘制字形的命令时,如果没有正确处理输入数据, 使用eval或new Function处理变量,攻击者可能会构造恶意的 PDF 文件,诱使解析器执行任意代码。
0x2 PDF.js解析PDF流程:
在详细分析CVE-2024-4367漏洞之前,先了解一下PDF.js在解析PDF时的关键流程。
1. PDF.js首先从外界的PDF中获取属性:
在PDF.js在处理PDF文件时,首先会调用PartialEvaluator.translateFont()方法,该方法从与字体相关的PDF字典对象中加载各种属性,并将这些信息组织成一个properties对象,其中有一个加载的关键属性为fontMatrix,加载时若未找到这个属性则会使用默认值FONT_IDENTITY_MATRIX(fontMatrix的默认值是[0.001, 0, 0, 0.001, 0, 0])。
1 | async translateFont({ |
1.1 fontMatrix属性:
在 PDF.js 中,fontMatrix属性是一个6元素的数组,用于定义字体的缩放和变换矩阵,它主要用于将字体坐标系转换到用户空间坐标系。
1.1.1 fontMatrix的常见格式:
fontMatrix: [a, b, c, d, e, f](如fontMatrix: [0.001, 0, 0, 0.001, 0, 0]),对应一个 3x2 的矩阵:
1.1.2 fontMatrix的主要作用:
- 缩放:将字体从字体空间缩放到用户空间。
- 旋转和倾斜:通过调整矩阵中的 b 和 c 值,可以对字体进行旋转和倾斜变换。
- 位移:通过调整 e 和 f 值,可以对字体进行位移操作。
1.2 PDF字典对象:
主要包括descriptor、dict、baseDict等对象,其各自的对象属性将被用于构建和处理字体对象。
1.2.1 descriptor 对象:
- 简介:descriptor对象来自字体描述字典,包含字体度量和外观相关的详细信息。
- 提取:从PDF文件的字体对象中提取,通过get()或getArray()方法从dict对象中提取属性。
1.2.2 dict 对象:
- 简介:dict对象是与字体资源相关的主要字典,包含基本的字体信息和一些默认设置。
- 提取:从PDF文件的字体对象中提取,通过get()或getArray()方法从dict对象中提取属性。
1.2.3 baseDict 对象:
- 简介:baseDict对象包含了字体在PDF文档中的基本加载信息。
- 提取:直接从baseDict对象中提取属性。
2. 根据属性转换为绘制命令(初始化cmds数组):
PDF.js处理的核心就是compileGlyph()函数,它的主要职责就是编译字形,即将字形编码(glyph code)转换为绘制命令集。通过compileGlyph()函数传入的各种属性,函数会创建一个命令集(cmds)列表,包含“保存当前状态”、“变换坐标系”以及“对坐标系进行比例调整”等命令。
1 | compileGlyph(code, glyphId) { |
compileGlyph()函数利用传入的属性进行初始化cmds数组,然后编译生成用于渲染字形的函数,其中:
- { cmd: “save” } 对应 c.save()
- { cmd: “transform”, args: fontMatrix.slice() } 对应 c.transform(0.001,0,0,0.001,0,0)(假设 fontMatrix 是 [0.001, 0, 0, 0.001, 0, 0])
- { cmd: “scale”, args: [“size”, “-size”] } 对应 c.scale(size, -size)
3. 将初始化完的cmds数组转换为JavaScript函数并执行:
在 PDF.js 中,不同的字体格式有不同的处理方式。具体来说,一些字体格式是需要PDF.js自己将字形描述转换为曲线,而其他字体格式则主要依赖于浏览器自身的字体渲染器。
3.1 PDF.js需要自己转换为曲线的字体格式:
3.1.1 Type 1 字体:
- Type 1字体(也称为 PostScript 字体)是由Adobe开发的,需要PDF.js自行解析和渲染这些字体。
- PDF.js需要读取Type 1字体文件,解析其字形描述(通常是基于 Bezier 曲线的数学表达),并将其转换为可以在页面上绘制的路径。
3.1.2 Type 3 字体:
- Type 3 字体是一种可以包含任意PostScript绘图指令的字体格式。
- PDF.js必须手动解析这些指令并绘制相应的字形,因为Type 3字体的灵活性和复杂性常超出了浏览器内置渲染器的能力。
3.2 主要依赖于浏览器字体渲染器的字体格式:
3.2.1 TrueType 字体:
- TrueType字体是一种现代的、广泛支持的字体格式,浏览器通常能够高效地解析和渲染。
- PDF.js 可以依赖浏览器内置的字体渲染器来处理TrueType字体,从而提高性能和一致性。
3.2.2 OpenType 字体:
- OpenType 字体是基于 TrueType 和 PostScript 的扩展格式,广泛用于现代数字字体。
- 浏览器通常支持OpenType字体的解析和渲染,因此PDF.js可以利用浏览器的能力来处理这些字体。
3.3 实际应用中的字体处理流程:
3.3.1 手动解析和渲染(Type 1、Type 3):
- 读取字体文件:从PDF文件中提取字体数据。
- 解析字形描述:解析字体文件中的字形定义,这些定义通常是基于数学表达(如Bezier曲线)。
- 绘制字形:使用 Canvas 或 SVG 技术,将解析后的字形描述转换为绘图指令,并在页面上绘制。
3.3.2 利用浏览器渲染器(TrueType、OpenType 字体):
- 嵌入字体:如果字体嵌入在 PDF 中,PDF.js 将其嵌入到页面的样式中。
- 依赖浏览器渲染:利用浏览器内置的字体渲染引擎来显示文本。
PDF.js为了优化性能和效率,每个字形都会预先编译一个路径生成器函数,通过getPathGenerator()函数,将这些命令转换为JavaScript代码片段并生成一个新的路径生成器函数,最后,使用生成的路径生成器函数在Canvas上下文中开始绘制字形。
3.4 getPathGenerator()函数:
在 PDF.js 中,处理需要手动转换字形的字体格式时,getPathGenerator() 函数扮演了重要角色。这个函数用于生成路径数据,以便在画布上绘制字形。
1 | getPathGenerator(objs, character) { |
当缓存没有检查到该字符编译过了路径生成函数,则会开始获取需要的路径命令,若当前的PDF.js环境恰好支持eval或new Function的使用,则可以开始将路径命令转化为JavaScript的代码。
3.5 路径命令转化为JavaScript的代码:
1 | if (this.isEvalSupported && _util.FeatureTest.isEvalSupported) { |
这个过程会初始化一个空的数组jsBuf[ ]用于存储生成的JavaScript代码,遍历cmds数组,其中每个current对象表示一个命令,最后使用new Function构造一个新的函数,函数体是jsBuf[ ]中所有代码片段的连接字符串,到这里PDF完成了主要的解析流程。
0x4 漏洞原理:
1. 未进行过滤的fontMatrix属性:
我们知道PDF.js在处理PDF文件时,会从与字体相关的PDF字典对象中加载各种属性,其中在加载fontMatrix属性时候,若未找到这个属性则会使用默认值FONT_IDENTITY_MATRIX(fontMatrix的默认值是[0.001, 0, 0, 0.001, 0, 0]),这里值得注意的是fontMatrix属性在加载过程中没有进行过滤!
1 | async translateFont({ |
既然会从与字体相关的PDF字典对象中加载未进行过滤的fontMatrix属性,那么我们可以尝试对PDF做一些恶意操作,开始制作恶意的PDF文件,制作前先了解下PDF的一些格式规范。
1.1 PDF格式规范:
在PDF格式中,字体定义由几个对象组成:Font、FontDescriptor和实际的FontFile。
1 | 1 0 obj |
其中一些字段的含义:
- /Type/Font:指定了对象的类型是一个字体。
- /Subtype/Type1: 指定了字体的子类型。在这个例子中是Type1字体。
- /FontDescriptor 2 0 R:引用了另一个对象(对象号为2,生成号为0),它是这个字体的描述符对象,包含了更多的字体度量信息。
其中PDF字典对象中的dict对象对应的就是PDF元数据中的Font对象。因此,我们可以自定义一个FontMatrix数组。
1 | 1 0 obj |
1.2 插入恶意字符:
现在我们可以从一个PDF对象中控制这个数组,由于PDF支持的不仅仅是数字类型。让我们尝试插入一个字符串类型的值而不是数字。在PDF中,如果想在/FontMatrix数组中插入字符串,使其可以作为PDF字符串对象的一部分。这是通过圆括号 () 包含字符串内容的方式来实现的,假设你想在/FontMatrix数组中的最后一个元素位置插入字符串 “yuzi”。
1 | /FontMatrix [0.1 0 0 0.1 0 (yuzi)] |
1.2.1 字符串转义:
在PDF中,字符串对象用圆括号括起来,内容可以包括任意字符,注意特殊字符需要转义。
- ( 表示纯字符左括号
- ) 表示纯字符右括号
- \ 表示纯字符反斜杠
1.2.2 数组语法:
PDF中的数组用方括号 [ ] 括起来,元素之间用空格分隔。
这里我们插入恶意的fontMatrix到PDF中。(这里为了达到执行效果,我们应选择需要自己转换为曲线的字体格式如Type1
)
1 | 1 0 obj |
2. 恶意字符被拼接:
对compileGlyph()函数传入的这个fontMatrix数组使用slice()进行分割,并初始化到cmds数组中,我们发现如果这个数组中有任何字符串,它也将被原样插入,周围没有过滤!
1 | compileGlyph(code, glyphId) { |
当传入的fontMatrix为PDF中我们设置的恶意字符串时候。(经过PDF自身转义后的字符串如下)
1 | /FontMatrix [0.1 0 0 0.1 0 (1); |
此时{ cmd: “transform”, args: fontMatrix.slice() }会对应:
1 | c.transform(0.1,0,0,0.1,0,1); |
显然在初始化过程中,我们精心设计的字符串已经将正常的语句进行了分割,巧妙地恶意代码也成功被插入到程序,现在只需要后续的流程中能够正常运行这段代码即可。
3. 恶意代码被执行:
cmds数组初始化完成后,为了达到恶意代码被执行的效果,我们需要PDF.js自己将字形(即字符)转换为曲线,在前面恶意PDF的构造过程我们使用符合预期的Type 1字体格式。
1 | if (this.isEvalSupported && _util.FeatureTest.isEvalSupported) { |
这个过程遍历cmds数组,使用new Function构造一个新的函数,函数体是jsBuf[ ]中所有代码片段的连接字符串,也就是这里当开始渲染恶意PDF文件时,恶意代码也就开始被偷偷执行了,成功触发我们插入PDF中的恶意代码!
1 | alert('document.domain: '+window.document.domain+'\nlocation: '+window.location+'\ncookie: '+window.document.cookie); |
0x5 修复思考:
通过上面对漏洞触发流程的推敲,可以发现最开始在调用PartialEvaluator.translateFont()方法时候,由于未进行过滤的fontMatrix属性,就会开始将含有恶意代码的内容传入,如果能在这里做出适当过滤可以很大程度上阻止恶意攻击的可能。
1 | static isValidMatrix(matrix) { |
在isValidMatrix()方法中,检查matrix是否为数组,长度是否为6,并确保每个元素都是数字,这有效防止了恶意或无效的数据被注入!
当然,于2024年4月29日 PDF.js官方更新至4.2.67版本后,也成功修复了这个安全漏洞。接下来让我们来看看官方的修复逻辑,将更新包进行下载分析https://github.com/mozilla/pdf.js/releases/tag/v4.2.67定位到关键PartialEvaluator.translateFont()方法时候发现官方也在获取fontMatrix属性的周围进行了有效过滤!
0x6 排查方法:
漏洞影响范围:
Mozilla PDF.js < 4.2.67
pdfjs-dist(npm) < 4.2.67
react-pdf(npm) < 7.7.3
8.0.0 <= react-pdf(npm) < 8.0.2
注:pdfjs-dist是Mozilla PDF.js 库的通用构建;React-PDF是一个React组件,它封装了PDF.js库。
根据漏洞影响范围,通过对所有直接或间接使用了PDF.js组件的客户端和Web端进行排查(如在线网盘PDF解析处、在线文档平台等),查看是否处于危险版本内,因为一些更高级的 PDF 相关库静态地嵌入了PDF.js,建议递归检查node_modules文件夹,以确保没有名为 pdf.js 的文件。
0x7 缓解措施:
1. 升级版本:
受影响用户可更新到PDF.js/pdfjs-dist版本4.2.67、react-pdf7.7.3或8.0.2,大多数封装库如react-pdf也发布了修补版本。
下载链接:
https://github.com/mozilla/pdf.js/tags
https://github.com/wojtekmaj/react-pdf/tags
2. 临时措施:
可通过将isEvalSupported设置为false来缓解这两个漏洞。对于 PDF.js,该设置是全局配置的;而在React-PDF中,需在Document组件的options属性中指定options.isEvalSupported为false。
声明:本文仅限于技术讨论与分享,严禁用于非法途径。若读者因此作出任何危害网络安全行为后果自负,与本号及原作者无关。