文章总结: 这篇文章描述了一次渗透测试中如何从HTML转PDF的API接口发现并利用漏洞实现远程代码执行的过程。作者通过PDF元数据识别出使用的是基于Chromium62的EO.Pdf渲染器,发现其以无沙箱模式运行,并成功利用了Chrome62.0.3202.62中的一个WebAssembly使用后释放漏洞,最终执行shellcode。文章强调了及时更新软件组件和禁用不必要功能(如JavaScript引擎)的重要性,建议开发者使用EO.Pdf的安全选项来防止此类攻击。
综合评分: 91
文章分类: 渗透测试,漏洞分析,红队,WEB安全,二进制安全
记一次渗透测试从PDF渲染到shellcode执行
原创
白帽子左一
白帽子左一
2025年7月15日 12:00
美国
扫码领资料
获网安教程
来Track安全社区投稿~
赢千元稿费!还有保底奖励~(https://bbs.zkaq.cn)
前言
在最近的一次渗透测试中,我们发现了一个 HTML 转 PDF 的转换器 API 接口,它允许我们列出远程服务器上的本地目录和文件。我们创建的其中一个 PDF 文件揭示,该转换器使用的是基于 Chromium 62 的 .NET 渲染框架。借此,我们通过将一个适用于 Chromium 62 的漏洞移植到该渲染器版本上,实现了远程代码执行(RCE)。
服务端 XSS
在这次渗透测试中,我们再次深刻体会到,深入的手动渗透测试依然是不可替代的。
这次测试涉及识别并手动挖掘出一系列经典漏洞,而这些漏洞是任何自动化扫描器都无法检测到的。
我们的方法简要概括如下:我们测试的服务允许用户生成包含动态内容的 PDF 发票。用于生成发票的接口 reports/export/pdf 接收多个 POST 字段:一个 JWT 令牌、一个报告标题以及一大串经过 URL 编码的 HTML 字符串。向 Web 服务器传递编码的 HTML?这立刻触发了我们的 XSS 直觉!此前我们接触过 HTML 到 PDF 的渲染器,因此我立刻想到了“服务端 XSS”。在这种特定的设置下,我们可以将恶意的 HTML 标签甚至 JavaScript 发送给 HTML 渲染器。然后,这些恶意代码将在远程服务器上被解析和执行。此类攻击可能带来的影响包括:
- • 通过 iframe、图片甚至 JavaScript 实现服务器端请求伪造(SSRF)
- • 通过 iframe 实现本地文件读取
- • 泄露 cookies 和应用存储中的内部数据
- • 如果浏览器启用了 JavaScript,还可执行 JavaScript 代码
下面来看看我们都做了些什么。
PDF 元数据
由于我们正在测试的是一个生产环境,为避免触发警报,我们没有盲目地向接口投放 payload。因此我们需要一个本地的测试环境。幸运的是,生成的 PDF 给了我们足够的线索:
PDF 元数据泄露了 EO.Pdf 渲染器及其精确的库版本号
元数据显示,PDF 文件是由 EO.Pdf 18.3.46.0 模块生成的。通过简单的 Google 搜索,我们发现了 EssentialObjects 提供的一个有趣的组件:EO.Pdf。这个库能够将 HTML 转换为 PDF,正是我们所寻找的功能。
在我们的渗透测试中,我们总是会标记出 PDF、图片和文档中那些不必要的元数据:这些信息对于潜在攻击者来说完全可以避免泄露。在这次测试中,这些元数据帮助我们准确识别了所使用的版本,从而为后续的攻击开发提供了便利!(因此,我们关于信息泄露的警告是非常有意义的。)
EO.WebView 框架中的服务端 XSS
我们很快搭建了一个本地测试环境。幸运的是,我们可以直接从 NuGet,这个广泛使用的 .NET 包管理器中下载 EO.Pdf 库。他们甚至提供了我们需要的那个精确版本!
NuGet 包管理器可用于获取精确版本的 EO.Pdf 库
官方文档:https://www.essentialobjects.com/doc/pdf/htmltopdf/overview.html提供了一个简短的示例代码,用于将 HTML 文件转换为 PDF:
EO.Pdf.HtmlToPdf.ConvertUrl("c:\\test.html", "c:\\result.pdf");
忽略 EO 弹出的试用许可提醒,我们成功生成了 result.pdf 文件。现在来测试一些 payload 吧!我们首先尝试的是 SSRF,因为即使渲染器不支持 JavaScript,这种方式也有可能奏效:
<html>
<body>
<img src="https://webhook.site/74db201f-292d-4197-b338-a468515182ef">
</body>
</html>
我们的 webhook 收到了请求!如果远程服务器使用的正是这个配置,那我们就已经发现了一个 SSRF 漏洞。
HTML 中嵌入的图片调用了 Webhook
看起来渲染引擎使用的是 Chromium 版本 62。天啊,这版本可真老!这大致对应 18.3.46 NuGet 包的发布时间,也就是 2018 年初。而 Chromium 62 则是在 2017 年 10 月发布的,很自然地,这个库确实使用了当时已经发布几个月的 Chrome 版本。
于是,我们决定继续深入尝试,使用 JavaScript 和 chrome://version 来获取更多细节。我们通过动态添加一个 <iframe> 元素到网页中来测试 JavaScript 是否可以执行,这样可以让我们在 PDF 中嵌入“子页面”:
<html>
<body>
<script>
var iframe = document.createElement('iframe');
iframe.height = 1000
iframe.width = 1000
iframe.src="chrome://version";
document.body.appendChild(iframe)
</script>
</body>
</html>
动态 JavaScript 执行及精确的 V8 版本号
成功了!我们可以使用 iframe 来嵌入页面,甚至能够执行 JavaScript 代码!太棒了。这看起来就像一个完整功能的 Chromium 浏览器。上面的截图标注了精确的 V8 版本 6.2.414.2(V8 是 Chromium 使用的 JavaScript 引擎),这个信息在后面将变得非常重要。
最后但同样重要的一步,我们尝试读取目录和文件:
<html>
<body>
<script>
var iframe = document.createElement('iframe');
iframe.height = 300
iframe.width = 1000
iframe.src="file://C:/";
document.body.appendChild(iframe);
var iframe2 = document.createElement('iframe');
iframe2.height = 300
iframe2.width = 1000
iframe2.src="file://C:/windows/win.ini";
document.body.appendChild(iframe2);
</script>
</body>
</html>
本地目录列举与读取 C:/Windows/win.ini 文件
在 Chromium 环境下,我们可以访问本地文件系统。太棒了,这个漏洞的严重性进一步提升了,因为我们现在可以浏览文件系统并访问文件了!
能够任意读取文件固然很好,但我们能否将这一发现的严重性提升到“高危”级别呢?毕竟这个浏览器已经有五年的历史了,某处肯定存在可用的 RCE(远程代码执行)漏洞。
利用 Chromium 62 引擎
首先说明一下:我对利用 JavaScript 引擎的经验非常有限,虽然在理论上有所了解,但之前从未写过完整的 V8 利用代码。下面所解释的一些概念可能不够严谨,甚至可能存在错误。如需系统学习 V8 利用技术,建议参考一些高质量的 V8 利用 博客、演讲、文章。
我推测浏览器引擎嵌套在那个高达 51 MB (!) 的 DLL 文件 EO.WebEngine.dll 中。这个 .NET 二进制文件被严重混淆,根本看不出 Chromium 引擎是如何嵌入其中的。即使用 x64dbg 对这个托管的 .NET DLL 进行调试,也没能找到浏览器嵌入方式的任何线索。是时候开始盲目利用了!
我希望找到的是一个不依赖 JIT(即时编译)优化的漏洞,因为我不确定 EO.Pdf Chromium 版本是否启用了 TurboFan JIT。在 Google 和 Chromium issue tracker 上搜索近几年的 Chromium CVE 漏洞时,发现 CVE-2017-15428 很有潜力。我们将该漏洞报告中提供的 POC(概念验证代码)运行后,成功造成了崩溃!
在使用 CVE-2017-15428 将 HTML 转换为 PDF 时,EO.Pdf 崩溃
虽然这验证了漏洞的存在,但我完全不知道该如何利用它。在 64 位可执行文件中,reg.lastIndex 显然控制了某个寄存器。而在我的测试中,我根本搞不清楚发生了什么。所以我决定搭建一个 V8 开发环境,进一步测试和分析!
搭建简单的 V8 调试环境
网上有许多教程教你如何使用 Google 的 depot_tools(仓库管理工具)和构建生成器来搭建 V8 环境。基本步骤如下:
- 1. 获取
depot_tools并添加到你的PATH中(https://chromium.googlesource.com/chromium/tools/depot_tools.git) - 2. 将 V8 克隆到一个文件夹:
git clone https://chromium.googlesource.com/v8/v8 - 3. 获取所有标签:
git fetch origin --tags - 4. 检出我们嵌入的 Chromium 所对应的 V8 版本:
git reset --hard 62.0.3202.9 - 5. 执行
gclient sync,执行一些“魔法操作” - 6. 执行
gclient runhooks,继续执行更多“魔法操作” - 7. 获取构建 V8 所需的所有依赖项:
./build/install-build-deps.sh - 8. 创建 32 位调试版本:
gm ia32.debug d8
第 7 步彻底失败:2017 年的 V8 版本使用 OpenSSL 1.1,而我使用的新版 Ubuntu 22.04 不再支持它。所以我换用了 Ubuntu 16.04 的 Docker 容器,祈祷所有依赖源还在线,最终成功构建出了精确的 V8 版本 62.0.3202.9。
小插曲
当我们在 32 位调试版本中运行这个正则表达式漏洞(RegEx CVE)时,确实触发了与 Chromium 问题追踪器中描述的相同 断言失败(assert failure):
root@32f21bbac6ef:/src/v8/out/ia32.debug# gdb --args ./d8 /tmp/CVE-2017-15428.js
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
[...]
Reading symbols from ./d8...done.
gdb-peda$ r
Starting program: /src/v8/out/ia32.debug/d8 /tmp/CVE-2017-15428.js
[...]
[New Thread 0xf0f26b40 (LWP 93133)]
abort: CSA_ASSERT failed: IsFastRegExp(context, regexp) [../../src/builtins/builtins-regexp-gen.cc:2966]
==== JS stack trace =========================================
Security context: 0x43096655 <JSObject>#0#
2: replace(this=0x23c8949d <Object map = 0x40d89a09>#1#,0x23c894b9 <JSRegExp <String[3]: abc>>#2#,0x23c8a179 <JSFunction (sfi = 0x43099d95)>#3#)
3: tt [/tmp/CVE-2017-15428.js:22] [bytecode=0x43099e2d offset=79](this=0x23c849a5 <JSGlobal Object>#4#)
4: /* anonymous */ [/tmp/CVE-2017-15428.js:26] [bytecode=0x430999c9 offset=43](this=0x23c849a5 <JSGlobal Object>#4#)
然而,在 32 位发布版本中执行该概念验证代码时,仅导致了空指针解引用错误 🙁
在 32 位发布版本中因 CVE-2017-15428 导致空指针解引用
那时我意识到自己根本不知道自己在做什么。从一个导致崩溃的 PoC(类型混淆/使用后释放漏洞)到真正实现远程代码执行,这中间还有很长的路要走。虽然学习并非不可能,但或许用内嵌了 Chromium 的 EO.Pdf 并不是最好的起点。只能回到原点了!
将 Chrome 62 漏洞利用移植到 EO.Pdf
我尝试了更多的 CVE 和 PoC,大多数只是触发了越界读写漏洞,但由于堆布局不同,经常无法奏效。最终,我找到了由“奇虎360 Vulcan Team”的安全研究员提供的 Chrome 62.0.3202.62 32 位完整 PoC。该漏洞原计划用于 Pwn2Own Mobile 大赛,但最终向 Google 报告。漏洞本身存在于 JavaScript 引擎的 WebAssembly 部分,通过在持有旧的、未扩展且已释放缓冲区的引用时增长缓冲区,导致了使用后释放(use-after-free)漏洞。该漏洞利用所针对的 Chrome 版本与我们使用的嵌入版 Chromium 非常接近,前景看好!
但首先,还需要做一些准备工作:用 HTML 转 PDF 的方式不是很适合深入了解发生了什么。我想捕获 JavaScript 异常、调试输出以及 JavaScript 调用的结果。因此,我将本地环境改成加载一个网站到专用的无界面 WebView 中,并直接将 JavaScript 代码传递给浏览器引擎:
ThreadRunner threadRunner = new ThreadRunner();
//Create a WebView through the ThreadRunner
WebView webView = threadRunner.CreateWebView();
threadRunner.Send(() =>
{
//Load a page, otherwise we have no JS context for execution
webView.LoadUrlAndWait("chrome://version");
// Execute JS code
var res = webView.EvalScript(File.ReadAllText("C:/pdftest/cve_test.js"));
Console.WriteLine(res);
});
此外,我们也不想让利用尝试过于显眼。因此,我将(自动启用的,显然!)崩溃错误报告设置为 false,并在一个名为 CrashDataAvailable 的特殊事件处理器中捕获了 Chromium 的崩溃数据:
static void Runtime_CrashDataAvailable(object sender, EO.Base.CrashDataEventArgs e)
{
Console.WriteLine(e.ToString());
System.Diagnostics.Process.GetCurrentProcess().Kill();
}
// [...]
EO.Base.Runtime.CrashDataAvailable += Runtime_CrashDataAvailable;
EO.Base.Runtime.EnableCrashReport = false;
现在我可以使用 alert 将调试信息显示在 .NET 消息框中。这也会暂停渲染过程,非常方便调试。例如,我添加了如下代码,用来显示(泄露的)数组缓冲区元素的地址:
big_array_element_addr = (u2d(big_array[0x1f7e0]))[0] + 8 - 0x20000 * 8,
alert("big_array_element_addr: " + big_array_element_addr.toString(16));
在 x64dbg 中打开进程列表以检查内存时,我们发现了一个意外:渲染器进程是以 --no-sandbox 参数运行的!Chromium 通常会对渲染器进程进行沙箱隔离,因此通常还需要另一个沙箱逃逸漏洞才能实现真正的远程代码执行。但这次看来,我们可以跳过这一步!
Chromium 渲染器进程禁用了沙箱!
如果我们检查泄露地址处的内存,可以看到预期中的所有 2.2 双精度浮点数值。这些 2.2 值是在漏洞利用代码中设置的,用来初始化我们用于相对读写操作的缓冲区。
数组元素地址泄露似乎有效
在 EO.Pdf 渲染器中测试漏洞时,不幸的是,类型混淆的 ArrayBuffer 并没有被接受为 DataView 的参数,尽管 fake_ab instanceof ArrayBuffer 返回了 true。我猜测某些内部的 ArrayBuffer 结构有问题,最终我使用了自己的 V8 调试环境来获取一些内部 V8 结构的内存布局。在撰写本文时,我已无法重现这些问题,所以这里就跳过这部分较为无趣的利用细节吧。JavaScript 真是奇妙。
计算器弹出(CALC)
最终,shellcode 成功执行,计算器弹出了!
并且当使用单次调用 EO.Pdf.HtmlToPdf.ConvertUrl 时,该漏洞利用依然有效,且稳定性提升到了约 80%!
结语
本文展示了我们报告中常写的那种令人头疼的发现——“过时版本且存在已知漏洞”——会带来多么严重的影响。我们再次看到用户输入永远不能也不应该被信任,即便它只是“无害的 HTML 渲染”。客户本可以禁用 PDF 转换过程中的 JavaScript 引擎,但开发者往往一旦默认且推荐的 EO.Pdf.HtmlToPdf.ConvertUrl 方法可用,就不再深究。其实,ConvertUrl 方法还有一个重载版本,接受 HtmlToPdfOptions 参数。HtmlToPdfOptions 可以禁止本地文件访问并完全禁用 JavaScript 引擎,从而防止上述所有漏洞。遗憾的是,这些选项默认并未启用。
对我个人来说,能够深入探索 Chromium/V8 利用是一件很有趣的事情。我之前担心 EO.Pdf 使用了一个被严格限制的定制 Chromium 版本,但事实证明这种担忧是多余的。根据 EO.Pdf 的文档,他们为 Chromium 添加了大量的 .NET 绑定。幸运的是,这些绑定并没有妨碍我们的漏洞利用。如果没有弹出 alert 弹窗,我仍然不知道该如何正确调试内嵌的 Chromium 引擎。对所有 V8 研究人员致以崇高敬意,这些漏洞利用和技术真的非常高深!正如开头所说,自动化安全和漏洞扫描几乎不可能发现此类问题
获取更多精彩内容,尽在Track安全社区~:https://bbs.zkaq.cn
声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权!
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:白帽子左一 白帽子左一《记一次渗透测试从PDF渲染到shellcode执行》