news 2026/9/11 2:46:52

Xss-Labs靶场1-10关实战:从反射型XSS到过滤绕过的完整思路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Xss-Labs靶场1-10关实战:从反射型XSS到过滤绕过的完整思路

刚刷完Xss-Labs前10关的时候,我最大的感受是:原来XSS并没有想象中那么玄乎,但也绝对没有某些速成教程说的那么简单。它更像是一场“猜谜游戏”——服务端到底过滤了什么、输出位置在哪、用什么办法能把我们的代码送到浏览器解析器嘴里,每一个环节都在考验对HTML解析机制的理解。

这篇就基于我自己刷Xss-Labs靶场1到10关的完整过程,把每一关的源码逻辑、绕过思路、最终payload和踩坑经验都拆开讲一遍。如果你正在学Web安全,尤其是想系统入门XSS攻击,这套靶场是非常好的练手材料。前10关难度是循序渐进设计的,从“完全不过滤”到“各种花式过滤”,刷完基本能建立一套自己的绕过思维框架。

提示:本教程仅供安全学习与合法的授权测试使用,未授权环境下请勿直接应用文中payload。

1. 准备工作与实验环境

1.1 靶场部署方案

Xss-Labs是一套基于PHP编写的本地靶场,每一关对应一个独立的PHP文件,核心逻辑都藏在源码的过滤函数里。部署方式很简单,最常见的就是用phpstudy或者小皮面板跑一个Apache/Nginx环境,把靶场文件夹丢进站点根目录就能开刷。

我这里用的是phpstudy 8.1版本,PHP版本选的7.4,靶场文件放在C:\phpstudy_pro\WWW\xsslabs目录下,浏览器访问http://127.0.0.1/xsslabs/就能看到关卡列表。如果你用的是其他集成环境,注意一下站点根目录和PHP版本兼容性就行。

部署完成之后还有一个建议:把PHP错误提示打开,方便调试。不同环境下源码细节可能有细微差别,出问题时直接查看.php文件的原始代码,比对着网上的教程猜要靠谱得多。

1.2 浏览器与工具准备

刷Xss-Labs最顺手的浏览器是Firefox或Chrome,推荐装一个HackBar插件,方便快速构造和发送payload。为什么不用Burp Suite全程代替?因为前10关很多payload需要不断修改微调,HackBar在浏览器里直接改URL参数,反馈速度比来回开代理抓包快得多。

另外一个非常重要的工具是浏览器的“检查元素”功能。每一关的payload发送之后,右键查看页面源代码,能直接看到服务端把我们的输入原样输出到了什么位置,这是一个非常关键的判断依据——输出在标签内、属性内,还是URL内,直接决定了用什么类型的payload。

我还习惯在浏览器里装一个SwitchyOmega,虽然前10关用不上代理,但这个习惯在后面刷更复杂的靶场时会省不少事。工具够用就行,没必要一开始就弄得花里胡哨。

2. 第1关到第5关:反射型XSS的入门阶梯

2.1 第1关:毫无过滤的Hello World

第1关的核心源码逻辑大概是这样的:

$str = $_GET["name"]; echo "<h2 align=center>欢迎用户".$str."</h2>";

代码非常简单,$_GET["name"]拿到的参数直接被拼进HTML的<h2>标签中间,没有任何过滤或转义。这一关就是XSS的“Hello World”,目的是让新手理解XSS最基本的发生条件:用户输入未经过任何处理,直接进入页面输出。

直接构造URL:

http://127.0.0.1/xsslabs/level1.php?name=<script>alert(1)</script>

如果浏览器拦截了,刷新一下或者换Firefox再试,payload就能弹窗。这里的alert(1)只是一个验证手段,证明我们的脚本被浏览器执行了;在实际攻击场景中,这里可能会换成窃取Cookie、伪造登录表单、劫持会话的恶意脚本。

第1关给我们的最大启示不是“哦原来可以弹窗”,而是建立反射型XSS的最小模型:用户输入 → 服务端取值 → 拼接到响应 → 浏览器解析执行。任何一步有防御,攻击就不能成立。

2.2 第2关:双引号属性闭合与事件注入

第2关代码变成了这样:

$str = $_GET["keyword"]; echo '<input type="text" class="form-control" id="name" value="'.$str.'">';

