1. 从零理解XSS闯关的核心逻辑
很多人第一次接触XSS闯关平台时,习惯性地把它当成"填空题"——看到输入框就塞<script>alert(1)</script>,弹窗不出来就换下一个payload,完全靠运气试。这种打法在低难度关卡偶尔能蒙混过关,但到了中高难度就会彻底卡死,因为你根本不知道过滤规则在拦什么、浏览器在怎么解析、服务端在怎么处理。
XSS闯关的本质不是"背payload",而是理解一条完整的攻击链路:用户输入 → 服务端处理 → 前端渲染 → 浏览器解析执行。每一环都可能存在过滤、编码、转义,而你要做的就是找到那条"输入能穿透所有关卡、最终被浏览器当作代码执行"的路径。这跟渗透测试中找SQL注入的思路是一回事——先判断上下文,再决定用什么语法闭合,最后构造出可执行的语句。
这篇文章适合三类人看:刚入门Web安全、想系统刷一遍XSS闯关的新手;刷了一半卡住、想搞懂每关背后原理的中级选手;以及想把这些关卡当作教学素材、需要一套完整解题思路的安全讲师。我会按照关卡难度递进的顺序,把每一关的上下文类型、过滤机制、绕过思路、最终payload拆开讲清楚,同时补充大量实战中才会遇到的细节,比如浏览器解析优先级、编码绕过、DOM型XSS的特殊性等。
需要提前说明的是,XSS闯关平台的设计目的是教学,所有关卡都是刻意构造的漏洞环境。你在真实项目中遇到的情况会复杂得多——有WAF、有CSP、有HttpOnly、有各种前端框架的自动转义。但闯关训练的价值在于,它帮你建立起"看到输入点就能条件反射地判断上下文"的能力,这个能力在实战中是无价的。
2. 反射型XSS的上下文判断与闭合技巧
2.1 第一关:没有任何过滤的"送分题"
第一关通常是这样的:页面有一个搜索框,你输入什么,它就原样显示在页面上,没有任何过滤。很多人觉得这关太简单,直接alert(1)就过了,但我想说的是,这关真正的价值在于让你建立"上下文判断"的意识。
你输入test,页面显示你搜索的内容是:test。这时候你要问自己:我的输入出现在HTML的什么位置?是在标签之间(<div>这里</div>),还是在标签属性里(<input value="这里">),还是在JavaScript代码里(var a = '这里')?位置不同,闭合方式完全不同。
第一关的输入出现在标签之间,所以最直接的payload就是:
<script>alert(1)</script>但这里有个细节很多人忽略:现代浏览器对于通过innerHTML插入的<script>标签是不会执行的。如果这关是用innerHTML渲染的,你写<script>可能没反应。这时候要改用事件型payload:
<img src=x onerror=alert(1)><img>标签的src加载失败时会触发onerror事件,这个事件在innerHTML插入时依然会执行。这是反射型XSS中最常用的"万能payload"之一,建议背下来。
提示:判断
<script>是否被执行,可以看页面是否真的弹窗。如果没弹,先别急着换payload,打开F12看看你的输入被插入到了DOM的哪个位置,这比盲目试payload高效得多。
2.2 第二关:value属性里的闭合陷阱
第二关的输入通常出现在<input>标签的value属性里,类似这样:
<input value="你的输入">这时候你直接写<script>alert(1)</script>是没用的,因为它会被当作value的一部分,变成:
<input value="<script>alert(1)</script>">浏览器会把整个东西当作属性值,不会执行。正确的思路是先闭合属性,再注入标签。你需要构造:
"><script>alert(1)</script>这样渲染出来就是:
<input value=""><script>alert(1)</script>">属性被"闭合,后面的<script>就成了独立的标签。但同样的问题,如果这关用innerHTML渲染,<script>不执行,那就换成:
"><img src=x onerror=alert(1)>这里有个容易踩的坑:有些关卡会把"转义成",这时候你闭合不了属性。要试试单引号',或者看看能不能用其他方式闭合。还有一种情况是属性没有用引号包裹,比如<input value=你的输入>,这时候你甚至不需要闭合引号,直接空格加事件就行:
x onmouseover=alert(1)2.3 第三关:单引号与HTML实体编码的博弈
第三关往往会把双引号转义,但单引号没处理。输入出现在类似这样的位置:
<input value='你的输入'>这时候你用双引号闭合没用,得用单引号:
'><img src=x onerror=alert(1)>但有些关卡更狠,它会把<和>都转义成<和>,这时候你注入不了新标签。怎么办?答案是利用现有标签的事件属性。比如输入出现在<input>的value里,你可以这样:
' onmouseover='alert(1)渲染出来就是:
<input value='' onmouseover='alert(1)'>你不需要注入新标签,只需要给现有标签加一个事件属性。用户鼠标划过输入框时就触发。这种思路在实战中非常有用,因为很多WAF只拦<script>和<img>,对事件属性反而宽松。
2.4 第四关:双写绕过与大小写混淆
到了第四关,过滤开始升级。常见的过滤方式是把script替换成空字符串。比如你输入<script>,服务端把script删掉,变成<>,payload就废了。
绕过思路是双写:
<scrscriptipt>alert(1)</scrscriptipt>服务端把中间的script删掉后,剩下的拼起来正好是<script>alert(1)</script>。这是最经典的绕过手法之一。
另一种是大小写混淆:
<ScRiPt>alert(1)</ScRiPt>如果服务端只匹配小写的script,大小写混合就能绕过。但要注意,HTML标签本身是大小写不敏感的,浏览器能正常解析。
还有一种情况是过滤了alert,这时候可以用confirm、prompt,或者用eval拼接:
<img src=x onerror=eval('ale'+'rt(1)')>2.5 第五关:a标签href里的javascript协议
第五关的输入通常出现在<a href="你的输入">里。这时候你注入<script>没用,因为href属性里不能放标签。正确的做法是用javascript:协议:
javascript:alert(1)点击链接时就执行。但很多关卡会过滤javascript这个词,绕过方式有:
- 大小写混合:
JaVaScRiPt:alert(1) - 插入空白字符:
java\tscript:alert(1)(某些浏览器支持) - HTML实体编码:
javascript:alert(1)
如果javascript被完全过滤,还可以试试data:协议:
data:text/html,<script>alert(1)</script>不过data:协议在现代浏览器中限制较多,不一定能执行脚本。
3. 存储型XSS:从留言板到持久化攻击
3.1 存储型与反射型的本质区别
反射型XSS是"你输入什么,页面立刻显示什么",攻击是一次性的,需要诱导受害者点击特制链接。存储型XSS则是"你输入的内容被存到数据库,所有访问这个页面的用户都会中招"。这个区别决定了存储型XSS的危害等级要高得多——你不需要诱导,只要有人访问被污染的页面就会触发。
在闯关平台中,存储型XSS通常出现在留言板、评论区、用户昵称、个人签名这些地方。解题思路和反射型类似,但要多考虑一层:你的输入会被存进数据库,取出来的时候可能经过转义。所以你要测试的是"存入时过滤"还是"取出时过滤",还是两者都有。
3.2 留言板关卡的完整测试流程
假设有一个留言板,你输入昵称和内容,提交后页面显示所有留言。测试流程应该是这样的:
第一步,输入普通文本test123,提交后查看页面源码,确认你的输入出现在什么位置。是直接输出在HTML里,还是经过转义?
第二步,输入<b>test</b>,看页面是否加粗。如果加粗了,说明没有转义,可以注入标签。如果显示的是字面的<b>test</b>,说明被转义了。
第三步,输入<script>alert(1)</script>,看是否弹窗。如果不弹,换成<img src=x onerror=alert(1)>。
第四步,如果都不行,检查是不是只过滤了某些标签。试试<svg onload=alert(1)>、<body onload=alert(1)>、<details open ontoggle=alert(1)>这些冷门标签。
这里有个实战经验:存储型XSS的payload要考虑持久性。你提交的payload会被存进数据库,如果payload里有特殊字符导致SQL报错,可能存不进去。所以尽量用简单的payload,确认能弹窗后再逐步升级。
3.3 存储型XSS的隐蔽性与危害放大
存储型XSS最可怕的地方在于它的隐蔽性。反射型XSS的URL里带着明显的payload,稍微有点安全意识的人都能看出来。但存储型XSS的URL是干净的,受害者访问的是正常页面,却在不知不觉中执行了攻击者的脚本。
在闯关平台中,存储型XSS关卡通常会模拟一个后台管理页面。你作为普通用户提交留言,管理员查看留言时触发XSS,你的脚本就能读取管理员的cookie或者以管理员身份执行操作。这就是XSS最经典的利用链:存储型XSS + 后台 = 越权。
在实战中,存储型XSS的利用方式包括:
- 窃取cookie(如果没设HttpOnly)
- 构造CSRF请求,以受害者身份执行操作
- 键盘记录,窃取敏感信息
- 钓鱼,弹出伪造的登录框
注意:闯关平台中的cookie窃取通常需要配合一个接收服务器。在本地测试时,你可以用
alert(document.cookie)来验证,不需要真的搭建接收端。
4. DOM型XSS:不经过服务端的攻击面
4.1 DOM型XSS的独特之处
DOM型XSS和前面两种最大的区别是:payload不经过服务端。整个攻击过程发生在浏览器端,服务端返回的HTML是"干净"的,是前端JavaScript代码把用户输入拼接到了DOM里,导致了XSS。
典型的DOM型XSS代码长这样:
var name = location.hash.substring(1); document.getElementById("output").innerHTML = name;你访问page.html#<img src=x onerror=alert(1)>,JavaScript从hash里取出payload,用innerHTML插入页面,触发XSS。服务端完全不知道发生了什么。
4.2 定位DOM型XSS的source和sink
解DOM型XSS关卡,关键是找到source(输入来源)和sink(危险输出点)。常见的source有:
location.hashlocation.searchdocument.referrerwindow.namepostMessage的数据
常见的sink有:
innerHTMLouterHTMLdocument.writeevalsetTimeout/setInterval(传入字符串时)Function构造函数
在闯关平台中,你可以打开F12,在Sources面板里搜索这些关键词,找到拼接逻辑。比如你搜innerHTML,发现某段代码把location.search的值直接赋给了innerHTML,那这就是漏洞点。
4.3 DOM型XSS的绕过与利用
DOM型XSS的绕过思路和反射型类似,但要注意一点:payload可能经过URL编码。比如location.search取出来的值是编码后的,你需要用decodeURIComponent解码。在构造payload时,要确保编码后的形式能正确传递。
举个例子,如果代码是:
var q = decodeURIComponent(location.search.substring(1)); document.getElementById("result").innerHTML = q;你访问page.html?<img src=x onerror=alert(1)>,浏览器会自动把<、>编码成%3C、%3E,decodeURIComponent解码后还原成<img>,触发XSS。
但如果代码没有decodeURIComponent,你就需要手动编码payload,确保取出来的是可执行的HTML。
DOM型XSS还有一个特殊点:它可能不依赖用户交互。比如innerHTML插入的<img onerror>会自动触发,不需要用户点击。而javascript:协议需要点击。在闯关时,优先用自动触发的payload。
5. XSS闯关中的过滤绕过与编码技巧
5.1 常见过滤规则与对应绕过表
在闯关过程中,你会遇到各种各样的过滤规则。下面这张表整理了最常见的过滤方式和对应的绕过思路,建议收藏:
| 过滤规则 | 绕过思路 | 示例payload |
|---|---|---|
过滤<script> | 用事件型标签 | <img src=x onerror=alert(1)> |
过滤script关键词 | 双写或大小写 | <scrscriptipt>或<ScRiPt> |
过滤alert | 用其他函数或拼接 | confirm(1)或eval('ale'+'rt(1)') |
过滤<和> | 利用现有标签事件 | ' onmouseover='alert(1) |
| 过滤引号 | 用反引号或不用引号 | <img src=x onerror=alert(1)> |
| 过滤空格 | 用/或%09 | <img/src=x/onerror=alert(1)> |
过滤on事件 | 用其他事件或标签 | <svg onload=alert(1)> |
| 过滤括号 | 用throw或模板字符串 | <svg onload=alert\1`>` |
5.2 HTML实体编码与URL编码的实战运用
编码是绕过过滤的核心武器。你需要搞清楚三种编码的区别和适用场景:
HTML实体编码:把<变成<,>变成>,"变成"。这种编码在HTML解析时会被还原。如果你能控制输出点,可以用实体编码绕过关键词过滤。比如javascript可以写成javascript。
URL编码:把<变成%3C,>变成%3E。这种编码在URL传递时使用,服务端或前端解码后还原。在DOM型XSS中特别常用。
JavaScript编码:把字符变成\x3c、\u003c这种形式。在JavaScript代码里执行时还原。如果payload出现在<script>标签内部,可以用这种编码。
实战中经常需要多重编码。比如服务端解码一次,前端再解码一次,你就要编码两次。这个需要根据具体关卡测试,没有万能公式。
5.3 浏览器解析优先级与payload构造
浏览器解析HTML有一套优先级规则,理解这个规则能帮你构造更可靠的payload。比如:
<script>标签内的内容不会被当作HTML解析,所以里面不能放HTML标签- 属性值里的内容不会被当作标签解析,除非你闭合了属性
- 注释
<!-- -->里的内容不会被解析,但可以用-->提前闭合注释
有个经典技巧是利用<!--和-->来绕过某些过滤。比如你的输入出现在注释里:
<!-- 你的输入 -->你可以输入--><script>alert(1)</script><!--来闭合注释并注入脚本。
另一个技巧是利用标签的嵌套规则。比如<title>标签里的内容不会被解析为HTML,但你可以用</title>闭合它,然后注入脚本。
6. 从闯关到实战:XSS防御与检测的逆向思维
6.1 防御者视角:XSS的根治方案
刷完闯关,你应该反过来想:如果我是开发者,怎么防住这些攻击?答案其实很明确:
第一,输出编码。根据输出位置选择正确的编码方式。HTML内容用HTML实体编码,属性用属性编码,JavaScript用JS编码,URL用URL编码。这是最根本的防御手段。
第二,输入验证。对用户输入做白名单校验,比如昵称只允许字母数字,邮箱必须符合格式。但输入验证不能替代输出编码,因为攻击者可以绕过前端验证直接发请求。
第三,CSP(内容安全策略)。通过HTTP头限制页面能加载哪些资源、能执行哪些脚本。CSP是最后一道防线,即使有XSS漏洞,攻击者也可能因为CSP而无法执行脚本。
第四,HttpOnly Cookie。给敏感cookie加上HttpOnly标记,JavaScript就读不到,即使XSS成功也偷不走cookie。
6.2 检测者视角:如何快速发现XSS漏洞
在渗透测试中,发现XSS漏洞的效率取决于你的测试方法。我的经验是:
第一步,找所有输入点。URL参数、表单字段、HTTP头(Referer、User-Agent)、Cookie、postMessage,这些都是潜在的source。
第二步,用唯一标记测试。输入一个独特的字符串比如xss123test,然后在页面源码里搜索这个字符串,看它出现在哪里、有没有被转义。
第三步,根据上下文构造payload。如果出现在标签之间,试<img src=x onerror=alert(1)>;如果出现在属性里,试闭合属性;如果出现在JavaScript里,试闭合引号或字符串。
第四步,用自动化工具辅助。Burp Suite的Scanner、XSStrike、DalFox这些工具能帮你快速筛选,但最终验证还是要手动。
6.3 闯关训练的真正价值
很多人刷完XSS闯关就觉得自己会XSS了,其实差得远。闯关平台的环境是理想化的,没有WAF、没有CSP、没有前端框架的自动转义。真实项目中,你可能遇到:
- React/Vue的自动转义,让你根本注入不了
- 严格的CSP,只允许加载同源脚本
- WAF的语义分析,能识别出变形后的payload
- 各种编码嵌套,让你搞不清到底解码了几层
但闯关训练的价值在于,它帮你建立了条件反射式的上下文判断能力。你看到一个输入点,脑子里会自动浮现出"这是HTML上下文还是属性上下文还是JS上下文",然后自动匹配对应的payload。这个能力是实战的基础,没有它,你在真实项目中就是无头苍蝇。
我的建议是:闯关刷一遍就够了,重点是理解每关的原理。然后去真实的漏洞平台(比如DVWA、Pikachu)练习,再然后去SRC或众测项目里实战。只有经历过真实环境的毒打,你才算真正掌握了XSS。
最后分享一个我在实战中总结的小技巧:遇到过滤不要慌,先把过滤规则摸清楚。你可以输入<、>、"、'、script、alert这些关键词,看哪些被过滤了、哪些没被过滤。然后根据过滤规则反推绕过方案。这个过程比盲目试payload高效十倍。