XSS靶场实战入门:反射、存储、DOM型漏洞原理与绕过技巧

发布时间:2026/10/31 11:17:30
XSS靶场实战入门:反射、存储、DOM型漏洞原理与绕过技巧

简介:这是一份面向Web安全初学者与渗透测试入门者的XSS专项练习靶场,基于经典的xss-labs-master搭建,通过一系列循序渐进的关卡帮助学习者理解反射型、存储型与DOM型XSS的注入原理、payload构造思路及常见防御手段,适合作为网络安全课程实验或自学练手环境。压缩包共49个文件,以24个PHP关卡脚本为核心,辅以15张PNG关卡示意图、3个HTML入口页、2个JS脚本及若干SWF演示文件,整体约1.08MB,体积轻量便于本地部署。目前已有556人学习下载,具备一定的实践参考基础。资源完整保留了从注入点识别、脚本构造到开发者工具调试的闯关脉络,配合关卡截图可直观对照每关的过滤逻辑与绕过思路,帮助读者在动手解题中逐步建立输入验证、内容安全策略与安全编码意识,形成对XSS攻防较为系统的认识。

1. 从一份入门级 XSS 靶场说起:它到底能练什么

很多人第一次接触跨站脚本,是在某个真实站点上随手弹了个alert(1),然后被 WAF 拦得怀疑人生,连回显都看不到。入门级 XSS 靶场解决的正是这个断层:它把反射型、存储型、DOM 型这些概念拆成一个个独立关卡,每关只留一个注入点,让你在可控环境里把「输入从哪进、输出到哪去、过滤在哪层」这条链路走通。这份资源就是一套典型的 xss-labs 风格靶场,纯 Web 页面构成,关卡从无过滤到层层加码,适合刚学完 HTML 和 JavaScript 基础、想系统练手的人,也适合做安全培训时的演示环境。它不教你打真实目标,只教你读懂浏览器怎么把一段字符串变成可执行代码。

2. 搭起来再谈原理:本地环境与关卡结构

靶场本身是 PHP 写的静态关卡集合,跑起来只需要一个能解析 PHP 的 Web 服务。很多人卡在第一步不是不会 XSS,而是环境没通,所以先把「能打开第 1 关」这件事做扎实,再回头理解每关的过滤逻辑。

2.1 环境选型:为什么优先 PHP 内置服务器

常见做法是用集成环境一键装,但对靶场这种轻量场景,PHP 自带的开发服务器更干净,不用配虚拟主机、不用改 hosts,出问题也好排查。前提是本机有 PHP 7.x 或 8.x,命令行能直接调用。

# 进入靶场根目录,确认入口文件存在 cd /path/to/xss-labs ls index.php # 启动内置服务器,监听 8080,指定当前目录为根 php -S 127.0.0.1:8080 -t .

这段命令的含义很直接:-S启动内置服务器,127.0.0.1:8080是监听地址,-t .把当前目录当作文档根。启动后浏览器访问http://127.0.0.1:8080/应该能看到关卡列表。如果返回 404,先确认index.php是否在根目录;如果返回源码而不是执行结果,说明 PHP 没装或没进 PATH,用php -v验证。参数上唯一要留意的是端口,8080 被占用就换 8081,别去动系统保留端口。

2.2 关卡目录与参数传递:先看懂 URL 再动手

xss-labs 的关卡通常以level1.php、level2.php这样命名,通过?name=之类的查询参数接收输入。打开第 1 关,地址栏里会出现类似level1.php?name=test的结构,页面把test原样回显。这一步的意义是让你确认「输入点」和「输出点」是同一个参数。

关卡区间典型过滤手段练习目标
1–5无过滤到简单实体编码理解反射型回显位置
6–10大小写、双写、属性闭合绕过基础黑名单
11–15事件属性、编码变形理解上下文决定 payload
16–20更严格的标签与属性限制综合判断可用注入面