在第1关直接注入<script>标签的做法,在这里行不通了,因为输出位置变了——我们的输入被放在value="..."属性值里面。如果直接传<script>alert(1)</script>,浏览器只会把它当成input标签的value属性值的一部分,脚本不会执行。

这时候需要先闭合前面的双引号,让我们的输入逃逸出属性值,再构造新的标签或事件。先试最简单的闭合方式:

"><script>alert(1)</script>

payload里的"闭合了value属性的左引号,>闭合了input标签,后面的<script>alert(1)</script>就成了独立的脚本标签,可以正常执行。

还有一种更优雅的解法是用事件属性,不需要闭合标签:

" onfocus="alert(1)

把payload填进value属性后,input标签变成:

<input type="text" id="name" value="" onfocus="alert(1)">

只要input框获得焦点就会触发弹窗。实际测试中点一下输入框即可成功。第二种方法更贴近真实场景,因为很多情况下标签括号会被过滤,事件注入是比插入新标签更稳妥的绕过思路。

2.3 第3关:单引号闭合下的属性注入

第3关的代码有一个明显变化:

$str = $_GET["keyword"]; echo "<input type='text' id='name' value='".$str."'>";

value属性周围的引号从双引号变成了单引号。如果还用第2关的">去闭合,会发现闭合不掉,因为这里的属性是用单引号包裹的,"在HTML属性里就是普通字符,不起闭合作用。

所以闭合字符要跟着输出位置变:

' onfocus='alert(1)

这样拼出来的HTML是:

<input type='text' id='name' value='' onfocus='alert(1)'>

同样,点击input框触发弹窗。

这一关最需要留意的思维转换是:不要死记payload,要观察输出位置的引号类型和标签结构。双引号属性就用双引号闭合,单引号属性就用单引号闭合,后面很多关卡的花式过滤都建立在这个基础上。

还有一种常见的第3关解法是直接闭合后插入标签:

'><script>alert(1)</script>

两种都能过,但理解原理比记住哪一个能用更重要。

2.4 第4关:尖括号被过滤怎么办

第4关明显开始上强度了。源码对输入做了处理:

$str = $_GET["keyword"]; $str2 = str_replace(">", "", $str); $str3 = str_replace("<", "", $str2); echo "<input type='text' id='name' value='".$str3."'>";

str_replace函数依次把><替换成空字符串,意味着<script>这种需要尖括号的标签型payload彻底失效。输入<script>进去,出来就变成script,标签结构被破坏。

这时候“事件注入”的优势就体现出来了。事件属性payload根本不需要尖括号:

' onfocus='alert(1)

为什么会成功?因为过滤规则只处理了尖括号,没有处理单引号和onfocus事件,而HTML标签属性本身就是可以执行JavaScript的。当输入框获得焦点时,onfocus事件触发,alert执行。

这一关的深层启示是:过滤了某些字符不等于过滤了执行途径。XSS的载体不只是<script>,标签事件、伪协议、CSS表达式、SVG标签这些都是潜在的注入点。防御方如果只做简单的字符黑名单,永远是堵不完的。

2.5 第5关:过滤script标签与双写绕过

第5关的过滤逻辑又升级了:

$str = strtolower($_GET["keyword"]); $str2 = str_replace("<script>", "", $str); $str3 = str_replace("</script>", "", $str2); echo "<input type='text' id='name' value='".$str3."'>";

这关先做了strtolower把所有字母转小写,然后过滤了<script></script>。这意味着大写绕过和原始script标签都行不通了。

第一种思路是改用不含script的标签,比如img加onerror事件:

"><img src=x onerror=alert(1)>

img标签加载图片失败时会触发onerror事件,同样能执行JavaScript。但要注意,这关源码在过滤前已经转小写了,onerror事件本身没有被过滤,所以能过。

第二种思路是双写绕过:

"><scr<script>ipt>alert(1)</script>

为什么双写能绕过?关键在于str_replace是单次替换,从左到右扫描时匹配到<script>后替换为空,但剩下的部分重新拼接后,又组成了一个完整的<script>标签。比如<scr<script>ipt>,中间的<script>被删掉后,剩下的是<script>

这一关建议两种payload都亲手试一遍,感受一下“标签型”和“事件型”两种XSS在实战中是如何根据过滤规则进行取舍的。

3. 第6关到第10关:绕过过滤的进阶套路

3.1 第6关:大小写绕过

第6关代码:

