1. 从零开始理解XSS靶场练习的价值
很多人第一次接触Web安全练习平台时,都会有一个共同的困惑:看了一堆理论,知道反射型、存储型、DOM型XSS的分类,但真给一个输入框,却不知道从哪下手。我当初也是这样,书翻了三遍,笔记记了两大本,结果打开靶场第一关就卡住了——不是不会技术,而是不知道“怎么开始”。
这个练习记录系列,就是把我从第一关开始踩过的坑、绕过的弯、以及后来总结出来的高效通关思路,完整地梳理一遍。它适合两类人:一类是刚学完XSS基础理论、急需上手实操的新手;另一类是有一定经验、但想系统梳理自己知识盲区的中级练习者。整个系列会围绕一个在线安全练习平台的XSS模块展开,从最基础的反射型开始,逐步深入到存储型、DOM型以及各种绕过技巧。
为什么选择从靶场练习入手?因为XSS这个东西,光看原理很容易产生“我已经会了”的错觉。比如你知道<script>alert(1)</script>是最基本的payload,但实际环境中,服务器可能过滤了<script>标签,可能对引号做了转义,可能把输入放进了HTML属性里而不是标签之间。这些场景的差异,只有亲手试过才能形成肌肉记忆。靶场的好处在于,它把这些场景一个个拆开,让你在受控环境里反复练习,直到形成条件反射。
我个人的体会是,前五关是最痛苦的,因为你要同时处理“理解漏洞原理”和“熟悉工具操作”两件事。但过了第十关之后,你会发现很多关卡的核心逻辑是相通的,区别只在于过滤规则和输出位置的细微变化。所以这个记录会尽量把每一关的“为什么”讲清楚,而不是只给一个payload让你复制粘贴。毕竟,靶场的目的是训练思维,不是收集答案。
2. 练习前的环境准备与基础工具选型
2.1 浏览器选择与开发者工具配置
做XSS练习,浏览器就是你的主战场。我试过几款主流浏览器,最后固定用Chrome或者基于Chromium的Edge。原因很简单:开发者工具(DevTools)的Elements面板对DOM结构的展示最直观,Console面板的报错信息也最清晰。Firefox的开发者工具这几年进步很大,但在处理一些编码问题时,Chrome的Network面板显示原始请求和响应的方式更符合我的习惯。
具体配置上,有几个设置我建议你提前打开。第一,在DevTools的Settings里,把“Console”中的“Preserve log”勾上。这样页面跳转时,之前的console输出不会被清空,对于跟踪反射型XSS的触发过程非常有用。第二,在“Network”面板里,勾选“Disable cache”,确保每次请求都是最新的,避免浏览器缓存干扰你判断payload是否被服务器处理。第三,如果你用的是Chrome,可以在地址栏输入chrome://flags,搜索“Auto-fill”,把自动填充相关的选项关掉。这个很关键——有时候你输入payload后,浏览器自动填充功能会弹出下拉框,遮挡页面元素,让你误以为payload没生效。
提示:不要用浏览器的“无痕模式”做练习。无痕模式下,某些扩展和缓存行为与正常模式不同,可能导致你观察到的现象和实际有偏差。就用普通窗口,但记得每次测试前手动清一下缓存(Ctrl+Shift+Delete)。
2.2 必备的辅助工具与插件
除了浏览器自带的开发者工具,我强烈建议装一个专门用来做XSS练习的插件。这里不推荐具体品牌,但你可以搜索“XSS辅助”类的浏览器扩展,核心功能是帮你快速编码/解码URL、HTML实体、Base64等。为什么需要这个?因为很多关卡会对你输入的字符做编码处理,比如把<变成<,把"变成"。如果你手动去数编码后的字符,效率极低,而且容易出错。有了编码工具,你可以一键切换查看原始值和编码后的值,快速判断服务器的过滤逻辑。
另一个我常用的工具是Burp Suite的社区版。虽然靶场练习大部分时候用浏览器就够了,但当你遇到需要修改请求方法、添加自定义Header、或者重放请求的场景时,Burp的Proxy和Repeater功能会节省大量时间。比如有一关要求你通过修改Referer头来触发XSS,如果你没有代理工具,就只能靠浏览器插件或者手动构造请求,非常麻烦。Burp的安装和基础配置网上教程很多,这里不展开,只提醒一点:记得在浏览器里安装Burp的CA证书,否则HTTPS请求会报错。
2.3 靶场账号注册与练习节奏安排
大部分在线安全练习平台都需要注册账号才能记录进度。注册时建议用一个专门的邮箱,不要用你的主力邮箱。密码方面,虽然平台本身是安全的,但养成好习惯总没错。注册完成后,先花十分钟把平台的界面熟悉一下。通常会有“关卡列表”、“提示”、“提交答案”这几个核心区域。有些平台还会提供“重置进度”的选项,如果你某一关卡太久,想从头梳理思路,这个功能很有用。
关于练习节奏,我的建议是每天只做2-3关,但每一关都要做到“三遍原则”:第一遍自己摸索,哪怕花一个小时;第二遍看提示或 writeup,理解正确思路;第三遍关掉所有参考,从头独立完成。这个过程很枯燥,但效果远超一天刷十关。我见过太多人为了追求进度,直接复制payload过关,结果遇到稍微变形的场景就束手无策。靶场练习的核心价值在于“试错”,而不是“收集”。
3. XSS核心原理与靶场关卡设计逻辑
3.1 反射型XSS的触发链条拆解
反射型XSS是靶场里最常见的类型,也是理解其他类型的基础。它的核心逻辑是:服务器接收了你的输入,没有做充分处理,直接“反射”回响应页面中,浏览器把这个响应当作HTML解析,从而执行了你注入的脚本。听起来很简单,但实际关卡会从三个维度增加难度:输入点位置、输出点位置、过滤规则。
输入点位置决定了你如何提交payload。常见的有URL参数(如?q=xxx)、表单字段(POST请求)、HTTP头(如User-Agent、Referer)。输出点位置决定了payload的上下文。比如输出在<div>xxx</div>之间,你可以直接用<script>标签;输出在<input value="xxx">的属性里,你就需要先闭合引号,再注入事件处理器,比如" onmouseover="alert(1)。过滤规则则是服务器对你输入的字符做了哪些限制,比如过滤了<script>、转义了引号、限制了长度等。
我刚开始练习时,最大的问题是“不看上下文就乱试payload”。比如明明输出在属性里,我还在试<script>alert(1)</script>,当然不会成功。后来我养成了一个习惯:每关先提交一个特殊字符串,比如xss123test,然后在响应里搜索这个字符串,看它出现在哪里、周围是什么字符。这个“探针法”能帮你快速定位输出点,比盲目试payload高效得多。
3.2 存储型XSS与反射型的本质差异
存储型XSS和反射型的区别,用一句话概括就是:反射型是“一次性”的,存储型是“持久化”的。反射型XSS的payload存在于URL或请求体中,只有受害者点击了构造好的链接才会触发。存储型XSS的payload被服务器保存到了数据库或文件里,任何访问该页面的用户都会中招。
在靶场里,存储型XSS通常出现在留言板、评论框、用户昵称这些功能中。你提交的内容会被保存,然后在其他页面展示。练习存储型XSS时,有一个关键点容易被忽略:你需要确认payload是否真的被存储了。有些平台会在你提交后立即显示内容,但这可能是前端渲染的结果,并不代表后端存储了。正确的验证方法是:提交payload后,关闭浏览器,重新打开页面,或者换一个浏览器/无痕窗口访问,看payload是否还在。如果还在,说明是存储型;如果消失了,那可能只是反射型。
另一个差异是危害程度。反射型XSS需要诱导用户点击链接,存储型XSS则不需要任何交互,只要用户访问页面就会触发。所以在实际漏洞评级中,存储型XSS的严重性通常更高。靶场练习时,存储型关卡往往还会结合其他漏洞,比如CSRF,让你构造一个自动提交的payload,模拟蠕虫传播的效果。这些进阶场景在后面关卡中会逐步遇到。
3.3 DOM型XSS的独特之处与常见误区
DOM型XSS是三种类型里最特殊的一个。它的特殊之处在于:payload完全没有经过服务器。服务器返回的HTML是“干净”的,但页面上的JavaScript代码在处理URL参数、location.hash、或者用户输入时,把这些数据直接写入了DOM的innerHTML、document.write等危险位置,导致浏览器执行了脚本。
很多新手在练习DOM型XSS时,会习惯性地去查看Network面板,看服务器响应里有没有自己的payload。如果没有,就以为payload没生效。这是最大的误区。DOM型XSS的触发完全在浏览器端,服务器响应里看不到任何异常。正确的排查方法是:打开DevTools的Sources面板,在页面加载的JavaScript代码里搜索innerHTML、document.write、eval、setTimeout这些危险函数,看它们是否接收了来自location、document.URL、document.referrer等源的数据。
我踩过的一个坑是:有一关的DOM型XSS,我提交payload后页面没有任何反应。我以为是过滤问题,试了各种编码。后来才发现,触发条件是在URL的hash部分(#后面的内容),而我在提交时把payload放在了查询参数里。hash部分不会发送到服务器,所以Network面板里看不到,但JavaScript代码会读取location.hash并写入DOM。这个教训让我明白:做DOM型XSS时,一定要先看JavaScript代码的逻辑,而不是盲目试payload。
4. 逐关实操记录与核心Payload构造
4.1 第一关:最基础的反射型注入
第一关通常是“送分题”,目的是让你熟悉靶场的操作流程。输入点是一个搜索框,输出点直接在页面中央显示“你搜索的内容是:xxx”。没有任何过滤,也没有任何编码。你输入<script>alert(1)</script>,页面就会弹窗。
这一关的payload就是最经典的<script>alert(1)</script>。但我想说的是,不要因为简单就跳过。你要观察的是:输入框提交后,URL变成了什么?是GET请求还是POST请求?响应页面里,你的payload出现在HTML的哪个位置?是标签之间,还是属性里面?这些观察结果会决定你后面关卡的解题速度。
我第一遍做的时候,直接输入payload就过关了。但第二遍我特意去看了Network面板,发现请求是GET /?q=<script>alert(1)</script>,响应里payload原封不动地出现在<div class="result">里面。第三遍我尝试了<img src=x onerror=alert(1)>,同样成功。这说明服务器对标签类型没有限制,只做了最基本的反射。这个信息在后续关卡中很有用——如果某一关过滤了<script>,你可以先试试<img>。
注意:有些靶场的第一关会故意把payload放在
<h1>标签里,这时候<script>标签可能被浏览器解析为标题的一部分,导致不执行。如果遇到这种情况,试试</h1><script>alert(1)</script>,先闭合前面的标签。
4.2 第三关:属性内的注入与引号闭合
到了第三关左右,难度会明显提升。这一关的输出点在一个<input>标签的value属性里,类似<input value="你的输入">。如果你直接输入<script>alert(1)</script>,浏览器会把它当作属性值的一部分,不会执行。你需要先闭合value属性的引号,然后注入事件处理器。
正确的payload是" onmouseover="alert(1)。拆解一下:第一个"闭合了value属性的双引号;空格用来分隔属性;onmouseover="alert(1)"是一个事件处理器,当鼠标悬停在输入框上时触发。注意最后的"是可选的,因为浏览器会自动补全。但为了严谨,我通常会加上。
这一关的难点在于:你需要判断属性是用双引号还是单引号包裹的。如果是单引号,payload就要改成' onmouseover='alert(1)。怎么判断?看响应里的HTML源码。如果源码显示value="xxx",就是双引号;如果显示value='xxx',就是单引号。有些服务器会把双引号转义成",这时候你就不能用双引号闭合了,得试试单引号,或者用不需要引号的事件处理器,比如onmouseover=alert(1)(注意没有引号)。
我踩过的一个坑是:payload里的空格被服务器过滤了。比如onmouseover=alert(1)里的空格没了,变成onmouseover=alert(1),浏览器无法解析。这时候可以用/代替空格,比如onmouseover=alert(1)/,或者用Tab字符(%09)代替。这些技巧在后面的关卡中会反复用到。
4.3 第五关:标签过滤与替代方案
第五关开始,服务器会过滤一些关键词。最常见的是过滤<script>,你输入后会发现它被删除了,或者变成了空字符串。这时候你需要找替代标签。<img>是最常用的,因为它的onerror事件在图片加载失败时必然触发。payload是<img src=x onerror=alert(1)>。src=x是一个不存在的图片地址,所以浏览器会触发onerror。
如果<img>也被过滤了,可以试试<svg>。SVG标签支持onload事件,payload是<svg onload=alert(1)>。<body>标签也可以,<body onload=alert(1)>,但要注意,如果页面已经有一个<body>标签了,再插入一个可能会导致DOM结构混乱,有些浏览器会忽略第二个<body>的onload事件。
还有一个万能标签是<details>。<details open ontoggle=alert(1)>,ontoggle事件在details元素展开或收起时触发,配合open属性,页面加载时就会触发。这个标签的好处是,它不太常见,很多过滤规则不会把它列入黑名单。
我个人的经验是:先试<img>,不行再试<svg>,再不行试<details>。如果都不行,那说明服务器可能过滤了事件处理器(如onerror、onload),这时候就需要更高级的绕过技巧,比如用<a href="javascript:alert(1)">,或者利用CSS的expression(老版本IE有效,现代浏览器已废弃)。
4.4 第八关:编码绕过与大小写混淆
第八关的过滤规则更严格了。服务器不仅过滤了<script>,还过滤了onerror、onload这些事件处理器关键词。你输入onerror,它可能被替换成空字符串。这时候就需要用到编码绕过。
HTML实体编码是最常用的。比如onerror可以写成onerror,浏览器在解析HTML属性时,会把这些实体解码成onerror。但要注意,实体编码在标签之间(如<div>o...</div>)会被解码,但在属性值里,如果属性本身没有被引号包裹,实体编码可能不会被解码。所以这个技巧要看具体的输出上下文。
大小写混淆也很有效。如果服务器只过滤小写的onerror,你可以试试OnError、oNeRrOr。HTML属性名是大小写不敏感的,浏览器都能识别。但有些服务器会用正则表达式做不区分大小写的匹配,这时候大小写混淆就无效了。
还有一个技巧是使用eval和String.fromCharCode。如果服务器过滤了alert,你可以用eval(String.fromCharCode(97,108,101,114,116,40,49,41))来构造alert(1)。这个payload比较长,但绕过效果很好。不过要注意,eval本身也可能被过滤,所以有时候需要把eval也编码,比如window['ev'+'al']。
我实测下来,第八关最稳的payload是<img src=x onerror=eval(String.fromCharCode(97,108,101,114,116,40,49,41))>。但如果onerror被过滤了,就得换成<svg onload=...>,或者用<details ontoggle=...>。这一关的核心思路是:不要死磕一个标签或一个事件,多准备几套备选方案。
4.5 第十关:存储型XSS与持久化验证
第十关通常是存储型XSS的入门关。页面有一个留言板功能,你提交的内容会被保存,并在下方显示。输入点是一个<textarea>,输出点在<div class="comment">里面。服务器可能对<script>做了过滤,但对其他标签没有限制。
我的payload是<img src=x onerror=alert(1)>。提交后,页面立即弹窗。但关键步骤是验证持久化:我关闭浏览器,重新打开页面,发现弹窗再次出现。这说明payload确实被存储到了服务器端。如果重新打开后没有弹窗,那可能只是前端渲染,并没有真正存储。
这一关还有一个变种:留言内容会被转义,比如<变成<。这时候你需要检查转义发生在哪个环节。如果是前端JavaScript做的转义,你可以用Burp抓包,直接修改请求体,绕过前端验证。如果是后端转义,那就需要找其他注入点,比如用户昵称、个人简介这些可能没有转义的地方。
我踩过的一个坑是:留言板有长度限制,我的payload太长被截断了。后来我把payload精简成<img src=x onerror=alert(1)>,刚好在限制范围内。如果实在超长,可以用<script>alert(1)</script>,但前提是<script>没有被过滤。另一个技巧是使用短域名或者外部JS文件,但靶场环境通常不允许外联,所以还是尽量精简payload。
5. 常见问题排查与避坑经验汇总
5.1 Payload不生效的排查思路
Payload不生效是练习中最常见的问题。我总结了一个排查顺序,基本能覆盖90%的情况。第一步,确认输出点位置。用探针字符串(如xss123test)提交,然后在响应里搜索,看它出现在HTML的哪里。如果出现在标签之间,用<script>或<img>;如果出现在属性里,先闭合引号再注入事件。第二步,检查特殊字符是否被转义。看响应里的<是否变成了<,"是否变成了"。如果是,说明服务器做了HTML实体编码,你需要找不需要这些字符的payload,或者用编码绕过。第三步,检查关键词是否被过滤。提交<script>alert(1)</script>,看响应里script这个词是否还在。如果没了,说明被过滤了,换<img>或<svg>。第四步,检查浏览器是否执行了脚本。打开Console面板,看有没有报错。有时候payload被正确注入了,但被CSP(内容安全策略)阻止了。靶场环境一般不会开CSP,但如果你在本地搭建环境练习,可能会遇到。
还有一个容易被忽略的点:payload里的引号类型。如果你在HTML属性里注入,而属性用的是双引号,你用了单引号闭合,那肯定不生效。反过来也一样。所以每次都要先看源码里的引号类型。
5.2 编码与转义的常见陷阱
编码和转义是XSS练习中最容易混淆的概念。我简单区分一下:编码(Encoding)是把字符转换成另一种表示形式,比如URL编码把空格变成%20,HTML实体编码把<变成<。转义(Escaping)是在字符前面加反斜杠,比如JavaScript里的\"。服务器可能对输入做编码,也可能做转义,还可能两者都做。
URL编码的陷阱在于:浏览器会自动对URL中的特殊字符进行编码,但服务器解码的时机和方式可能不同。比如你提交%3Cscript%3E,服务器可能解码成<script>,也可能不解码,直接原样输出。这取决于服务器使用的框架和配置。我的经验是:如果payload在URL里,先试试不编码直接提交,如果不行再手动编码。手动编码时,注意只编码必要的字符,比如<、>、"、',不要全部编码,否则服务器可能无法识别。
HTML实体编码的陷阱在于:实体编码只在特定的上下文里被解码。在标签之间,<会被解码成<;但在属性值里,如果属性没有被引号包裹,<可能不会被解码。所以用实体编码绕过时,一定要确认输出点的上下文。
5.3 浏览器差异与兼容性注意事项
不同浏览器对XSS payload的解析行为有细微差异。比如<svg onload=alert(1)>在Chrome和Firefox里都能触发,但在某些老版本的Safari里可能不生效。<details ontoggle=alert(1)>在Chrome里需要open属性才会自动触发,但在Firefox里,即使没有open,用户点击details元素时也会触发。
另一个差异是事件处理器的执行时机。onerror在图片加载失败时触发,但如果图片地址是有效的,就不会触发。onload在资源加载完成时触发,如果资源加载很慢,弹窗会延迟。onmouseover需要用户交互,不适合自动化测试。
我的建议是:靶场练习时,固定用一个浏览器(推荐Chrome),把每个payload在不同浏览器里的表现记录下来。这样在实际项目中,你就能根据目标用户的浏览器分布,选择最合适的payload。另外,现代浏览器对某些标签和事件有安全限制,比如<script>标签通过innerHTML插入时不会执行,必须用document.write或createElement。这些细节在DOM型XSS关卡中会经常遇到。
5.4 靶场练习的常见误区与心态调整
最后一个想聊的是心态问题。我见过很多人卡在某一关,试了半小时没成功,就开始烦躁,最后直接搜答案。搜答案本身没问题,但关键是搜完之后要复盘:为什么我没想到这个思路?是哪个知识点遗漏了?下次遇到类似场景,我能不能自己推导出来?
另一个误区是“追求通关数量”。靶场有几十关,如果你一天刷完,除了收获一堆payload,什么也没留下。但如果你每天只做两三关,每关都尝试不同的payload,记录成功和失败的原因,一个月下来,你对XSS的理解会远超那些刷完所有关卡的人。
还有一个实用技巧:建一个自己的payload库。用笔记软件或者Excel,把每一关的输入点、输出点、过滤规则、成功payload、失败payload都记下来。过一段时间回头看,你会发现很多关卡的核心逻辑是重复的,只是过滤规则变了。这个库会成为你以后实际测试时的速查手册。
提示:靶场里的payload不要直接用在未授权的目标上。练习的目的是理解原理,不是攻击。在实际项目中,一定要在获得明确授权的前提下进行安全测试。
6. 从靶场到实战的思维迁移
靶场练习的最终目的,是让你在实际遇到XSS漏洞时,能快速判断类型、定位输出点、构造有效payload。但靶场和实战有一个关键区别:靶场的过滤规则是“已知”的,你可以反复试错;实战的过滤规则是“未知”的,你只有一次机会(或者很少的机会)。
所以,从靶场到实战,你需要培养的是“一次性判断”的能力。具体来说,就是看到一个输入点,能迅速判断它可能输出到哪里,可能遇到什么过滤,然后直接构造出最有可能成功的payload。这个能力没有捷径,只能通过大量练习和复盘来积累。
我个人的做法是:每做完一关,都问自己三个问题。第一,如果这一关的过滤规则再严格一点,比如把<img>也过滤了,我还能用什么标签?第二,如果输出点从标签之间变成了属性里面,我的payload要怎么改?第三,如果服务器把alert也过滤了,我该怎么弹窗?这三个问题能帮你把一关的知识点扩展成三关,效率翻倍。
另外,实战中还要考虑WAF(Web应用防火墙)的存在。WAF的过滤规则比靶场复杂得多,可能会检测payload的长度、字符频率、甚至请求的上下文。这时候就需要更高级的绕过技巧,比如分块传输、Unicode编码、注释混淆等。这些内容超出了入门靶场的范围,但你可以先把基础打牢,后面再逐步深入。
最后分享一个小技巧:在靶场里,如果某一关你实在过不去,可以试试把payload拆成两半,分别提交。有些服务器的过滤逻辑是“匹配到关键词就删除”,如果你把<script>拆成<scr和ipt>,分两次提交,服务器可能不会触发过滤,但浏览器在渲染时会把它们拼接起来。这个技巧在实战中也很有效,尤其是对付那些基于正则表达式的简单WAF。