这张表不是让你背,而是建立预期:前面几关是让你确认「原样输出就会执行」,后面才是过滤对抗。新手最容易犯的错是一上来就背 payload,结果连当前关卡把输入放进了<div>还是<input value>都没看清。正确顺序永远是先看源码里输出点的上下文,再决定用标签闭合还是事件属性。

2.3 用浏览器开发者工具定位输出上下文

按 F12 打开元素面板,搜索你输入的字符串,看它落在哪个标签、哪个属性里。如果落在标签之间,<script>alert(1)</script>这类直接生效;如果落在value="..."里,就得先闭合引号再引入事件,比如"><img src=x onerror=alert(1)>。这一步没有代码可抄,但它是整个靶场最核心的动作:上下文决定 payload 形态。我一般会先在输入框里打一个特殊标记如zzz123,再在源码里搜它,比肉眼翻 DOM 快得多。

3. 三类 XSS 的实操路径:反射、存储、DOM

靶场关卡按难度递进,但底层就三类。把这三类的触发条件分别跑通一遍,后面再花哨的过滤都只是在这三类上做变形。

3.1 反射型:参数回显即执行

反射型的特征是服务端把参数原样拼进 HTML 返回,不落库。第 1 关通常就是这种。构造 URL:

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

浏览器把name的值拼进页面,脚本被执行。这里要注意 URL 里的特殊字符,<和>在部分浏览器地址栏会被自动编码,实际发送时可能变成%3C,服务端解码后又还原,所以多数情况能直接生效。如果没弹窗,先看页面源码里尖括号是否被转成了&lt;,转了就是服务端做了实体编码,属于下一关的过滤逻辑,不是环境问题。

3.2 存储型:落库后的持久触发

存储型关卡会把输入写进文件或数据库,再在列表页读出来。这类关卡的价值在于让你理解「一次注入、多次触发」的危害模型。操作上先在某关提交带脚本的内容,再刷新列表页观察是否执行。常见做法是靶场用文本文件模拟存储,提交后内容出现在另一个页面。

# 若关卡用文件存储,可查看写入结果,确认是否被转义 cat data/messages.txt

如果文件里存的是原始<script>,说明存储层没过滤,问题在输出层;如果存的是实体编码,说明入库前就处理了。这个区分很重要:过滤放在入库前还是输出前,决定了你能不能通过二次编码绕过。参数上留意提交表单的字段名,别把 payload 填进了昵称却去查留言板。

3.3 DOM 型:不经过服务端的执行链

DOM 型的输入根本不发给服务端,全靠前端 JavaScript 用innerHTML、document.write或eval把 URL 片段写进页面。这类关卡看源码最有效:搜location.hash、location.search、innerHTML这几个关键字,找到取值点再构造。

// 典型 DOM 型取值:从 hash 读内容并写入页面 var payload = location.hash.substring(1); document.getElementById('out').innerHTML = payload; // 触发方式:在 URL 后加 #<img src=x onerror=alert(1)>

这段代码说明 DOM 型的执行完全在浏览器端,服务端日志里看不到 payload,传统基于请求体的检测容易漏。实操时把 payload 放在#后面,因为 hash 不会发送到服务端,正好对应这类关卡的取值方式。如果页面用的是location.search,就放查询参数里。判断依据始终是源码里那个取值函数,不是猜。

4. 过滤对抗的避坑清单:五个真实翻车点

靶场后半段的关卡专门制造过滤,新手在这里翻车最多。下面五条是我见过频率最高的,按「现象 → 原因 → 解决」拆开。

4.1 弹窗没出来,但源码里脚本明明在

现象:页面源码里能看到完整的<script>alert(1)</script>,浏览器却不执行。原因通常是脚本被插进了<textarea>或注释里,HTML 解析器不把它当脚本;也可能是</script>被提前闭合导致结构错乱。解决:回到元素面板确认实际 DOM 结构,若在 textarea 内,先闭合</textarea>再引入脚本;若在属性内,改用事件属性而非 script 标签。