$str = $_GET["keyword"]; $str2 = str_replace("<script>", "", $str); $str3 = str_replace("</script>", "", $str2); echo "<input type='text' id='name' value='".$str3."'>";

注意对比第5关,第6关没有strtolower。也就是说,过滤规则只匹配小写的<script></script>,于是直接大小写混合就能绕过:

"><ScRiPt>alert(1)</ScRiPt>

HTML标签名本身不区分大小写,浏览器照样能把<ScRiPt>解析成script标签。这一关和上一关的对比非常经典:同样的过滤规则,多一个strtolower就让大小写绕过失效,少一个就形同虚设。

这给我们划了一个重点:在分析过滤逻辑时,先看有没有做大小写统一,再做对应决策。很多防御方的思路是“黑名单 + 大小写归一化”,但黑名单本身永远有遗漏。

如果这关也想用事件注入,那更简单:

" onfocus="alert(1)

事件注入根本不触碰<script>关键词,只要贴合当前环境的属性结构就能过。越到后面的关卡越会发现,事件注入是XSS绕过中非常通用的手段。

3.2 第7关:多关键词过滤与双写、编码混合

第7关的过滤规则明显复杂了:

$str = strtolower($_GET["keyword"]); $str2 = str_replace("</script>", "", $str); $str3 = str_replace("<script>", "", $str2); $str4 = str_replace("javascript:", "", $str3); $str5 = str_replace("onerror", "", $str4); echo $str5;

这关同时过滤了<script></script>javascript:onerror,而且有strtolower,大小写绕过直接失效。过滤顺序是从左到右依次执行的。

先看有onerror事件怎么办。过滤规则是单次替换,那我们就用双写:

"><img src=x onnerror=alert(1)>

等等,这里有个细节:如果过滤的是onerror,双写应该写成ononerrorerror还是onnerror?原字符串是onerror,过滤后变成空,所以想在过滤之后剩下onerror,需要在过滤前构造onnrror吗?不对。

正确的双写思路是:把onerror写成onenerror。当str_replace把其中匹配到的onerror去掉后,剩下的是onerror

onenerror → 去掉第一个 onerror(on + error的部分) → 剩下 onerror

我试过的经验是,第7关最稳的payload有两种:

"><img src=x onnerror=alert(1)>

这个思路是:原字符串onnerror包含子串nerror,其实不匹配。真正匹配的是onerror,如果要过滤后完整剩下原始关键词,更常见的是写成ononerrorerror这种双写形式。但实际操作中需要根据过滤函数的行为测试。

另一种完全避开onerrorscript的思路是用伪协议,但javascript:也被过滤了。用java&#x73;cript:这种实体编码绕过?理论上也可以,但这一关的上下文是value属性,实体编码在属性解析时会被解码。不过第7关我个人实测最简单的方式还是双写插入:

"><scr<script>ipt>alert(1)</script>

因为<script>过滤是单次的,<scr<script>ipt>经过替换后重新组成<script>。这一关刷下来的体会是:当多个关键词都被过滤时,先逐一分析哪些过滤规则可以双写绕过,哪些要用编码绕过,不要指望一个payload通吃所有过滤。

3.3 第8关:JavaScript伪协议与实体编码绕过

第8关的输出位置完全变了:

$str = strtolower($_GET["keyword"]); $str2 = str_replace("javascript:", "", $str); $str3 = str_replace(";", "", $str2); echo '<a href="'.$str3.'">链接</a>';

输入被放到<a>标签的href属性里,并且javascript:和分号;都被过滤了。直接输入javascript:alert(1)会被替换成alert(1),href变成无效链接。

这一关的关键在于理解HTML实体解码的时机。过滤发生在服务端字符串处理阶段,而实体解码发生在浏览器解析HTML阶段。服务端看到的java&#x73;cript:里面没有javascript:这个连续字符串,所以过滤规则直接放行;浏览器解析href属性时,把&#x73;解码成s,得到的是javascript:alert(1),执行伪协议。

最终payload:

java&#x73;cript:alert(1)

注意这里分号;被过滤了,但&#x73;必须要用分号结尾才能被正确解码。实测发现这关的过滤规则没有把实体编码的&#x这些字符纳入黑名单,所以能正常过。另外alert后面的分号会被过滤,但不影响JavaScript执行,因为alert函数调用语句后面不加分号也不会报错。

