1. 项目概述:为什么我们需要一个XSS靶场?
如果你是一名Web安全爱好者,或者正在学习渗透测试,那么“XSS-Labs”这个名字你一定不陌生。它不是一个商业产品,而是一个由安全社区爱好者自发维护的、专门用于练习跨站脚本攻击与防御的在线靶场。我第一次接触它,是因为在真实渗透测试中遇到了一个非常刁钻的过滤规则,常规的payload全部失效,急需一个能系统化训练绕过思维的环境。市面上的综合靶场如DVWA、Pikachu虽然也有XSS模块,但关卡设计往往比较基础或分散,而XSS-Labs则像一本精心编排的“XSS绕过技巧习题集”,从最基础的反射型XSS开始,层层加码,逐步引入各种过滤和防护机制,逼迫你去思考如何变形、如何组合、如何利用上下文环境。
这个靶场的核心价值在于“实战模拟”。它模拟了真实开发中可能出现的各种不安全的代码写法,比如直接在innerHTML中输出用户输入、使用了不安全的JavaScript函数如eval()或document.write()、尝试用黑名单过滤但存在遗漏等等。通过通关这一个个关卡,你不仅能记住一堆payload,更重要的是能建立起一套完整的绕过分析思路:看到过滤规则,能立刻想到几种可能的绕过方向;看到输出点,能快速判断可利用的上下文和闭合方式。这对于应对真实世界的WAF、各种框架的默认过滤机制以及开发人员自定义的防护逻辑至关重要。接下来,我将结合我通关XSS-Labs前十关的实战经验,为你拆解每一关的核心考点、绕过思路以及那些容易被忽略的细节技巧。
2. 环境准备与靶场搭建
2.1 靶场获取与部署
XSS-Labs通常以PHP源码的形式在GitHub等平台传播。部署它非常简单,你只需要一个能运行PHP的Web服务器环境。对于初学者,我强烈推荐使用XAMPP或PHPStudy这类集成环境,它们能一键安装Apache、MySQL、PHP,省去大量配置麻烦。
- 下载源码:从可靠的源(如GitHub上的开源项目)获取XSS-Labs的完整源码包。确保下载的版本是完整的,包含所有关卡文件(通常命名为
level1.php,level2.php等)和一个统一的入口文件index.php。 - 部署到Web目录:将解压后的整个XSS-Labs文件夹,复制到你的Web服务器根目录下。例如,对于XAMPP,就是
htdocs文件夹;对于PHPStudy,就是WWW文件夹。 - 访问靶场:在浏览器中输入
http://localhost/xss-labs/(假设文件夹名为xss-labs)即可看到关卡选择界面。通常界面非常简洁,就是一个数字列表,点击即可进入对应关卡。
注意:请务必在本地或授权的测试环境中搭建和练习。未经授权对任何线上系统进行测试都是非法且不道德的。
2.2 必备工具与浏览器设置
工欲善其事,必先利其器。除了靶场本身,准备好以下工具能让你的测试事半功倍:
- 浏览器与开发者工具:Chrome或Firefox是首选,它们的开发者工具(F12打开)是核心分析武器。重点关注“元素”(Elements/Inspector)标签页查看HTML结构,以及“控制台”(Console)标签页查看JavaScript错误和执行结果。
- 代理抓包工具:Burp Suite社区版或OWASP ZAP。这类工具能拦截、查看和修改浏览器发送的HTTP请求,是手动测试和绕过过滤的关键。你可以通过它们修改URL参数、POST数据、Cookie等,尝试各种payload变体,而无需反复在浏览器地址栏输入。
- 浏览器扩展:一些扩展能辅助测试,如用于快速编码/解码的
Hack-Tools,或者用于修改页面内容的EditThisCookie。但核心依赖仍是开发者工具和代理工具。 - 编码工具意识:不需要单独软件,但要熟知URL编码、HTML实体编码、JavaScript Unicode编码等概念。浏览器的地址栏、控制台都可以直接进行简单的编码解码测试。
一个关键的浏览器设置:为了更清晰地看到payload的执行结果,建议在测试时暂时关闭Chrome的XSS Auditor(如果版本较老)或相关内置过滤器。新版本Chrome中,确保不要开启过于严格的内容安全策略(CSP)测试模式,以免本地靶场的弹窗被浏览器本身拦截,干扰你对漏洞是否存在的判断。不过,XSS-Labs的设计通常能兼容现代浏览器的基本安全机制。
3. 前十关核心技巧与绕过思路逐关精析
下面,我将以最常见的XSS-Labs关卡设计为例,逐一解析前十关的典型场景和突破方法。请注意,不同版本的靶场具体代码可能略有差异,但核心思路是相通的。
3.1 第一关:无过滤的反射型XSS
场景:一个简单的搜索框,输入内容后直接回显在页面上。源码关键点猜测:<?php echo $_GET[‘keyword’]; ?>或类似代码,直接将GET参数输出到HTML中。绕过思路:无需绕过,构造基本payload即可。Payload示例:<script>alert(‘xss’)</script>实操与解析: 在搜索框输入上述payload,提交。查看页面源代码,你会发现你的输入被原封不动地插入到了某个HTML标签之间,例如在一个<h2>标签内。这构成了一个最经典的反射型XSS。成功弹窗。技巧心得:
- 第一件事永远是看源码:提交payload后,立即右键查看页面源代码(或使用F12开发者工具),确认你的输入被放置在页面的哪个位置,是否被HTML标签包裹。这决定了你后续的payload构造方式。
- 使用非侵入性验证:在真实不确定的环境,可以先使用
<img src=1 onerror=console.log(1)>这类payload。alert会干扰测试,而console.log只在控制台输出,更为隐蔽友好。
3.2 第二关:搜索框内的属性值转义
场景:输入内容被放入一个HTML标签的属性值里,比如<input value=”你的输入”>。源码关键点猜测:<input type=”text” value=”<?php echo $_GET[‘keyword’]; ?>”>。绕过思路:需要先闭合双引号,然后引入事件处理器或新的标签。Payload示例:”><script>alert(‘xss’)</script>或” onmouseover=”alert(‘xss’)实操与解析: 输入”><script>alert(‘xss’)</script>。首先,开头的双引号”会闭合value属性的前一个双引号。紧接着的>会闭合<input>标签。然后我们成功在页面中插入了新的<script>标签。查看源码,会看到类似<input type=”text” value=””><script>alert(‘xss’)</script>”>的结构。技巧心得:
- 属性值上下文:当输出点在属性值内时,
<script>标签可能无法直接执行,因为浏览器不会解析属性值内的标签。优先考虑使用事件处理器属性,如onclick,onmouseover,onerror等。 - 闭合符号选择:观察页面源码,确定属性是用单引号还是双引号包裹,使用对应的符号进行闭合。有时甚至没有引号,此时直接用空格和事件处理器即可。
3.3 第三关:单引号属性与初步过滤
场景:输出点位于单引号包裹的属性值内,并且服务端可能对双引号或<、>做了简单处理。源码关键点猜测:<input value=’<?php echo htmlspecialchars($_GET[‘keyword’], ENT_QUOTES); ?>’>, 但可能只过滤了双引号。绕过思路:尝试闭合单引号,并使用事件处理器。因为<和>可能被转义,插入新标签困难。Payload示例:’ onmouseover=’alert(/xss/)实操与解析: 输入’ onmouseover=’alert(/xss/)。开头的单引号闭合了value属性,然后添加了onmouseover事件处理器。注意,这里事件处理器的值我用/xss/代替了字符串,这是一种简写方式。最终构造出<input value=” onmouseover=’alert(/xss/)’>。当鼠标滑过输入框时触发弹窗。技巧心得:
- 利用JavaScript字面量:在事件处理器中,
alert(1)、alert(‘1’)、alert(/1/)通常是等价的。当引号被过滤时,使用正则表达式字面量/.../是一个常用的绕过技巧。 - 大小写混淆:如果靶场对
onmouseover这类关键词进行黑名单匹配,尝试OnMoUsEoVeR或onmouseover(末尾加空格)有时能绕过简单的基于字符串匹配的过滤。
3.4 第四关:双引号属性与标签过滤
场景:输出点回到双引号属性,但服务端可能过滤或删除了<和>。源码关键点猜测:使用str_replace函数将<和>替换为空。绕过思路:既然无法插入新标签(<script>),那就专注于利用现有标签的属性。闭合双引号,添加事件处理器。Payload示例:” onmouseover=”alert(‘xss’)实操与解析: 这个payload和第三关类似,只是引号不同。输入后,构造出<input value=”” onmouseover=”alert(‘xss’)”>。这里的关键是理解,过滤了<>只是阻止了你创建新标签,但并没有阻止你在现有标签上添加新的属性。技巧心得:
- 黑名单过滤的局限性:只过滤
<>是极其不安全的。攻击者完全可以在不引入新标签的情况下完成攻击。这提醒我们,安全的输出必须结合上下文进行编码或白名单过滤。 - 多种事件测试:除了
onmouseover,还可以测试onfocus、onblur、onload(针对<img>等标签)等。不同场景下可触发的事件不同。
3.5 第五关:对script和on关键词的过滤
场景:服务端开始过滤script、on等明显的关键词。源码关键点猜测:$keyword = str_replace(‘script’, ‘’, $_GET[‘keyword’]);以及$keyword = str_replace(‘on’, ‘’, $keyword);, 可能不区分大小写。绕过思路:使用非script标签和非on事件。HTML5提供了很多可以执行JavaScript的标签和属性。Payload示例:”><a href=”javascript:alert(‘xss’)”>click</a>或”><svg/onload=alert(‘xss’)>实操与解析:
- 使用
<a>标签的href属性:javascript:伪协议可以执行代码。但注意,这需要用户点击链接。不过,在证明漏洞存在时是有效的。 - 使用
<svg>标签:SVG标签内嵌在HTML中,其onload事件可以在标签加载时自动触发,且onload中的on可能因为字符串替换变成load,但<svg onload=作为一个整体,如果过滤是简单的str_replace(‘on’, ”), 会得到<svg load=, 这无法执行。所以需要看过滤的具体实现。更可靠的可能是使用<img src=1 onerror=alert(1)>,但onerror里的on也会被过滤。这时可以尝试双写绕过:<img src=1 oonnerror=alert(1)>,如果过滤函数只执行一次替换,会得到onerror。技巧心得: - 双写绕过:这是对抗简单字符串替换的经典方法。如果过滤
script, 就写<scrscriptipt>, 过滤后变为<script>。 - 探索冷门标签和属性:除了
<script>和on事件,还有<iframe>、<embed>、<object>的src属性(支持javascript:)、<details>的ontoggle事件、<body>的onpageshow事件等。积累这些向量很重要。
3.6 第六关:大小写绕过与黑名单扩展
场景:黑名单关键词增加,但过滤可能对大小写敏感。源码关键点猜测:黑名单包含script,on,src,data等,但使用如str_ireplace(不区分大小写)或str_replace(区分大小写)。绕过思路:如果过滤区分大小写,尝试大小写混合。如果黑名单有遗漏,尝试未过滤的标签或事件。Payload示例:”><ScRiPt>alert(‘xss’)</sCrIpT>或”><img sRc=1 onErRor=alert(1)>实操与解析: 输入”><ScRiPt>alert(‘xss’)</sCrIpT>。如果后端使用str_replace(‘script’, ”), 它不会匹配ScRiPt,因此payload得以保留并执行。查看源码确认标签被正确插入。技巧心得:
- 测试过滤逻辑:通过输入
<script>、<SCRIPT>、<ScRiPt>,观察页面源码中它们的变化,可以推断后端是直接删除、转义还是替换,以及是否区分大小写。这是信息收集的关键一步。 - 使用编码试探:有时可以尝试HTML实体编码的一部分,如
<script>,看后端是否会解码。但通常简单的黑名单过滤不会做解码处理。
3.7 第七关:关键字删除与双写绕过
场景:黑名单继续扩大,并且对关键词执行删除操作。源码关键点猜测:$keyword = str_replace(array(‘script’, ‘on’, ‘src’, ‘data’, ‘href’), ”, $_GET[‘keyword’]);。绕过思路:双写绕过。因为删除是直接替换为空,所以双写后,删除中间的关键词,两边的字符会拼接成新的关键词。Payload示例:”><scrscriptipt>alert(‘xss’)</scrscriptipt>或” oonnmouseover=”alert(‘xss’)实操与解析: 以scrscriptipt为例。后端查找script并删除,那么从scrscriptipt中删除中间的script后,剩下的部分拼接起来就是script。最终页面中呈现的就是<script>标签。技巧心得:
- 精确双写:确保你双写的部分正好是过滤词。如果过滤
on和script,那么oonn和scrscriptipt都要用上。 - 组合测试:一个payload里可能同时需要绕过多个关键词过滤,例如
<img srsrcc=1 oonnerror=alert(1)>。
3.8 第八关:HTML实体编码与javascript:协议过滤
场景:输入内容被输出到<a>标签的href属性中,并且服务端可能对javascript:进行了过滤。源码关键点猜测:<a href=”<?php echo htmlspecialchars($_GET[‘link’]); ?>”>Click</a>, 并且可能额外检查href值是否以javascript:开头。绕过思路:
- 利用HTML实体编码绕过
htmlspecialchars:htmlspecialchars默认只转义&,”,’,<,>。对于已经位于属性值内的内容,如果属性值本身用双引号包裹,那么双引号被转义成", 我们无法闭合。但我们可以尝试注入&和#来构造HTML实体,浏览器在解析href属性时会对其进行解码。 - 绕过
javascript:过滤:使用大小写、插入空白符、使用java script:(Tab)、java\nscript:等。Payload示例:javascript:alert(‘xss’)-> 尝试JavaSCript:alert(‘xss’)或javascript:alert(1)。实操与解析: 首先输入简单的javascript:alert(1),查看源码发现被转义或过滤。尝试JavaSCript:alert(1)。如果不行,尝试利用HTML实体:javascript:alert(1)。这里t是字符t的十六进制HTML实体。当浏览器解析href属性时,会将其解码为t,从而组合成javascript:。技巧心得:
- 属性值上下文编码:在HTML属性值中,浏览器会进行HTML实体解码。这为我们绕过对特定字符串的过滤提供了可能。可以构造
&#xXX;(十六进制)或&#XXX;(十进制)形式的实体来替换关键词中的字符。 - 协议伪指令:
javascript:协议非常强大,但也容易被WAF重点关照。除了它,还可以在允许的情况下测试data:协议,它可以承载HTML或JavaScript代码,但通常也被严格过滤。
3.9 第九关:链接合法性校验与绕过
场景:在第八关基础上,服务端增加了校验,要求href的值必须包含http://,否则不显示链接或做其他处理。源码关键点猜测:检查$_GET[‘link’]中是否包含子串http://。绕过思路:在javascript:协议伪指令前或后拼接一个合法的http://链接,利用javascript:协议执行代码的特性,或者尝试注释掉后面的部分。Payload示例:javascript:alert(‘xss’);//http://example.com或http://example.com#javascript:alert(‘xss’)实操与解析:
- 方法一(注释法):
javascript:alert(‘xss’);//http://example.com。//在JavaScript中是单行注释,后面的http://example.com会被注释掉,不会影响前面代码的执行。同时整个字符串包含了http://, 可能通过校验。 - 方法二(锚点法):
http://example.com#javascript:alert(‘xss’)。#后面是URL的片段标识符(hash),通常不会发送到服务器。如果校验只是简单的字符串包含http://,那么这个payload可以通过。当用户点击时,浏览器会跳转到http://example.com,但片段标识符中的javascript:协议可能在某些浏览器的特定上下文中不被执行,成功率较低。注释法更可靠。技巧心得: - 理解校验的松紧:校验是“必须包含”还是“必须以…开头”?如果是“必须包含”,注释法很好用。如果是“必须以…开头”,可能需要像
http://javascript:...这样的payload,但这通常无效,因为http://后面应该跟主机名。 - 利用JavaScript语法灵活性:分号
;、注释//或/* */、字符串拼接等都可以用来构造既能通过校验又能执行的payload。
3.10 第十关:隐藏参数与事件触发
场景:页面中存在隐藏的表单字段(<input type=”hidden”>),其值由URL参数控制,并且这个值最终会以某种方式影响页面,可能被输出到JavaScript变量或某个属性中。源码关键点猜测:URL中存在一个参数如?keyword=test, 这个值被赋给一个隐藏输入框的value:<input type=”hidden” name=”hiddendata” value=”<?php echo $_GET[‘keyword’]; ?>”>。然后页面的某个JavaScript会读取这个value并进行处理。绕过思路:
- 寻找输出点:首先通过查看源码,找到隐藏输入框。确认其
name和id。 - 追踪数据流:在页面的JavaScript代码中搜索这个
name或id,看它的值被用在了哪里。可能被直接eval(), 或者拼接进innerHTML, 或者作为document.write的参数。 - 构造闭合:根据数据被使用的上下文(JavaScript字符串、HTML字符串等),构造合适的payload进行闭合和注入。Payload示例:假设JS代码是:
var data = document.getElementById(‘hiddendata’).value; document.write(data);那么我们可以注入:</script><script>alert(‘xss’)</script>。但这样会破坏原有JS结构。更常见的是,如果数据被用在innerHTML或类似场景,可以直接注入HTML/JS标签。 如果JS代码是:eval(‘var x = “‘ + document.getElementById(‘hiddendata’).value + ‘”;’);那么我们需要闭合字符串并注入代码:”; alert(‘xss’);//。这样拼接后变成eval(‘var x = “”; alert(‘xss’);//”;’);, 成功执行alert。实操与解析: 这一关没有固定的payload,完全取决于后端代码如何“隐藏”地使用这个参数。你需要像一个侦探一样: - 提交一个简单值如
test123。 - F12打开开发者工具,在“元素”面板找到隐藏的input,确认其值已被设置。
- 在“源代码”(Sources)面板或“元素”面板的
<script>标签中,全局搜索test123这个字符串,找到它是如何被JavaScript代码引用的。 - 分析引用处的代码上下文,是字符串拼接、直接输出还是函数参数?
- 根据上下文构造payload,并在浏览器控制台先模拟测试拼接后的代码是否语法正确、能否执行。技巧心得:
- 学会代码审计:这一关开始涉及简单的客户端代码审计能力。这是真实世界XSS挖掘中至关重要的一环,很多XSS漏洞源于不安全的JavaScript动态操作DOM。
- 控制台是你的沙盒:在不确定payload是否有效时,先在浏览器控制台模拟执行拼接后的完整JavaScript代码,确认无语法错误且能触发预期效果,再提交到服务器。
- 注意字符转义:如果数据被放在JavaScript字符串内(被单/双引号包裹),你需要闭合引号,并确保注入的代码不会因为包含特殊字符(如未转义的单引号)而破坏语法。使用
\进行转义,或者用String.fromCharCode等方式构造字符串。
4. 通用绕过思路与高级技巧汇编
通过前十关,我们已经见识了从简单到复杂的各种过滤。下面我系统性地总结一下XSS绕过的核心思路和高级技巧,这些在后续关卡和真实环境中都极其有用。
4.1 基于过滤逻辑的绕过
这是最直接的对抗方式,针对后端采取的防护措施进行突破。
字符串匹配与删除:
- 大小写绕过:
<ScRiPt>,OnClIcK。 - 双写绕过:
<scrscriptipt>,oonn。 - 插入干扰字符:在关键词中插入浏览器可忽略而过滤器可能不可忽略的字符。例如,在HTML标签名或属性名中插入
/、\0(空字符)、换行符、Tab符等。<img/src=1/onerror=alert(1)>(利用/代替空格)。<svg onload=alert(1)>(在onload前插入一个空字符,需URL编码为%0c)。 - 嵌套标签:某些过滤器可能只检查一层标签。
<scr<scriipt>pt>alert(1)</scr</scriipt>pt>, 如果过滤器递归删除不彻底,可能留下<script>。
- 大小写绕过:
正则表达式缺陷:
- 不匹配多行:如果正则表达式没有使用
/m标志或.不匹配换行符,可以利用换行符分割payload。 - 贪婪/非贪婪匹配:了解正则的匹配模式,可能构造字符串使其匹配失败或匹配到非预期部分。
- 回溯绕过:构造超长或特定模式的字符串,可能使正则引擎陷入灾难性回溯,导致过滤失效(在WAF场景更常见)。
- 不匹配多行:如果正则表达式没有使用
4.2 基于HTML/JS解析差异的绕过
浏览器解析HTML和JavaScript的方式有时与后端过滤器的理解不同,利用这些差异是高级绕过的关键。
- HTML实体编码:如前所述,在HTML文本或属性值中,
&开头的实体会被浏览器解码。可以构造<img src=1 onerror=alert(1)>(o是o)。如果过滤器在解码前检查,就可能被绕过。 - JavaScript编码:
- Unicode转义:在JavaScript字符串中,
\u0061表示字符a。可以构造\u0061lert(1)。 - 十六进制/八进制转义:
\x61lert(1)(十六进制),\141lert(1)(八进制)。 - 利用
eval()和String.fromCharCode:如果输出点在一个eval的参数里,可以注入String.fromCharCode(97, 108, 101, 114, 116, 40, 49, 41)来动态生成alert(1)的代码。
- Unicode转义:在JavaScript字符串中,
- 标签属性与语法宽松性:
- 属性值可不加引号:
<img src=1 onerror=alert(1)>是有效的。如果过滤器只检查引号内的内容,这可能绕过。 - 属性分隔可用多种字符:除了空格,Tab、换行、
/(在标签名后)等都可以分隔属性。 - 自闭和标签:
<img src=1 onerror=alert(1) />, 末尾的/是可选的,但有时能绕过对>的过滤检查。
- 属性值可不加引号:
- 利用SVG/MathML等XML命名空间:SVG标签内的某些事件处理器或脚本执行方式可能与HTML略有不同,有时能绕过针对HTML的过滤规则。例如
<svg><script>alert(1)</script></svg>或<svg onload=alert(1)>。
4.3 基于上下文切换的绕过
有时直接注入失败,但可以尝试将执行上下文切换到另一个更有利的解析器。
- 从属性切换到HTML内容:通过闭合当前属性甚至当前标签,将注入点“提升”到HTML正文层面,然后插入新的标签。这是第二关到第五关的主要思路。
- 从JavaScript字符串切换到代码:通过闭合字符串引号,并加上分号或注释,结束当前语句,开始新的JavaScript语句。这是第十关和很多DOM型XSS的利用方式。例如:
’; alert(1);//或</script><script>alert(1)</script>。 - 利用
<textarea>或<title>等标签的解析规则:这些标签内的内容通常被当作纯文本,直到遇到闭合标签。如果页面中已存在这些标签且未闭合,注入闭合标签可能逃逸出来。
4.4 利用浏览器特性与怪异模式
浏览器为了兼容旧网页,存在一些特殊的解析行为。
- 字符集问题:如果页面指定了错误的字符集或未指定,某些特殊字节序列可能被浏览器以不同方式解析,导致过滤器识别失败。
<script>标签的charset属性:理论上可以指定非标准字符集,但实用价值不高。- 过时或不推荐的技术:如
<img dynsrc=lowsrc=1 onerror=alert(1)>(利用旧属性),了解即可,现代浏览器支持度差。
5. 防御视角:从攻击中学习如何编写安全代码
作为一名开发者,从这些绕过技巧中,我们应该汲取教训,知道如何避免写出存在漏洞的代码。
- 原则:绝不信任用户输入。这是黄金法则。所有来自客户端的数据(URL参数、POST表单、Cookie、HTTP头)都必须视为不可信的。
- 根据输出上下文进行编码:
- 输出到HTML正文:使用HTML实体编码。例如PHP的
htmlspecialchars($str, ENT_QUOTES | ENT_HTML5, ‘UTF-8’)。ENT_QUOTES会编码单双引号,非常重要。 - 输出到HTML属性值:同样使用HTML实体编码,并确保属性值总是用引号(单或双)包裹。
- 输出到JavaScript代码或变量:使用JavaScript编码。不要简单用
addslashes, 应使用专门的函数,如json_encode()(用于生成JSON)或确保数据被放在引号内并进行正确的转义。更好的做法是避免将用户输入直接拼接到<script>标签内,而是通过>
- 输出到HTML正文:使用HTML实体编码。例如PHP的