资讯动态

XSS-Labs靶场1-8关实战:从反射型到过滤绕过全解析

发布时间:2026/9/25 11:29:54 来源:尧图企业网站定制
XSS-Labs是我见过最适合新手练手的一类XSS靶场总共十几关每一关都是同一个搜索框但过滤规则一级一级加码。刷完前八关你基本就把反射型XSS最常见的几种绕法摸清了直接输出、属性闭合、尖括号被禁、事件属性、伪协议、大小写绕过、双写绕过、HTML实体编码绕过。这篇我就按实战顺序把1到8关逐个拆开讲每一关都说清楚“payload为什么这么写”“过滤规则到底卡在哪一步”而不是只给你一串能弹窗的代码然后完事。这个项目适合刚学XSS、准备CTF入门、或者想搞清楚过滤器为什么总是被绕过的开发同学。先说一句所有payload都只在你本地靶场里用不要对没有授权的目标做任何测试这是底线。1. 搭环境与第1关最原始的反射型XSS长什么样1.1 xss-labs环境准备xss-labs是PHP开发的靶场部署起来非常简单。我用的是PHPStudy把下载好的xss-labs压缩包解压到WWW目录下目录名保持xss-labs然后启动Apache和PHP访问http://127.0.0.1/xss-labs/level1.php?nametest能看到页面显示“欢迎用户test”说明靶场已经跑起来了。环境上我建议用PHP 5.x或PHP 7.0尽量不要用太高版本的PHP因为靶场里部分写法比较老在PHP 8.x下可能因为函数行为变化或告警影响页面显示。另外新手经常遇到的一个坑是payload里有script这类字符时浏览器地址栏会自动做URL编码这个不会影响执行服务器最终拿到的还是原始字符串不需要手工百分号编码。1.2 Level 1无过滤的核心链路第1关是整个靶场最简单的入口源码核心就几行?php $str $_GET[name]; echo h2 aligncenter欢迎用户.$str./h2; ?参数name没有经过任何过滤或转义直接拼到了HTML标签中间。所以payload也非常直白http://127.0.0.1/xss-labs/level1.php?namescriptalert(1)/script访问之后页面会直接弹窗。这个弹窗看起来“很没技术含量”但它说明了一个关键事实服务器把用户的输入当成HTML代码的一部分原样输出了。你要理解浏览器在解析HTML时scriptalert(1)/script不是一个普通文本而是一个可执行脚本标签。用户输入、服务端拼接、浏览器解析这三者连成了一条线就是反射型XSS的完整链路。1.3 为什么叫“反射型”第1关这种类型叫做反射型XSS因为payload不会存在服务器上而是藏在这个URL里服务器收到请求后“反射”到响应页面。受害者必须打开这个攻击者精心构造的URL才会触发所以它通常情况下是一锤子买卖不像存储型XSS那样能持续影响所有访客。但反射型XSS依然是高危问题尤其是配合钓鱼链接、短链接伪装危害一点不比存储型小。我在实际做代码审计时看到这种裸拼接的输出第一反应就是直接标记为高风险不需要再确认其他条件。2. Level 2-3输出位置进入HTML属性闭合成了第一道坎2.1 Level 2双引号属性闭合第2关还是那个搜索框但如果你把第1关的scriptalert(1)/script原封不动粘进去会发现没有弹窗。这时候一定要学会看源码而不是瞎猜。第2关核心代码如下?php $str $_GET[keyword]; echo h2 aligncenter没有找到和.$str.相关的结果./h2; echo input namekeyword value.$str.; ?前一个h2标签里是直接拼接所以直接塞script理论上也能弹但标准解法是处理第二个输出点——input标签的value属性。这里的问题在于你的输入被放进了input value...的双引号里面单靠一个script标签改变不了HTML的解析结构因为浏览器看到双引号时认为value还没结束。所以要做的第一件事是“闭合属性”payload写成http://127.0.0.1/xss-labs/level2.php?keywordscriptalert(1)/script拆开看就明白了用来闭合value属性的双引号用来关闭input标签然后scriptalert(1)/script作为新的标签插入页面。浏览器最终解析出来的结构是input namekeyword valuescriptalert(1)/script这一下脚本就能正常执行了。这也是XSS里最经典的“属性闭合”思路先搞清楚你的输入出现在哪个引号、哪个标签里再决定怎么闭合。2.2 Level 3单引号闭合与事件属性第3关的表面变化很小但实际处理方式变了。核心代码如下?php $str htmlspecialchars($_GET[keyword]); echo input namekeyword value\.$str.\; ?这里有两个细节输出位置改成了单引号包围的value...。用了htmlspecialchars()做输出编码。htmlspecialchars()默认会把双引号、尖括号等字符转成HTML实体所以这套闭合在Level 3走不通因为尖括号和双引号都被实体化了。但它默认不会转义单引号所以我们可以用单引号把value闭合掉然后塞一个事件属性进去。常用payloadhttp://127.0.0.1/xss-labs/level3.php?keyword onfocusalert(1) autofocus解析成HTML大概是input namekeyword value onfocusalert(1) autofocus你的输入先用单引号把value闭合掉然后添加了onfocus事件属性。autofocus的作用是让输入框一加载就自动获得焦点onfocus事件就会立刻触发。如果你不想自动触发改成 onclickalert(1)然后手动点一下输入框也行。这里的关键是在属性上下文里就算不能用script标签事件属性依然能执行JavaScript。2.3 输出位置判断清单刷到第3关你应该养成一个条件反射拿到一个回显点先判断它出现在HTML的什么位置再决定构造思路。我自己常用的检查顺序是直接在URL里传一个特殊字符比如test看页面源码里这个字符出现在哪里、有没有被转义看源代码使用的是双引号还是单引号包裹属性看是否使用了htmlspecialchars()之类的编码函数观察尖括号、引号有没有变成lt;、quot;这类实体。这四步能帮你判断90%以上的输出位置问题后面的关卡基本都在围绕这个检查表做文章。3. Level 4-5尖括号和事件被禁payload开始变形3.1 Level 4过滤尖括号后的事件属性方案第4关开始真正的“过滤”出现了。核心处理逻辑大致是?php $str $_GET[keyword]; $str preg_replace(/|/, , $str); echo input namekeyword value.$str.; ?这一关把尖括号和直接替换掉了所以script、a这类标签方案全部失效。但是注意它没有过滤双引号也没有过滤事件属性关键字。所以我们依然可以用双引号闭合value再用事件属性触发脚本http://127.0.0.1/xss-labs/level4.php?keyword onfocusalert(1) autofocus最终解析出来的HTML是input namekeyword value onfocusalert(1) autofocus页面加载后输入框自动聚焦onfocus触发弹窗。如果你觉得自动弹窗不够自然也可以改成鼠标相关的事件比如 onclickalert(1)但需要用户点击交互。事件属性在尖括号被禁的情况下是效率最高的替代方案因为它不需要构造新标签只要闭合当前标签的属性并在原地追加事件就行。3.2 Level 5过滤script和on后的伪协议接力第5关又往前走了一步过滤的东西变多了大致是这两条规则preg_replace(/script/i, , $str); preg_replace(/on/i, , $str);script标签被过滤on开头的所有事件属性也被过滤因为onfocus、onclick等事件里都含on。这时候再用事件属性方案就不行了。但黑名单的漏网之鱼也很明显href没被过滤javascript:协议也没被过滤。那出路就是a标签伪协议http://127.0.0.1/xss-labs/level5.php?keyworda hrefjavascript:alert(1)点我/a先闭合value和input插入一个带hrefjavascript:alert(1)的链接页面会出现一个“点我”的链接点击之后执行JavaScript。这里的javascript:协议本质上是一种浏览器支持的URL scheme用户点击链接时浏览器会执行后面的JavaScript代码。对了这一关的源码里可能同时还在h2标签里回显了你的输入但那个位置被过滤后已经没什么可挖的了专注属性闭合思路就好。3.3 黑名单过滤的盲区在哪看到第5关你应该能总结出一个规律了黑名单过滤永远是“堵了一个点漏了一个面”。开发者过滤了script忘了事件属性补上事件属性又忘了javascript:伪协议补上伪协议后面还有实体编码等着。核心原因是浏览器的HTML解析规则非常丰富同一个执行JavaScript的方式可以有无数种表达形式而黑名单只能基于有限的字符串匹配。这也解释了为什么后续几关的绕法会越来越多本质都是在“过滤规则覆盖范围”和“浏览器实际解析行为”之间找一个交集。4. Level 6-7关键词替换成空值后的两条经典绕过4.1 Level 6大小写绕过第6关过滤的关键字更多了script、on、src、data、href基本都有。如果你直接用小写payload多半会被清掉。但这一关的过滤规则写成了只匹配小写字符串没有加大小写不敏感的i修饰符。也就是说script会被过滤但SCRIPT不会被过滤。所以payload可以直接大写http://127.0.0.1/xss-labs/level6.php?keywordSCRIPTalert(1)/SCRIPTHTML标签和属性名是不区分大小写的浏览器照样把SCRIPT当成一个有效的脚本标签来执行。HTML5规范里标签名的解析步骤就是大小写不敏感的所以服务端过滤规则一旦忘了忽略大小写这道防线基本等于没设。这里也提醒你一件事写过滤规则的时候凡是跟HTML关键字相关的正则只要没有明确理由都应该加上i修饰符。我这个习惯就是在刷靶场时养成的。4.2 Level 7双写绕过第7关把过滤规则改成了直接把关键字替换成空字符串看起来比第6关更严格因为script、on、src这些词都会被删掉。但它有一个致命缺陷只删一次而且不递归。以script为例如果你构造一个scrscriptipt过滤逻辑从左到右匹配到一个script子串后把它删掉剩下的字符拼在一起恰好就是script。这就是双写绕过的核心原理。xss-labs第7关的经典payloadhttp://127.0.0.1/xss-labs/level7.php?keywordscrscriptiptalert(1)/script开头的scrscriptipt经过删除后变成script后面的/script也会被删掉一部分浏览器在容错解析下通常也能正常执行。但我个人更推荐另一种更稳定的玩法——在属性上下文里双写事件关键字不依赖浏览器对损坏闭合标签的容错能力http://127.0.0.1/xss-labs/level7.php?keyword onnfocusalert(1) autofocus因为过滤规则会把on删掉onnfocus删掉一个on之后就变成了onfocus事件属性完好无损。这条payload在大多数环境里行为非常稳定我实测多次没有翻车。双写绕过的本质是过滤器把字符串中的某些子串“擦除”后剩下的内容又会重新拼出攻击关键字典型的过滤层和解析层之间的信息差。4.3 替换型过滤的通病双写、大小写、实体编码这些绕过方式能成立前提都是过滤逻辑在做“字符串查找替换”时没有考虑“替换后可能产生新的危险结构”。你如果把这种替换型过滤用到生产环境我的建议是不要做这种层层替换直接换成白名单校验或者标准输出编码否则永远有下一招等着你。拦截XSS最可靠的方式不是黑名单而是从输出侧做统一的上下文编码。5. Level 8href里的实体编码伪协议5.1 第8关的难点在“小写化关键字过滤”第8关的场景是一个“友情链接”入口你的输入会出现在a标签的href属性里。核心处理逻辑大致有两条把所有输入先转成小写大小写绕过的路被堵死对script、on、src、data、href等关键字做字符串替换。如果你直接提交javascript:alert(1)会发现javascript部分被过滤掉链接报废。这里就要用到第8关的标准解法HTML实体编码。payload如下http://127.0.0.1/xss-labs/level8.php?keyword#106;#97;#118;#97;#115;#99;#114;#105;#112;#116;:alert(1)这一串十进制实体编码对应的字符是javascript加上后面的:alert(1)整体在页面上点击“友情链接”时就能弹窗。为什么这样能绕过因为服务端的字符串替换看到的是#106;这种实体编号它不会去解码后再匹配所以“javascript”这个关键字就被成功绕过了。5.2 为什么实体编码在href属性里会被浏览器解码这里很多新手会困惑实体编码的字符串在HTML里不是会被原样显示吗为什么在href里就能变成真正的脚本原因在于浏览器解析HTML属性值时有两个阶段先做HTML实体解码再做URL或脚本解析。具体到href...属性浏览器在计算出最终属性值时会把像#106;这样的字符实体翻译成对应的字符得到javascript:alert(1)然后发现这是一个合法URL scheme点击链接时就会执行JavaScript。这个细节也解释了为什么很多全局XSS过滤器面对实体编码会失效过滤器只处理输入侧字符串而浏览器在输出侧有一个“二次解码”的过程。只要过滤规则没有跟着浏览器一起解析就永远存在绕过缝隙。5.3 顺带说一下DOM型XSS前8关本质上都是服务端反射型XSS但你在学习时经常会看到“DOM型XSS”这个名词。DOM型XSS的特点是服务端根本不参与拼接而是JavaScript直接读取location.hash、location.search里的内容再通过innerHTML、document.write等API写进页面。它的构造思路和前8关很像先搞清楚你的输入最终进了哪个DOM API再考虑要不要闭合标签、要不要编码。xss-labs后面的关卡也会有类似的场景等你把前8关的闭合和绕法练熟再去碰DOM型XSS会轻松很多。6. 通关后的防御复盘6.1 输出编码必须按上下文区分刷完前8关最应该带走的一项技能不是“怎么绕过过滤器”而是“怎么防御这些绕过”。防御XSS最核心的手段是输出编码但编码不能一刀切要看你输出在什么上下文里输出上下文典型场景推荐编码方式HTML内容文本直接插在div、p标签内部htmlspecialchars()转义、、、引号HTML属性输入插在value、href、title等属性中对引号和尖括号做实体编码最好属性整体用单引号包裹JavaScript输入插在script代码块或JS字符串中使用JSON序列化或反斜杠转义绝不可直接拼接URL输入拼接到href、src等URL中使用urlencode()或对协议白名单校验只允许http/https很多框架的模板引擎自带上下文感知转义比如Vue里的插值、React里的JSX文本节点默认就已经帮你做了转义。真正危险的是那些用字符串拼接写HTML的旧代码以及被“图省事”绕开模板引擎的写法。6.2 全局过滤器能解决什么不能解决什么现在很多项目会做一个全局XSS Filter比如SpringBoot工程里对请求参数统一清洗也常看到有人把上传PDF、处理文件流时顺便做一层XSS处理。我只能说这种Filter适合做“第一道兜底”能拦住大部分反射型XSS和基础的存储型XSS但它永远替代不了输出编码。原因很简单Filter在输入侧清洗而业务数据可能会在多个地方、以多种上下文被输出。同一段用户输入在HTML内容里、属性里、JS脚本里、URL里需要的处理方式完全不同。全局Filter要么清洗过头把正常业务数据弄坏要么清洗不足被实体编码、大小写、双写这种手法绕过。所以架构上正确做法是三层配合输入侧做基础校验和长度限制输出侧按上下文编码再配合CSP内容安全策略做最后兜底。CSP能限制页面内脚本的来源和执行方式是现在对抗XSS非常有效的一层防线。6.3 给新手的下一步建议如果你刚把前8关走通我建议你按这个顺序继续往下走用开发者工具查看每一关最终渲染出来的DOM结构复盘payload每个字符的作用这一步比背一百个payload都有用自己改造一下源码比如把preg_replace加上i修饰符看看原来的绕过是不是就不成立了这样能加深你对过滤规则的理解去玩DVWA和Pikachu里的XSS靶场把反射型、存储型、DOM型都对比着做一遍有条件的话做几道CTF里的XSS题尤其是xss-labs后续关卡会碰到JavaScript上下文和更复杂的过滤思路会再上一个台阶。一定要记住这些技能是用来做安全测试和漏洞修复的合法的测试永远只在靶场、CTF和你自己授权的系统里进行。我在实际工作中见过太多因为安全意识不足拿真实站点练手导致的事故真的没必要。最后再分享一个小习惯我从刷完xss-labs之后写代码或者做代码审计时脑子里会立刻问自己“这个输入最终会出现在哪个上下文里浏览器解析到这一步时它会被怎么当成什么角色”。想清楚这个问题很多XSS漏洞和WAF绕过案例基本都能看懂了。这8关看起来是游戏其实是在帮你建立一个“浏览器视角”的思维模型。把这个模型建起来后面再难的东西也就是在这个模型上堆细节。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