XSS-Labs靶场实战:从反射型到属性绕过的跨站脚本攻击通关指南
1. 跑通靶场再谈原理环境准备与XSS的三个基本形态XSSCross-Site Scripting跨站脚本攻击是Web安全里最容易上手、也最容易被人低估的一类漏洞。网上一搜全是alert(1)的截图但真到自己写payload时很多人连“为什么弹窗没出来”都搞不清楚。我的建议很直接与其零散地看各种文章不如系统地把Xss-Labs靶场的前十关刷一遍。这套基于PHP的开源靶场几乎每一关都在模拟一种过滤场景从最原始的反射型XSS到闭合属性、大小写绕过、双写绕过、实体编码、隐藏参数注入恰好覆盖了日常渗透测试和代码审计里最常见的套路。这篇文章把我刷1-10关的完整思路、payload构造过程和踩过的坑写下来适合刚接触XSS的读者也适合准备面试前想快速复习的人。靶场本身是本地环境没有任何合规风险放心练。1.1 跑一个本地靶场需要什么Xss-Labs是个纯PHP项目不需要数据库部署非常简单。准备一个PHP运行环境版本建议用PHP 5.x或7.x然后去开源社区把靶场源码拉到本地解压后放到Web根目录访问首页就能看到关卡列表。有一点特别提醒这个靶场年头不短了部分关卡代码用到了老PHP函数如果你用的是PHP 8环境打开页面可能直接白屏或报错。遇到这种情况别慌看报错信息把对应位置改掉或者直接换个低版本PHP的集成环境跑。千万别拿高版本环境硬扛时间都浪费在环境上了。另外这是本地靶场纯学习用途不要把它部署到公网免得被人拿来做跳板。环境起来之后第一件事不是急着打关而是先确认自己能看懂页面源码。后面每一关我都会反复让你F12看Elements或者右键“查看网页源代码”因为XSS题目的本质就是“找到数据进HTML的位置然后逃逸出去”。1.2 反射型、存储型、DOM型三句话区分第1-10关绝大部分是反射型XSS但后面还有存储型和DOM型关卡所以基础分类先理清。类型数据走向什么时候触发典型场景反射型参数 → 服务端 → 页面原样回显受害者必须打开带payload的链接搜索框、URL参数回显存储型数据 → 数据库 → 页面渲染给所有人任何用户访问被污染的页面评论区、留言板、个人资料DOM型参数 → 前端JS直接操作DOM前端逻辑拼接导致可能不经过服务端URL hash、前端渲染逻辑用生活化类比反射型像你朝山谷喊话山谷把话原样弹回来只有你能听到存储型像在墙上刻字每一个路过的人都看得见DOM型像你把自己说的话告诉朋友朋友自己脑补后念了出来问题出在朋友“脑补”那一步。第1-10关先把“反射”这条链路练扎实后面遇到存储和DOM思路不会差太远。1.3 为什么一定要按顺序刷网上很多writeup喜欢跳着写哪关好打就打哪关。但Xss-Labs的关卡设计是严格递进的第一关无过滤第二关到第四关训练“闭合标签”和“闭合属性”第五关到第七关训练各种绕过第八关到第十关开始上编码和隐藏参数。如果你直接跳到第八关发现自己连javascript伪协议都不熟那就漏了前几关打下的基础。我的习惯是每过一关就去看一眼这一关的PHP源码。靶场源码就放在本地web目录里翻开源码你就能看到出题人到底做了什么过滤这种“边打边看源码”的方式比对着writeup抄一遍有效太多。前面几关源码极短过滤逻辑一眼就能看懂正好帮你在脑子里建立起“输入是怎么被处理的”这个模型。2. Level 1-4从直接弹窗到属性闭合注入点决定攻击姿势前四关的payload本身很简单但它们的价值在于让你学会一件事先判断输入数据到底被拼到了HTML的哪个位置再决定怎么逃逸。这四关分别对应“标签内直接执行”“标签间注入”“单引号属性闭合”“双引号属性闭合”把这几个场景吃透后面所有关卡都在它的延长线上。2.1 Level 1参数原样进HTML最原始的反射型XSS第一关基本是送分题。页面上有个链接点进去后URL里带着一个name参数这个参数的值直接显示在页面上。输入hello页面显示hello输入scriptalert(1)/script页面直接弹窗。http://localhost/xss-labs/level1.php?namescriptalert(1)/script为什么这么简单因为服务端拿到name参数后没做任何过滤就拼接进了HTMLh1参数值/h1。浏览器解析这段HTML时把script标签当作脚本执行了。这一关虽然简单但它完整定义了反射型XSS的两个必要条件一个用户可控的参数一个输出位置。缺一个都不行。后面所有关卡不过是给这两者之间加上各种过滤和转义。第一关的源码值得看一眼它帮你理解“反射”的含义——不是攻击者主动发数据而是目标页面对访问者输入的“回声”。2.2 Level 2value双引号包裹先把标签闭合再注入第二关开始上难度了。页面上有个搜索框你在框里输入内容提交后内容会出现在一个输入框的value属性里。这时候如果你还是直接提交scriptalert(1)/script会发现页面没弹窗。F12看一下Elements你会看到input namekeyword valuescriptalert(1)/script问题很明显你输入的脚本被放在了value...里面成了属性值的一部分浏览器不会执行属性值里的标签。要想让脚本“逃”出来首先要把value属性闭合掉再把input标签闭合掉让后面的内容重新成为HTML标签级别的结构。payload就三截scriptalert(1)/script拆开看第一个负责闭合value属性的双引号紧跟的闭合了整个input标签之后注入的scriptalert(1)/script不再处于任何属性内部浏览器直接把它当作新标签解析执行。这里有个很容易踩的坑如果你只输入scriptalert(1)/script而不闭合引号脚本永远只是属性值字符串。所谓“逃逸上下文”就是先把包裹你的容器打破再谈注入。如果你是在地址栏直接拼参数注意双引号和尖括号最好做一下URL编码比如%22%3E%3Cscript%3Ealert(1)%3C/script%3E否则浏览器可能提前把特殊字符按URL规则处理服务端收到的数据跟你以为的不一样。2.3 Level 3单引号包裹下的onfocus事件注入第三关和第四关是姐妹关。这一关的搜索框把输入回显到了单引号包裹的属性里input namekeyword value你的输入如果第二关用的是双引号闭合这关自然想到用单引号闭合。但光闭合还不够你还需要构造一个能自动触发的路径。这里引入了一个非常重要的思路XSS不一定非要script标签事件属性同样能执行JS。payload onfocusalert(1) autofocus逐段看第一个闭合了value属性的单引号后面加一个空格把onfocusalert(1)作为新属性塞进input标签autofocus让这个input元素进入页面后自动获得焦点最后的只是让属性结构看起来完整。onfocus事件在元素聚焦时触发所以页面一加载输光标自动落进这个输入框alert就弹了。如果你不想依赖聚焦也可以用scriptalert(1)/script闭合整个标签很多writeup确实这么写。但我个人更建议把事件型payload练熟因为从第四关开始尖括号一旦被转义事件属性几乎就是唯一出路。这个“不闭合整标签、只在属性层面做文章”的思路会陪你走完后面相当多关卡。2.4 Level 4尖括号被转义属性注入成了唯一出路第四关开始在服务端做过滤了。你提交scriptalert(1)/script看页面源代码会发现尖括号被转义成了lt;scriptgt;也就是说标签注入这条路被堵死了。但关键问题是引号有没有被过滤这关没有双引号还能直接用。所以输入被包裹在input namekeyword value你的输入既然尖括号不能用了就不跟它硬碰硬直接在属性里注入事件。payload onclickalert(1)第一个闭合value属性onclickalert(1)注册一个点击事件最后的双引号虽然不闭合也经常能跑通但完整闭合更稳妥。生成的结构相当于input namekeyword value onclickalert(1)点击这个输入框弹窗。如果你想让它自动触发用 onfocusalert(1) autofocus进页面自动弹窗。这关给我的教训很直接看到尖括号被转义别急着放弃先检查引号是否还在。很多真实场景里开发者只转义了和却忘了属性外面还套着引号结果属性注入直接打穿。判断过滤有没有“过滤到位”看的是你所处的HTML上下文里哪些字符还能被你自由控制。3. Level 5-7当过滤出现三种绕过思路的递进拆解从第五关开始服务端开始主动过滤关键字了。这三关的递进关系特别清晰先过滤script标签再过滤事件属性最后连大小写都处理了。每一关都在告诉你一件事——黑名单过滤永远有盲区盲区在哪绕过思路就在哪。3.1 Level 5script被过滤换标签换事件就能破第五关你提交scriptalert(1)/script页面源码里script这几个字直接消失了。这说明服务端做了关键字替换把script整个删掉了。但这里有个误区要纠正XSS想执行JS不是非script标签不可。事件属性、伪协议、CSS表达式都是可执行JS的载体。第五关只堵住了script标签没有堵住事件属性所以换一个带事件的标签就行。payloadimg srcx onerroralert(1)img标签加载srcx这个不存在的地址触发onerror事件执行alert(1)。这里src的值随便写只要让图片加载失败就行。这关还有一个经典变体用a标签配合javascript伪协议a hrefjavascript:alert(1)点我/a点击这个链接就会执行JS。这个变体在后面第八关、第九关还会遇到所以现在就要记住hrefjavascript:...是一种独立于script标签的JS执行通道。第五关的实际意义是当你做黑名单绕过时第一反应不应该是“怎么绕过script这几个字”而是“除了script还有什么能执行JS”。img的onerror、svg的onload、a的href伪协议、iframe的srcdoc都是可用的执行载体。黑名单只能堵住它认识的关键字堵不住所有可能性。3.2 Level 6大小写绕过过滤脚本的常见盲区第六关比第五关狠了一点你把img srcx onerroralert(1)提交上去发现onerror也没了。过滤规则盯上了事件属性。但这里有个细节很多服务端过滤是基于字符串匹配的而匹配时没有把大小写归一化。HTML标签名、属性名对大小写完全不敏感JS的ALERT(1)也能正常执行。于是payload直接改大小写IMG SRCx ONERRORALERT(1)或者更简单ScRiPtalert(1)/ScRiPt浏览器会把ScRiPt当成script来解析而服务端的字符串匹配看的是原样输入ScRiPt不等于script于是放行。这个绕过在CTF和靶场里非常常见因为很多开发者写过滤代码时忘了strtolower或正则没加i标志。不过说句实话真实生产环境里靠大小写绕过的机会越来越少了但它帮你建立了一个非常重要的意识过滤代码是程序写的程序对“等价写法”的认知永远不如浏览器完整。大小写只是众多等价变体里最简单的一种。如果把本题的过滤条件理解为“过滤了script和on*事件关键字但不区分大小写”的话大小写就是唯一的突破口。第五关教你“换载体”第六关教你“换写法”这两者其实是同一个思路的两面在不能直达终点的路上找替代路径。3.3 Level 7双写嵌套删掉关键字后残留拼接第七关把大小写这条路也堵了服务端对关键字做了不区分大小写的过滤。但这里出现了一个更有趣的漏洞过滤逻辑是“把关键字整段删除”而删除操作本身可能导致前后残留字符拼出新的关键字。payloadscrscriptiptalert(1)/scr/scriptipt看第一段scrscriptipt。服务端把中间的script删掉剩下的scr和ipt拼在一起正好是script。结尾同理/scr/scriptipt删掉中间的/script拼出/script。浏览器最终看到的是scriptalert(1)/script于是正常执行。这就是“双写绕过”的本质过滤不是把危险字符替换成安全字符而是直接删除关键字删除本身制造了新的拼接机会。这个问题在真实代码里也经常出现很多开发者用str_replace(script, , $input)以为把所有script都删干净了却没意识到删除前后片段可能重新组合成攻击载荷。我刷这关时的体会是不要死记scrscriptipt这个固定串要理解它的构造过程。你只需要问自己两个问题第一过滤删的是什么第二我输入的哪些字符在删除后会被拼到一起。想明白这两个问题遇到任何“删除式过滤”你都能现场构造payload。4. Level 8-10从伪协议到隐藏参数难度开始上升打到这里前面的“闭合标签”和“绕过关键字”已经基本熟练了。第八关到第十关的玩法完全不一样它们不再跟你纠结script和尖括号而是把战场搬到了链接协议、URL校验和隐藏参数上。这三关更接近真实Web应用里XSS的表现形式我认为含金量比前几关高一个档次。4.1 Level 8href里的javascript协议用实体编码绕过滤第八关页面上有个搜索框输入内容后会生成一个链接链接的href值正好包含你的输入。也就是说服务端把你的输入直接拼进了a href你的输入。这里最常见的XSS手法是javascript伪协议a hrefjavascript:alert(1)点我/a点击链接执行JS。这关显然预料到了这一点把你输入的javascript:关键字过滤掉了。绕过方式是用HTML实体编码java#x73;cript:alert(1)#x73;是字符s的十六进制HTML实体。服务端在原始字符串里找javascript:看到的却是java#x73;cript:不匹配javascript:于是放行。浏览器解析href属性时会先把HTML实体解码把#x73;还原成s最终得到javascript:alert(1)点击链接就执行了。这关揭示了一个非常关键的概念过滤发生在服务端的字符串层执行发生在浏览器的解析层而HTML实体编码是两层之间的一个“翻译过程”。只要服务端过滤时没有对输入做HTML实体解码这个差距就能被利用。另外补充一个变体有些场景下用制表符也可以绕过比如java\tscript:alert(1)因为JS解析器对javascript协议后的控制字符容忍度很高。但实体编码是最稳定、最通用的方案优先掌握它。测试payload时别忘了点击那个链接这关不像前面会自动触发需要交互。4.2 Level 9服务端校验http://用注释符把特征值无害化第九关是第八关的一个升级版你的输入里必须包含http://这几个字否则页面直接返回“你的输入不合法”的提示。也就是说出题人加了一个合法性校验要求输入必须是一个URL。这里出现了一个很有意思的情况过滤规则要求存在http://但它没有限制http://出现的形式。那么payload可以写成javascript:alert(1)//http://服务端检测字符串里确实包含http://放行。浏览器执行javascript:alert(1)//后面的//是JS行注释http://整个被当成注释内容忽略不影响alert执行。还有一个更“优雅”的写法javascript:alert(http://)把http://塞进JS字符串常量里服务端看到它、浏览器也不觉得它碍事。这一关的价值在于“特征值无害化”的思维当过滤规则要求某段字符串必须出现时你要做的是让这个特征值出现在不影响payload执行的位置而不是正面违抗它。注释符、字符串常量、属性值都是放置特征值的“无害位”。这种思路在真实漏洞挖掘里特别实用因为很多WAF和代码审计工具就是靠关键字匹配来识别攻击一个不起眼的注释符就能打乱它们的判断。4.3 Level 10隐藏参数t_sort才是真正的注入点第十关的坑很多人踩过。页面上有个看起来人畜无害的搜索框你在keyword参数里怎么折腾payload都被过滤得干干净净尖括号、引号全被处理掉了。如果你只盯着可见的输入框这关会卡很久。右键查看网页源代码表单里藏着一个隐藏字段input namet_sort typehidden valuexxx而且t_sort的值也来自URL参数同样被回显到value属性里过滤规则比keyword宽松得多——尖括号被处理了但双引号还能用。于是把攻击参数从keyword换成t_sort onclickalert(1)生成的结构相当于input namet_sort typehidden value onclickalert(1)点击这个隐藏元素触发的时机不太好控制更稳妥的写法是 onfocusalert(1) autofocus页面加载后隐藏input获得焦点自动弹窗。这关教会我的是XSS的注入点不只有页面里能输入的地方而是所有“用户可控、并且被拼接到HTML里”的数据。隐藏域、cookie、请求头、文件名都可能成为注入点。做测试时一定要抓包把整个请求的所有参数看一遍逐个测试谁被反射了、谁被过滤了。防守方最怕的就是开发者只盯着“看起来危险”的字段却忘了角落里还藏着一个没做防护的参数。5. 通关之后我踩过的坑以及用防御视角看这十关十关刷完你手里已经有了一批payload但真正的收获不在于背会它们而在于看代码时能一眼找出“数据从哪里进、往哪里去、在哪一层缺了防护”。最后这部分我总结几个实战里常踩的坑再把防御思路对着十关过一遍你会发现攻防本来就是同一件事的两面。5.1 照着payload打不中四个最常见的翻车原因刷靶场时经常遇到“我payload明明跟writeup一样为什么没弹窗”的情况多半是下面几个原因第一没看包裹字符。第二关用双引号闭合第三关用单引号闭合搞错一个整个payload就成了属性值里的普通字符串。我的习惯是先提交一个123F12看Elements里它到底被什么符号包裹再决定闭合方式。第二事件型payload没触发。onclick需要点击、onmouseover需要鼠标移上去你构造完payload后不做交互当然不弹窗。测试时优先用onerror、autofocus这类能自动触发的属性省去交互环节。第三地址栏直传参数时特殊字符没编码。双引号、尖括号在URL里有特殊含义直接写进URL可能被浏览器提前处理导致服务端收到的数据和你想的不一样。建议用表单提交或者在URL里做百分号编码。第四分不清“渲染后的页面”和“源代码”。浏览器把标签渲染成看不见的控件但你得看Elements才能确认payload到底落在哪个节点。我见过很多人对着页面上“啥都没显示”就以为注入失败其实代已经弹过了只是没注意到。5.2 用开发者工具当靶场外挂刷这十关浏览器自带的开发者工具是我最常用的外挂比任何writeup都好使。Elements面板能直接看到浏览器解析后的最终DOM结构你的payload落在标签内、属性内还是属性外一眼见底。Console面板可以手动执行alert(1)用来验证当前页面的上下文是否允许JS执行。Network面板能看清每个请求的完整参数列表Level 10这种隐藏参数的发现就靠它。右键菜单里的“查看网页源代码”和服务端原始返回最接近有些靶场关卡会在HTML注释里直接留下过滤规则的提示。我刷每关的标准动作是先提交一个普通值看它被拼到哪再提交一个img srcx onerroralert(1)试水最后根据Elements显示的解析结果微调payload。这三步走下来大部分关卡不用看writeup也能过。5.3 从攻击思路反推防御十关对照三条防线把前10关的攻击思路合并同类项防护对策其实很清晰防御手段能挡住哪几关的攻击为什么有效输出编码Level 1-4、8-10的标签和属性注入对、、、引号做实体编码后payload无法逃逸出当前上下文黑名单过滤Level 1-2有效Level 5-7能绕过黑名单永远有遗漏适合减小攻击面不能当唯一防线CSP内容安全策略几乎所有内联payload禁止内联脚本和javascript伪协议后即使注入成功也执行不了HttpOnly Cookie把XSS造成的损失降到最低防止会话被窃取但不是阻止XSS本身输出编码是对付前四关最有效的手段Level 1如果不拼接原始输入第一个payload就死了Level 2到Level 4如果属性值也做编码属性注入的路径也被彻底堵死。黑名单过滤看起来能挡住Level 1和Level 2但第三关到第七关的绕过案例已经证明了黑名单的局限——过滤规则不可能列全所有等价写法。CSP属于纵深防御的最后一道门即使前面的编码和过滤都失效了浏览器策略仍然能拦下内联脚本的执行。按我自己的体会刷这十关别急着看writeup。卡住半小时再看一行提示效果比自己瞎折腾好也比直接抄作业好。每过一关花两分钟做一件事把这一关的输入原样提交然后用开发者工具看它最终被拼到了哪个节点。看明白节点你就真正吃透了这一关。把这套流程走完XSS对你就不再是几个神秘的payload而是一条清清楚楚的数据流。等遇到真实业务时你自然会知道哪里可能出问题出问题后应该在哪一层做修复。