1. 从零理解XSS:为什么这19关值得反复刷
很多人第一次接触XSS(跨站脚本攻击)时,觉得概念特别简单——不就是往页面里塞一段JavaScript吗?我当初也这么想,结果真正上手打靶场的时候,第一关就卡住了。后来才明白,XSS的难点从来不是"注入JS"这个动作本身,而是绕过各种过滤、编码、上下文限制,让注入的代码真正在目标浏览器里执行。这套XSS Challenges从第1关到第19关,几乎把Web开发中常见的XSS防御手段和对应的绕过思路都覆盖了一遍,是打基础阶段性价比极高的练习材料。
这篇文章我会按照自己的通关顺序,把每一关的注入点判断、上下文分析、payload构造、绕过思路完整拆开讲。不是简单贴答案,而是把"为什么这个payload能过、为什么那个不行"讲清楚。适合已经了解HTML和JavaScript基础、想系统掌握XSS实战技巧的朋友。如果你连<script>标签和事件属性都还没分清,建议先补一下前端基础再来看,不然会有点吃力。
先说一个贯穿全程的核心方法论:打XSS靶场,第一步永远是判断输出上下文。你注入的内容最终出现在HTML的什么位置?是标签之间、属性值里、还是JavaScript代码块中?不同上下文对应的payload完全不同。很多人上来就甩<script>alert(1)</script>,结果发现被转义了或者根本不执行,就是因为没做上下文分析。这19关里,至少有三分之一是在训练这个判断能力。
另外提前说明,本文所有内容仅用于安全学习和防御研究,请勿用于任何未授权的测试。靶场环境是专门搭建来练习的,真实业务系统上乱来是要负法律责任的。
2. 前五关:从最裸的注入到属性上下文突破
2.1 第1-3关:无过滤直出与基础标签注入
第1关基本是送分题。页面直接把URL参数拼接到HTML里输出,没有任何过滤。你只要在参数后面接<script>alert(document.domain)</script>就能弹窗。这一关的意义在于让你确认输出点位置——打开浏览器开发者工具,看你的输入落在了哪个标签的哪个位置,这是后面所有关卡的基础动作。
第2关开始有了变化。它把输入放进了<input>标签的value属性里,类似这样:
<input value="你的输入">这时候你直接写<script>是没用的,因为它在属性值内部,浏览器会把它当成属性值的一部分。正确做法是先闭合属性,再注入标签。payload是:
"><script>alert(document.domain)</script>这里的"闭合了value属性的引号,>闭合了input标签,然后插入script标签。这一关教的是属性上下文下的闭合思路。
第3关的过滤稍微升级,它把<和>做了转义或者过滤。这时候script标签用不了了,但可以换事件属性。比如:
" onmouseover="alert(document.domain)或者用onfocus配合autofocus:
" autofocus onfocus="alert(document.domain)autofocus让输入框自动获得焦点,onfocus事件随即触发,不需要用户交互就能弹窗。这个技巧在后面很多关都会用到,非常实用。
2.2 第4-5关:大小写与编码绕过初体验
第4关开始对script关键字做过滤,但它只过滤了小写的script。这时候最简单的绕过就是大小写混写:
<ScRiPt>alert(document.domain)</ScRiPt>HTML标签名是大小写不敏感的,浏览器照样解析执行。这一关告诉你一个道理:过滤规则写得不够严谨,等于没写。很多初级防御就是简单替换小写关键字,绕过成本极低。
第5关进一步,它可能把script这个字符串整个过滤掉了(不管大小写)。这时候可以用嵌套构造来绕过。比如输入<scr<script>ipt>,过滤函数删掉中间的<script>后,剩下的部分重新拼接成<script>。这是一种经典的过滤绕过思路——利用过滤函数"删除一次"的特性,构造出删除后刚好成型的payload。
这一关还有一种思路是用其他标签,比如<img src=x onerror=alert(document.domain)>。onerror在图片加载失败时触发,src=x保证加载失败,弹窗就执行了。这个payload在后面会反复出现,是XSS里最常用的向量之一。
3. 中段关卡:编码、闭合与过滤逻辑的博弈
3.1 第6-8关:HTML实体编码与属性闭合的细节
第6关的输出位置又变了,这次输入被放进了<a href="...">这样的链接属性里。这种上下文下,<script>和事件属性都可能被限制,但可以用伪协议:
javascript:alert(document.domain)如果href的值可控,点击链接时就会执行JS。不过要注意,现代浏览器对javascript:协议在href里的执行有一些限制,但在靶场环境里通常还是有效的。
第7关开始对引号做处理。如果双引号被转义了,可以试试单引号,或者不用引号。比如在属性值里,如果空格没被过滤,可以这样:
<img src=x onerror=alert(document.domain)>这里onerror的值没有用引号包裹,因为alert(document.domain)里没有空格,浏览器能正确解析。如果表达式里有空格,可以用/**/注释或者eval配合String.fromCharCode来规避。
第8关的过滤逻辑有点意思,它可能把on开头的属性都过滤了。这时候可以换其他事件,比如onpointerover、onanimationstart等冷门事件。或者用<svg>标签配合onload:
<svg onload=alert(document.domain)>SVG标签在HTML5里是合法内联的,onload事件在SVG加载时触发。这个向量在很多过滤规则下都能绕过,因为防御者往往只盯着img和script。
3.2 第9-11关:JavaScript上下文里的逃逸技巧
第9关是个转折点,输入被放进了<script>代码块内部,类似:
var name = "你的输入";这种上下文下,<script>标签和HTML事件属性都没用了,因为你在JS代码里。你需要先闭合字符串,再执行代码。如果双引号没被转义:
";alert(document.domain);//"闭合了字符串,;结束当前语句,alert执行,//注释掉后面的代码防止报错。
第10关对引号做了转义,但可能没处理反斜杠。这时候可以试试用\来逃逸:
\";alert(document.domain);//如果转义逻辑是简单地在引号前加\,那么你输入\",它可能变成\\",反斜杠被转义,引号反而逃出来了。这种转义逻辑的漏洞在真实代码里也很常见。
第11关可能把alert关键字过滤了。这时候可以用编码或替代函数:
";eval(String.fromCharCode(97,108,101,114,116,40,49,41));//String.fromCharCode把字符码转成字符串,eval执行。或者用top["al"+"ert"](1)这种拼接方式绕过关键字检测。
3.3 第12-14关:DOM型XSS与URL编码绕过
第12关开始涉及DOM型XSS。这类XSS的特点是:注入的内容不经过服务器,而是被前端JavaScript直接读取并写入页面。比如:
document.getElementById("output").innerHTML = location.hash.substring(1);这种场景下,你需要在URL的hash部分注入。因为hash不会发送到服务器,服务端的过滤完全无效。payload可能是:
#<img src=x onerror=alert(document.domain)>第13关可能对URL做了编码处理。这时候需要双重编码或者混合编码。比如<的URL编码是%3C,如果服务器解码一次后直接输出,你可以用%253C(%25是%的编码),服务器解码一次变成%3C,浏览器再解码一次变成<。这种编码层级不匹配是绕过过滤的经典手法。
第14关的过滤可能针对alert做了黑名单。这时候可以用confirm、prompt,或者用console.log配合其他方式验证。靶场通常只要求弹窗,所以confirm(1)和prompt(1)都是合法替代。
4. 后段关卡:高级绕过与综合实战
4.1 第15-17关:白名单、CSP与冷门向量
第15关可能引入了白名单机制,只允许特定标签或属性。这时候需要找白名单里的"漏网之鱼"。比如只允许<a>标签,那可以试试<a href="javascript:alert(1)">。如果只允许<img>,那就用onerror。白名单的关键是找到允许列表里能执行JS的组合。
第16关可能加了简单的CSP(内容安全策略)。CSP通过HTTP头限制哪些源可以执行脚本。如果CSP配置不严,比如允许unsafe-inline,那内联脚本照样执行。如果限制了script-src,可以试试JSONP端点或者AngularJS沙箱逃逸(如果页面引入了Angular)。靶场里的CSP通常有配置缺陷,找到那个缺陷就能绕过。
第17关的过滤可能针对空格、括号、引号都做了处理。这时候可以用反引号替代括号(在某些上下文里),或者用throw配合onerror:
<img src=x onerror="throw onerror=alert,1">throw会触发window.onerror,如果onerror被赋值为alert,就会执行alert(1)。这种利用错误处理机制的思路比较巧妙,值得记住。
4.2 第18-19关:综合过滤下的组合拳
第18关通常是前面所有过滤的集合,可能同时过滤了script、on事件、引号、括号等。这时候需要组合多种技巧。比如用<svg><script>(SVG内的script标签),配合href和xlink:href,或者用<math>标签配合事件。
一个常见的综合payload是:
<svg><animate xlink:href=#x attributeName=href values=javascript:alert(1) /><a id=x><text x=20 y=20>click</text></a></svg>这个payload利用了SVG的animate元素动态修改href属性,点击文本时执行JS。它绕过了对script和常规事件属性的过滤。
第19关是最终Boss,过滤规则最严。这时候往往需要仔细阅读页面源码,找到过滤逻辑的漏洞。比如过滤函数可能只检查了第一次出现的危险字符,或者正则表达式写得有缺陷。我当时的做法是:把每一关的源码都看一遍,理解它的过滤逻辑,然后针对性地构造payload。理解过滤逻辑比背payload重要得多,因为真实环境里的过滤规则千变万化,只有掌握原理才能随机应变。
5. 通关之后:我总结的XSS实战检查清单
打完这19关,我整理了一份自己的检查清单,每次遇到新的XSS场景都会过一遍:
| 检查项 | 具体动作 | 常见绕过 |
|---|---|---|
| 输出上下文 | 看输入落在HTML的哪个位置 | 标签间、属性内、JS内、CSS内 |
| 过滤字符 | 测试< > " ' ( )和关键字 | 大小写、编码、嵌套、替代标签 |
| 编码层级 | 确认服务器解码几次 | 双重编码、混合编码 |
| 浏览器解析 | 确认浏览器如何解析 | 伪协议、SVG、MathML、冷门事件 |
| CSP策略 | 看HTTP头里的CSP配置 | JSONP、unsafe-inline、配置缺陷 |
几个我踩过的坑也分享一下。第一,不要忽略document.domain和alert(1)的区别。有些靶场要求弹出特定内容,用alert(1)可能不算通关,要看清楚题目要求。第二,浏览器版本会影响payload效果。现代浏览器对某些向量(比如javascript:协议在href里)有额外限制,如果payload不生效,先换个浏览器或者查一下兼容性。第三,开发者工具是你的好朋友。每次注入后,打开Elements面板看你的输入最终变成了什么HTML,这比盲目试payload高效得多。
最后说一个心态问题。打XSS靶场最忌讳的就是"背答案"。这19关的payload网上一搜一大把,但如果你只是复制粘贴,遇到变种就懵了。真正有价值的是理解每一关的过滤逻辑和绕过原理,这样你才能在实际的安全评估中快速定位问题。我自己的习惯是:每过一关,都在笔记里写下"这关的过滤是什么、我用了什么思路、还有没有其他解法"。积累下来,就是一套属于自己的XSS知识体系。
另外,如果你是想做防御方,这19关反过来看就是一份XSS防御检查清单。每一关的过滤缺陷,都对应着一种防御措施:输出编码、上下文感知转义、CSP、白名单验证等。理解攻击手法是为了更好地防御,这个思路在安全领域永远成立。