第8关给我们的经验非常实用:过滤黑名单只对原始字符串生效,一旦浏览器存在解码过程(HTML实体、URL编码、Unicode),就可能出现解码后能执行但解码前不匹配的间隙。这类“双重解析”漏洞在真实Web应用中也经常出现。

3.4 第9关:强制包含http时的注释绕过

第9关代码:

$str = strtolower($_GET["keyword"]); $str2 = str_replace("javascript:", "", $str); $str3 = str_replace(";", "", $str2); if (strpos($str3, 'http://') === false) { echo "我真不明白,你为什么就是不能好好的提交!"; } else { echo '<a href="'.$str3.'">链接</a>'; }

跟前一关类似,但多了一个硬性条件:payload里必须包含字符串http://,否则直接拒绝输出。这意味着我们不能只写javascript:alert(1),还得想办法让http://混进payload里且不影响JavaScript执行。

常见解法是利用JavaScript的注释符:

javascript:alert(1)//http://

//在JavaScript里是单行注释,所以//http://这部分不会被解释执行,但它满足了strpos检测包含http://的条件。整个payload在输出后是:

<a href="javascript:alert(1)//http://">链接</a>

浏览器解析href时,javascript:伪协议成立,alert执行,后面的//http://只是注释。

还有一种写法:

javascript:alert(1)/*http://*/

用块注释包住http://,同样满足条件且不影响执行。实际测试单行注释写法足够通过。

这一关的技巧核心是:用注释符把“检测用的关键词”和“执行用的代码”隔离。检测逻辑看到的是整个字符串包含http://,JavaScript引擎执行时看到的只是注释后面被忽略的多余内容。

3.5 第10关:POST型XSS与抓包实战

前面9关全部是GET型,直接改URL参数就能测。第10关开始变了:

$str = $_POST["keyword"]; echo "<input type='text' id='name' value='".$str."'>";

参数从$_GET变成了$_POST,直接改URL不行了,因为URL里的参数进的是$_GET数组,$_POST里根本拿不到。

第一个能想到的办法是用Burp Suite抓包改请求。正常提交表单时拦截请求,把GET改成POST,把参数从URL挪到请求体里。具体操作:浏览器先正常访问level10.php,Burp拦截到GET请求后,右键Change Request Method改成POST,然后在请求体里加keyword=%27%20onfocus%3D%27alert(1)

第二个更轻量的办法是在浏览器控制台里用fetch发POST请求:

fetch('http://127.0.0.1/xsslabs/level10.php', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: "keyword=' onfocus='alert(1)" })

页面刷新后,input框里已经注入了onfocus事件,点击输入框就能弹窗。

这一关在真实场景中对应的是“存储型XSS和反射型XSS的输入点差异”。很多开发者防护了GET参数却忽略POST参数,或者反过来,所以测试时一定要覆盖所有请求方法。第10关的PHPSESSID等会话机制也提示我们,POST型XSS更容易配合CSRF形成完整的攻击链。

4. 刷靶场过程中的常见问题与排查方法

4.1 弹不出框先别急:三步定位问题

刷靶场最让人抓狂的就是payload发出去,页面安安静静,一个弹窗都没有。多数情况下不是靶场坏了,而是payload和输出位置不匹配。我总结了一个三步排查法,每一步都在浏览器里验证。

第一步,先看输出位置。右键查看页面源代码,搜索自己输入的某个特征字符串(比如xsstest),看它被渲染到了哪里——是在HTML标签内部、标签属性内部、JavaScript代码里,还是被HTML编码成了&lt;script&gt;?输出位置决定了闭合方式,这一步错了后面全错。

第二步,看过滤规则对payload做了什么。把原始输入和源代码里显示的输出对比一下,尖括号还在不在?关键词被删了还是被转义了?如果<变成了&lt;,那是做了HTML实体编码,事件注入也救不了;如果只是把script删了,那考虑双写或换事件。

第三步,确认事件能触发。用了onfocus要点击输入框,用了onerror要让图片加载失败,用了onmouseover要鼠标移上去。Payload本身没问题,但触发条件没满足,一样弹不出框。这一步最容易被新手忽略。

4.2 工具与环境的几个坑