4.2 大小写混写被拦,以为黑名单很严

现象:<ScRiPt>也被过滤。原因:不少关卡用不区分大小写的正则匹配标签名,混写无效。解决:换思路,不用 script 标签,改用<img src=x onerror=alert(1)>或<svg onload=alert(1)>,事件属性往往不在同一套黑名单里。这也是靶场想教你的:过滤规则决定绕过方向,不是 payload 越长越好。

4.3 引号闭合了却没生效

现象:输入"><script>后页面结构崩了但没执行。原因:输出点可能被放在单引号属性里,你用双引号闭合自然无效;或者属性值后面还有别的字符干扰。解决:先确认属性用的是单引号还是双引号,再决定闭合符;必要时用' onmouseover='alert(1)这种不依赖闭合标签的写法。

4.4 编码后 payload 变成死字符串

现象:把alert写成\u0061lert或 HTML 实体后完全不执行。原因:编码只在特定解析阶段有效,HTML 实体在 script 标签内不会被二次解码,JS 的 Unicode 转义又要求处在 JS 执行上下文。解决:先判断当前上下文是 HTML 还是 JS,HTML 层用实体编码绕过关键字过滤,JS 层才用\u转义,两者别混用。

4.5 本地能弹,换浏览器就不行

现象:同一关在某个浏览器生效,另一个没反应。原因:不同浏览器对畸形 HTML 的容错解析不同,标签闭合顺序、属性解析规则有差异。解决:以目标环境的主流浏览器为准做验证,别拿一个浏览器的结果当通用结论;调试时优先看 DOM 树而不是页面视觉,DOM 结构才是解析器的真实输出。

提示:每关动手前先看源码里输入落在哪,比背一百个 payload 有用。靶场的价值是训练「读上下文」的习惯,不是收集绕过技巧。

5. 从通关到能用:把靶场变成检测思路的训练场

打通全部关卡只是起点,真正有价值的是把每关的过滤逻辑反过来当成检测规则来想。比如某关只过滤了<script,那检测侧就应该覆盖事件属性、javascript:伪协议、SVG 标签这几类等价执行面;某关对on开头属性做了黑名单,检测侧就要考虑大小写、空格、换行、HTML 实体等变形。我习惯在通关后做一张对照表,左边写关卡用的过滤手段,右边写这种过滤会漏掉什么,这张表比关卡本身更耐看。

验证方法上,别只依赖弹窗。alert(1)只是最直观的信号,实际检测中更稳的做法是让 payload 产生一个可观测的副作用,比如请求一个本地不存在的路径,然后在开发者工具的 Network 面板里看有没有发出请求。这样即使弹窗被浏览器策略拦了,你也能确认代码确实执行了。

// 用发起请求替代弹窗,便于在 Network 面板确认执行 new Image().src = '/xss-probe-' + Date.now();

这段代码构造一个带时间戳的图片请求,执行成功就会在 Network 里留下记录,时间戳还能区分是哪次触发。参数上把路径换成靶场同源的任意地址即可,跨域请求可能被策略限制,同源最稳。对于存储型关卡,用这种方式还能确认「刷新后是否再次触发」,比反复看弹窗可靠。

进阶一点,可以把每关的 URL 和 payload 整理成一个可重复执行的清单,用脚本批量请求并检查响应里是否原样回显了危险字符。这不是为了自动化通关,而是训练「把手工验证转成可回归的检查项」的思路,真实项目里的安全测试也是这么沉淀下来的。

关卡类型手工验证信号可回归的检查点
反射型弹窗或请求发出响应体是否含未编码的注入串
存储型列表页二次触发存储内容与输出编码是否一致
DOM 型hash 变化即执行前端取值函数是否直接写 innerHTML

从那以后我每次碰这类靶场,都会先把「输入点、输出上下文、过滤层」三件事写在纸上再动手,通关速度反而比乱试快。希望帮到你。

本文还有配套的精品资源,点击获取