文件类型混淆攻击:将JS伪装成PDF的原理、构造与实战利用 1. 项目概述为什么要把JS伪装成PDF在漏洞挖掘和安全测试的实战中我们经常会遇到一些有趣且实用的“旁门左道”。今天要聊的这个话题就属于那种听起来有点“歪”但用对了地方却能四两拨千斤的技巧将JavaScript代码伪装成PDF文件。你可能会问这有什么用一个正常的网站怎么会把JS当成PDF来处理这恰恰是问题的关键。这种手法的核心价值在于绕过前端或后端的文件类型检测逻辑从而在目标系统上执行我们预设的JavaScript代码。想象一个场景一个网站允许用户上传PDF文档用于预览或存档。它的上传接口通常会做一层校验比如检查文件扩展名是否为.pdf或者用更“高级”一点的方法读取文件的Magic Number文件头标识来判断。如果校验逻辑存在缺陷比如只检查了扩展名或者对文件内容的解析存在漏洞那么我们就可以构造一个“披着PDF外衣”的JS文件诱使服务器或客户端浏览器以非预期的方式处理它。一旦成功就可能触发跨站脚本攻击、信息泄露甚至配合其他漏洞实现更深入的利用。这不仅仅是理论在真实的SRC漏洞挖掘和渗透测试中这类因文件类型混淆导致的漏洞时有出现。所以这个技巧的目标读者很明确对Web安全、前端安全、漏洞挖掘感兴趣的安全研究人员、渗透测试工程师以及希望提升自己应用安全防御能力的开发人员。无论你是刚入门SRC的新手还是想拓宽攻击面的老手理解并掌握这些方法都能让你在挖掘逻辑漏洞和客户端漏洞时多一个趁手的工具。接下来我会结合原理、方法和实战中的坑带你彻底搞懂这件事。2. 核心原理与攻击面分析2.1 文件类型检测的常见方式与绕过点要成功伪装必须先了解“守卫”是如何工作的。服务器和浏览器判断文件类型主要依赖以下几种机制而每一种都可能成为我们的突破口扩展名检查这是最基础、也最容易被绕过的方式。后端代码可能简单地检查文件名是否以.pdf结尾。绕过方法极其简单直接给我们的JS文件命名为payload.pdf即可。但如今稍微有点安全意识的应用都不会只依赖这种方式。Content-Type检查HTTP响应头中的Content-Type字段例如application/pdf。服务器在返回文件时设置这个头浏览器据此决定如何处理文件。如果服务器错误地将一个JS文件的Content-Type设置为application/pdf浏览器可能会尝试将其作为PDF渲染但具体行为因浏览器和插件而异。我们的攻击点在于能否在上传或服务端处理流程中篡改或影响这个头的生成。Magic Number文件头检查这是相对可靠的方法。PDF文件的文件头通常是%PDF-对应十六进制25 50 44 46 2D。应用程序会读取文件的前几个字节进行比对。我们的核心挑战就在这里如何让一个JS文件同时拥有合法的PDF文件头。文件内容解析检查更严格的系统会尝试用PDF解析库如Python的PyPDF2、PHP的FPDI去解析文件如果解析失败则拒绝。这对伪装提出了更高的要求我们需要构造的文件必须能同时通过PDF解析器和JavaScript引擎的解析。浏览器MIME Sniffing即使服务器发送了Content-Type: application/pdf浏览器仍可能执行“MIME类型嗅探”通过分析文件内容来“猜测”其真实类型。如果它嗅探出内容是JavaScript可能会忽略声明的PDF类型转而将其当作JS执行。这有时对我们有利有时则会导致攻击失败需要具体情况具体分析。攻击面就存在于这些检查环节的疏漏或组合逻辑错误中。例如系统可能按顺序检查扩展名 - 文件头 - 内容解析。如果我们在文件头检查后就成功了后续的内容解析因为某种原因如解析库容错性强、抛出未被捕获的异常被跳过那么我们的伪装文件就可能被接受。2.2 为什么PDF是理想的伪装目标在众多文件格式中为什么选择PDF作为JS的伪装外壳这源于PDF格式的几个独特性质结构复杂且容错性不一PDF标准庞大解析器实现各异。一些解析器可能专注于渲染对文件内部结构的错误并不敏感只要找到可渲染的对象就行。这为我们插入额外数据如JS代码提供了空间。支持嵌入式对象与脚本PDF本身可以包含JavaScript用于表单交互、文档操作等。这意味着一个“合法”的PDF文件里本来就可以有JS代码。这模糊了恶意与合法的边界给防御带来困难。Magic Number简单PDF的文件头%PDF-非常简单且位于文件最开头易于构造和拼接。普遍信任度较高PDF常被用于传递正式文档用户和系统对其警惕性可能低于可执行文件或脚本文件更容易被触发或传播。理解这些原理后我们就可以着手尝试具体的伪装方法了。不同的方法对应不同的防御强度成功率也各异。3. 基础伪装方法文件头拼接与注释利用这是最简单直接的两种方法适用于防御非常薄弱仅检查扩展名或简单文件头的场景。3.1 直接拼接PDF文件头思路很简单在一个正常的JavaScript文件的开头直接加上PDF的文件头标识%PDF-。因为%在JavaScript中是单行注释符所以这行“文件头”对于JS解释器来说只是一行注释会被安全地忽略。而对于检查文件头的程序来说它看到了一个合法的PDF开头。操作步骤创建一个新的文本文件。在第一行写入%PDF-从第二行开始写入你的JavaScript有效载荷。例如%PDF- alert(XSS via Fake PDF); // 后续可以跟更复杂的JS代码...将文件保存为exploit.pdf.js或直接exploit.pdf。原理与注意事项注意这种方法非常初级只能绕过最简单的文件头检查。任何尝试解析PDF正文内容的检查都会失败因为alert语句显然不是有效的PDF语法。此外现代浏览器在加载此类文件时如果Content-Type是application/pdf它们会调用PDF阅读器插件或内置渲染器。渲染器在解析到非PDF内容时会报错或显示空白而不会执行JS。因此这种方法的主要利用场景可能是后端服务保存文件后有另一个功能点如“查看原始文件”以text/plain或application/octet-stream类型返回文件内容此时浏览器可能直接显示源码其中的JS代码在特定上下文中如反射进HTML被执行。3.2 利用PDF注释语法包裹JSPDF格式支持注释注释以%开头直到行尾。我们可以利用这一点将整个JS代码块“注释”掉吗不行因为PDF注释是单行的。但有一个技巧我们可以构造一个PDF文件其中包含一个stream对象在stream内部我们可以放置任意数据只要不影响stream的边界定义。更简单一点我们可以尝试将JS代码放在PDF的“对象”之间并用PDF注释符包裹每一行JS代码。但更有效的方法是反过来将PDF的语法元素写成JavaScript的注释。例如一个最小的PDF文件可能包含类似这样的结构%PDF-1.4 1 0 obj /Type /Catalog /Pages 2 0 R endobj ...我们可以尝试在obj和endobj等关键字之间插入JS代码并用/* */包裹PDF的关键字使其在JS看来是多行注释。构造示例%PDF-1.4 /* 1 0 obj /Type /Catalog /Pages 2 0 R endobj */ alert(This is JavaScript!); /* trailer /Root 1 0 R */实操心得这种方法比单纯加文件头稍好因为它让文件内容看起来更像一个“破损”的PDF而非完全无关的数据。它可能绕过一些简单的语法检查。但它的成功率依然很低因为文件缺乏有效的PDF对象结构任何认真的PDF解析器都会快速失败。它更像一种“混淆”而非“伪装”主要用于应对那些只做正则匹配扫描比如寻找endobj等关键字的简陋检测脚本。4. 高级伪装技术构造Polyglot文件Polyglot文件也叫“多语种”文件是指一个文件同时符合两种或多种文件格式的规范被不同的解释器解析时能产生不同的、符合预期的行为。我们的目标就是构造一个PDF-JS Polyglot文件PDF解析器认为它是一个可能有些奇怪的有效PDF文件而JavaScript引擎则能执行其中嵌入的代码。这是最具挑战性也最有可能绕过严格检测的方法。主要有两种实现路径。4.1 在PDF对象中嵌入JavaScript这是最“正统”的方法。如前所述PDF标准支持通过/JS或/JavaScript命名动作来包含JavaScript。我们可以创建一个结构完整、语法正确的PDF文件然后将我们的攻击载荷作为PDF内的合法JavaScript对象嵌入。使用工具手动构造我们可以用十六进制编辑器或编程方式来构造。一个极简的、包含JS的PDF结构如下%PDF-1.4 1 0 obj /Type /Catalog /Pages 2 0 R /OpenAction 3 0 R % 定义文档打开时执行的动作 endobj 2 0 obj /Type /Pages /Kids [] /Count 0 endobj 3 0 obj /Type /Action /S /JavaScript /JS (alert\(Polyglot PDF-JS executed!\);) endobj xref 0 4 0000000000 65535 f 0000000010 00000 n 0000000050 00000 n 0000000120 00000 n trailer /Size 4 /Root 1 0 R startxref 200 %%EOF关键点解析/OpenAction指向一个动作对象3号对象。动作对象的类型/S是/JavaScript并在/JS字段中包含了我们的代码。PDF中的字符串需要转义括号所以(和)前加了反斜杠\。文件尾部必须有正确的xref交叉引用表和startxref指针这是PDF解析器定位对象的关键。自动化工具利用手动构造复杂且易错。我们可以利用现有的PDF生成库来创建这样的文件。例如使用Python的PyPDF2库或更现代的pikepdfimport pikepdf from pikepdf import Pdf, Name # 创建一个新的PDF pdf Pdf.new() # 创建一个JavaScript动作 js_code app.alert(Hello from PDF JS); js_action pdf.make_indirect(pikepdf.Dictionary({ Name.Type: Name.Action, Name.S: Name.JavaScript, Name.JS: js_code })) # 将动作设置为文档打开动作 pdf.Root.OpenAction js_action # 保存文件 pdf.save(malicious.pdf)这样生成的malicious.pdf是一个完全合规的PDF文件任何PDF阅读器打开它时都会弹出一个警告框如果阅读器支持并启用了JS。这种方法能绕过所有基于文件结构、Magic Number和内容解析的检查因为它就是一个真PDF。其利用场景在于当用户使用支持并启用了JavaScript的PDF阅读器如旧版Adobe Reader、某些浏览器内置PDF查看器打开此文件时代码会被执行。4.2 利用增量更新创建PolyglotPDF有一个特性叫“增量更新”允许在不重写整个文件的情况下在文件末尾追加新的修改。这可以让我们在一个正常PDF的末尾追加任意数据而PDF解析器会忽略追加部分只读取到第一个%%EOF但其他处理器如文本提取工具、不规范的解析器可能会读到后面的数据。构造思路准备一个正常的、无害的PDF文件例如一个空白页PDF我们称之为base.pdf。在base.pdf的二进制内容末尾、%%EOF标记之后追加我们的JavaScript代码。确保追加操作没有破坏原始PDF的结构即%%EOF之前的部分完好无损。操作与影响对于PDF解析器它读取文件找到%%EOF后即认为文件结束忽略之后的所有内容。因此它认为这是一个合法的base.pdf。对于只检查文件头的安全扫描器它看到开头的%PDF-就会通过。对于某些文件上传处理流程如果服务器端在处理时不是用PDF解析器验证而是简单地读取整个文件进行病毒扫描或内容过滤那么追加的JS代码就可能被识别出来。但如果处理流程中存在漏洞比如将文件的一部分内容错误地包含进网页输出那么追加的JS就可能被注入。这种方法制造的是一个“寄生”Polyglot其JS部分并非PDF的有机组成部分。它的成功利用依赖于目标处理链中存在特定的逻辑缺陷。5. 实战利用场景与案例拆解理解了方法我们来看看在真实的漏洞挖掘中这些技巧如何被应用。关键不在于文件本身而在于文件被处理的全链路。5.1 场景一上传功能中的类型混淆这是最经典的场景。假设一个网站有用户头像上传功能但后端错误地允许上传.pdf格式。防御逻辑如下检查扩展名为.jpg,.png,.pdf之一。对于.pdf调用一个库比如pdfinfo命令来提取第一页文本用于建立搜索索引。将文件存储在/uploads/目录下。攻击链设计我们上传一个构造的Polyglot PDF-JS文件exploit.pdf。扩展名检查通过。后端调用pdfinfo。一个构造良好的Polyglot文件可能让pdfinfo成功运行并返回一些文本甚至是我们预设的文本从而通过检查。文件被保存为/uploads/12345.pdf。网站另一个功能是“文档预览”它通过iframe src/uploads/12345.pdf来嵌入PDF。关键点如果这个预览端点设置Content-Type的代码有误或者服务器被错误配置导致该PDF文件以text/html类型返回那么浏览器就会将文件内容作为HTML解析其中的JS代码如果被包含在注释或特定位置就可能被执行。实操心得在这个场景中单纯的文件伪装只是第一步。更重要的是探测服务器在处理、存储、返回文件时的行为。你需要测试文件是否被重命名返回的HTTP头是什么文件内容是否被修改如压缩、转码是否被不同的服务如CDN、预览服务二次处理这些环节中的任何一个点都可能成为突破口。5.2 场景二客户端PDF渲染器漏洞链这个场景更侧重于客户端。我们构造一个包含复杂JS的PDF利用PDF阅读器如浏览器内置查看器、Adobe Acrobat Reader的JavaScript接口或已知漏洞。利用合法JS接口PDF JS API允许脚本进行文件系统访问受限、网络请求、调用外部应用等。例如通过app.launchURL()可以发起请求可能用于SSRF或与本地服务交互。配合DOM-Based XSS如果网站有一个功能是将PDF文件内容提取并动态插入到页面DOM中例如显示PDF元数据并且提取过程存在缺陷未能正确过滤我们嵌入PDF中的JS字符串就可能触发DOM型XSS。案例拆解假设一个在线文档管理系统在用户上传PDF后会尝试用JavaScript库如pdf.js在前端渲染预览并同时提取文档属性作者、标题显示在侧边栏。我们上传一个PDF在文档信息字典中将/Title字段设置为img srcx onerroralert(1)。前端pdf.js库解析PDF提取出/Title。前端代码直接将提取出的标题使用innerHTML或类似的不安全方式插入到侧边栏的DOM中。浏览器解析该HTML字符串执行onerror事件中的JavaScript。这里PDF文件本身是合法的我们的“载荷”是存储在PDF元数据中的一个字符串。漏洞的根源在于前端对来自PDF一个看似可信的来源的数据未做输出编码。5.3 场景三绕过WAF与静态扫描一些Web应用防火墙或静态代码分析工具会对上传的文件内容进行简单的关键字扫描如扫描script、javascript:等。将JS代码嵌入到PDF的二进制流或经过编码的字符串中可以绕过这些基于文本匹配的简单扫描。例如在PDF的/JS动作中字符串可以使用十六进制或八进制编码/JS 6170702E616C657274282748656C6C6F27293B % 十六进制编码的 app.alert(Hello);对于扫描器来说这只是一串十六进制数字但对于PDF阅读器的JS引擎它会正确解码并执行。6. 防御视角与安全建议作为攻击者我们研究这些方法是为了更好地防御。从开发和安全建设角度应该如何防范此类攻击实施多层次、严格的文件类型验证扩展名白名单只允许业务必需的类型。MIME类型检查同时检查客户端上传的Content-Type不可信和服务器端检测出的真实类型。使用像python-magiclibmagic绑定这样的库进行准确的二进制内容检测。文件头/魔术字节验证验证文件开头几个字节是否符合预期格式。深度内容解析对于关键文件类型如PDF使用成熟、稳定的解析库如PyPDF2、PDFium尝试解析文件。如果解析过程抛出无法处理的致命错误则拒绝文件。注意这里要区分“解析警告”和“解析失败”。隔离与沙箱化将所有用户上传的文件存储在独立的、无法直接执行脚本的存储服务或目录中。通过单独的、无状态的文件预览/转换服务来处理用户文件。这个服务运行在隔离的容器或虚拟机中其唯一职责就是将上传的文件转换为安全的预览格式如转换成图片或纯文本。即使文件是恶意的其影响也被限制在沙箱内。安全的文件服务服务用户上传的文件时务必设置正确的、强制的Content-Type头如application/pdf并加上Content-Disposition: attachment或sandbox属性避免浏览器进行危险的MIME嗅探或直接内嵌执行。设置严格的Content-Security-Policy头限制脚本加载的来源这可以很大程度上缓解即使恶意文件被加载也无法执行的问题。客户端渲染安全如果使用前端库如pdf.js渲染PDF确保从PDF中提取的任何元数据或文本内容在放入DOM前都经过严格的HTML实体编码或使用安全的API如textContent。在浏览器环境或PDF阅读器中禁用不必要的JavaScript功能。持续更新与监控保持所有使用的文件解析库图像处理、PDF解析、文档转换等更新到最新版本以修复已知的解析漏洞。对上传功能进行安全监控和日志审计对异常文件如结构畸形、大小异常、频繁上传进行告警。7. 工具与资源推荐工欲善其事必先利其器。以下是一些在研究和测试PDF-JS Polyglot时可能有用的工具构造与分析工具hexedit / 010 Editor十六进制编辑器用于手动分析和修改文件底层字节理解文件结构必备。Python pikepdf/PyPDF2用于编程生成、修改和解析PDF文件实现自动化构造。pdftk命令行PDF工具包可以合并、拆分、旋转PDF有时可用于快速制作Polyglot原型。检测与验证工具file 命令Linux/Unix系统自带的文件类型检测工具基于magic number。python-magicPython接口的libmagic库用于准确检测文件真实类型。PDF解析器用PyPDF2或pdfminer写一个简单的脚本尝试解析上传的文件捕获解析异常。浏览器开发者工具上传文件后在Network面板查看服务器返回的准确Content-Type在Console面板查看是否有JS错误或执行痕迹。学习资源PDF官方标准 (ISO 32000)虽然冗长但它是理解PDF结构的终极参考。PortSwigger的Web安全学院包含关于文件上传漏洞的详细教程和实验室其中涉及MIME和内容类型混淆。OWASP Cheat Sheet Series - File Upload提供了全面的文件上传安全指南和防御策略。8. 常见问题与排查技巧实录在实际操作中你会遇到各种各样的问题。下面记录了一些典型问题及其解决思路。Q1我构造了一个带JS的PDF用Adobe Reader打开没弹窗为什么A首先检查Adobe Reader的JavaScript设置是否被禁用默认可能是启用的。其次现代版本的Adobe Reader和很多浏览器内置PDF查看器出于安全考虑大幅限制了嵌入式JavaScript的功能甚至默认禁用。你的代码可能被安全策略阻止了。测试时可以尝试使用旧版本的阅读器在隔离环境中或者寻找那些明确依赖PDF JS功能如复杂表单的网站作为目标它们更可能开启JS支持。Q2上传时服务器返回“文件类型不支持”但我确定扩展名和MIME都对怎么办A这说明服务器端做了更深度的检查。你需要分析它的检测逻辑。抓包分析对比上传一个正常PDF和你的伪装PDF的请求/响应差异。模糊测试尝试上传仅包含PDF文件头、文件尾但中间是JS的文件尝试在PDF文件的不同位置开头、中间、结尾插入JS尝试使用不同的JS编码方式。错误信息利用如果服务器返回了具体的错误信息如“PDF解析失败在第X行”这就是黄金信息可以指导你调整文件结构。Q3文件上传成功了但似乎没有任何效果如何进一步利用A上传成功只是第一步。你需要找到“触发点”。寻找文件访问点查看HTML源码寻找类似embed,object,iframe,a href指向你上传文件的链接。检查HTTP响应头直接访问上传后的文件URL查看服务器返回的Content-Type是什么是application/pdftext/plain 还是application/octet-streamContent-Disposition是什么测试内容嵌入如果网站有功能会将文件内容部分显示在页面上如文档摘要、预览图、属性展示尝试在你的文件中插入一些可识别的标记如TEST_PAYLOAD看是否会出现在页面中。如果会就可能存在注入点。Q4如何判断一个站点是否存在这类漏洞A可以遵循以下排查路径信息收集找到所有上传点图片、头像、文档、附件上传。基础探测尝试上传一个最简单的Polyglot文件如仅添加%PDF-文件头的JS文件观察反应。逻辑分析分析上传流程。文件存储路径是否有规律是否有预览功能预览是如何实现的直接链接、通过iframe、还是后端转换深入测试针对有预览或处理功能的上传点系统性地测试不同伪装方法并仔细检查每个环节的响应。踩坑记录不要忽视文件大小和结构一些系统会检查PDF的基本结构比如必须有xref表和trailer。一个只有文件头和一串JS的“PDF”很容易被高级检查过滤掉。尽量使用工具生成结构基本完整的PDF再在其中做手脚。注意编码和转义在PDF字符串中括号()、反斜杠\等字符需要转义。在JS字符串中单引号、双引号也需要处理。这可能导致精心构造的载荷在解析时被破坏。务必在保存后用二进制模式读取文件验证关键字节是否正确。环境差异性巨大在Chrome内置PDF查看器上成功不代表在Firefox、Edge或桌面阅读器上也能成功。测试时要明确你的目标环境。