刷题过程中环境问题也踩了不少。第一个坑是PHP版本差异。老版本靶场在新版PHP下可能出现警告或行为不一致,最典型的例子是str_replace在传入数组时的表现差异、$_GET对同名参数的取值规则等。建议就用PHP 5.6到7.4之间,太新的版本有时会有兼容性问题。

第二个坑是浏览器的XSS过滤机制。Chrome对反射型XSS有内置拦截,某些payload会被浏览器直接拦截不执行。遇到这种情况不要怀疑靶场出错了,换成Firefox或者关闭对应安全特性再试。实际工作中各浏览器对XSS的防护策略确实不同,这也是为什么渗透测试要随身备几款浏览器。

第三个坑是URL编码问题。#&+'这些字符直接放进URL会被浏览器编码或截断。HackBar里的URL Encode功能就是干这个的。记住一个原则:payload里的&在URL中要写成%26,空格要写成%20,不然参数会被切碎。

4.3 关键经验:从通关到迁移

一个非常值得做的练习是:通关之后,把payload放到前几关里试试。比如第7关的双写payload放到第4关,会是什么结果?第8关的实体编码放到第7关,能过吗?这些交叉测试能帮你快速建立对不同过滤规则组合的敏感度。

另一个更进阶的练习是把每道题的payload迁移到DVWA或pikachu靶场的对应难度上。不同靶场对同一漏洞的过滤策略不一样,但绕过思路是相通的。我的经验是:真正把一个漏洞吃透的标志不是能过这个靶场,而是换一个环境还能迅速找出注入点并构造出可用的payload。

关卡输出位置核心过滤点最稳的绕过思路
Level 1HTML标签内无过滤直接script标签
Level 2双引号属性内无过滤双引号闭合 + script标签/事件
Level 3单引号属性内无过滤单引号闭合 + 事件注入
Level 4单引号属性内过滤<>事件注入
Level 5双引号属性内过滤script,转小写script双写/事件注入
Level 6双引号属性内过滤script,无转小写大小写混合
Level 7双引号属性内过滤script/onerror/javascript等双写绕过
Level 8href属性内过滤javascript:和;HTML实体编码
Level 9href属性内过滤javascript:并强制含http://注释符隔离
Level 10POST参数属性内换用POST接收抓包改方法 + 事件注入

5. 一点实操心得

刷完这10关再回头看,XSS漏洞的本质就是一个词:信任。开发者信任了用户的输入,把未经校验的数据直接拼进HTML;浏览器信任了服务端返回的内容,把看似正常的标签和属性全都解析执行。攻击者做的所有事情——闭合引号、双写关键词、实体编码绕过——都是在利用这份信任的裂缝。

我个人在实际项目中最受用的不是某个具体payload,而是那套“见招拆招”的思维方式:先判断输出位置,再分析过滤规则,然后从标签注入、事件注入、伪协议、编码绕过这几个方向里选一个能走的路径。这套方法论就是你刷Xss-Labs最大的收获,比记住一百个payload都值。

后面几关还会涉及更多编码绕过和特殊上下文(比如JavaScript代码块内的XSS),那又是另一重难度了。等你把这套前10关的思维练熟,再去碰存储型、DOM型XSS,会发现很多问题是相通的。刷题愉快。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 2:42:29

三防布与防火布行业TOP5厂家核心竞争力和选型指南

1. 行业背景与三防布防火布的核心价值 三防布&#xff08;防水、防油、防污&#xff09;和防火布作为工业防护材料领域的"特种兵"&#xff0c;近年来在建筑、交通、仓储等领域的应用呈现爆发式增长。根据全球市场调研机构Statista的数据显示&#xff0c;2023年全球工…

作者头像 李华
网站建设 2026/9/11 2:33:41

Auracast蓝牙广播音频开发实战:BT2106C模块与LC3编码详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 2:33:37

数据结构实验三单链表详解:从指针原理到工程实现与避坑指南

说来也巧&#xff0c;每年到这个节点&#xff0c;总能在群里看到同一类问题&#xff1a;“实验3到底要写什么”“链表怎么老崩”“排序排完链表断了”。中国矿业大学的数据结构实验课进行到第三个实验&#xff0c;基本上就到了大家集体和指针搏斗的阶段。前两个实验如果还能靠静…

作者头像 李华
网站建设 2026/9/11 2:33:28

Django项目管理系统开发实战:从模型设计到部署上线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 2:32:50

嵌入式开发板完整启动流程:从环境搭建到烧录验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华