简介:这是一份面向Web安全初学者与渗透测试入门者的XSS专项练习靶场,基于xss-labs-master搭建,通过一系列递进式挑战帮助学习者理解反射型、存储型与DOM型XSS的触发原理,并掌握注入点识别、payload构造与防御思路。压缩包共49个文件,以24个PHP关卡脚本为核心,辅以PNG关卡示意图、SWF与JS辅助资源及HTML页面,整体约1.08MB,体积轻量便于本地部署。目前已有556人学习下载,适合作为安全入门阶段的动手素材。读者可借助各关卡场景逐步练习输入过滤绕过、脚本注入与开发者工具调试,在解题过程中体会输入验证、内容安全策略与HTTP-only Cookie等防护手段的实际作用,从而形成从发现漏洞到修复加固的完整认知,为后续Web安全学习打下基础。
1. 从被一个弹窗打脸说起:xss 靶机到底该怎么用
第一次碰 xss 靶机,我以为把<script>alert(1)</script>敲进输入框、看到弹窗跳出来就算通关了。结果换到下一关,同样的 payload 直接变成一行死文字,页面连个反应都没有。后来才明白,xss-labs 这类入门靶场真正练的不是“背 payload”,而是让你在一条从反射到存储、从黑名单到白名单的关卡链里,把浏览器怎么解析、服务端怎么过滤、编码在哪一层生效这三件事串起来。这篇写给两类人:刚接触 xss 靶机、想照着搭起来一关一关打的新手,以及打过几关但总在编码和绕过上翻车、想补底层逻辑的熟手。我会按关卡类型拆思路,把可复现的 payload 和排查方法摆出来,你照着做能跑通,也能看懂为什么这么打。
2. 搭起来再谈打:xss 靶机的环境准备与关卡地图
2.1 环境怎么起:一份能跑通的最小清单
xss-labs 是 PHP 写的,本质就是一组level1.php到level20.php的页面,加一个入口索引。搭建这件事网上资料很多,我不重复抄步骤,只讲几个真正会卡住人的点。你需要的是一个能跑 PHP 的 Web 环境,PHP 版本别太新也别太旧,5.6 到 7.x 区间最稳,因为部分关卡依赖老版本的字符串处理行为。把靶场目录丢进 Web 根目录,浏览器访问入口页能看到关卡列表,就算起好了。
我一般会先确认三件事再开始打:第一,浏览器开发者工具的网络面板能正常看到请求和响应;第二,页面源码里能搜到自己的输入原样回显的位置;第三,准备一个干净的浏览器配置文件,别拿日常用的浏览器打,存储型关卡会往本地塞东西,污染了排查起来很烦。这三步花不了五分钟,但能省掉后面大量“到底是靶场坏了还是我 payload 错了”的扯皮。
提示:靶场只在自己本地或授权环境里跑,别往任何线上站点丢 payload,这是底线,不是建议。
2.2 关卡地图:先分类,再动手
二十来关看着多,其实按反射点位置和过滤强度能分成几类。我习惯先扫一遍每关的输入回显在哪,再决定用哪套思路,而不是一关一关硬试。
| 关卡区间 | 典型特征 | 主要考点 | 打法方向 |
|---|---|---|---|
| 1~5 关 | 输入直接回显在 HTML 正文 | 最基础的标签注入 | 直接构造 script 或事件标签 |
| 6~10 关 | 回显在属性或特定标签内 | 闭合、大小写、编码 | 先看上下文再决定闭合方式 |
| 11~15 关 | 出现黑名单或关键字替换 | 绕过过滤 | 换标签、换事件、换编码 |
| 16~20 关 | 涉及 URL 编码、DOM、存储 | 编码层级与持久化 | 分层解码、盯 DOM 源 |
这张表不是标准答案,是我自己打下来总结的粗分类。它的价值在于:你拿到一关,先判断它属于哪一类,就能跳过大量无效尝试。新手最容易犯的错是拿着一关的 payload 去套下一关,结果全废。分类的意义就是让你知道“这一关考的是上下文还是过滤”,方向对了,payload 只是最后一步。
2.3 先学会看回显:上下文决定一切
打 xss 靶机,第一动作永远是看输入落在了 HTML 的哪个位置。落在标签之间、落在属性值里、落在 script 块里、落在 URL 参数里,四种情况的闭合方式完全不同。我一般会在输入框里填一个带特殊字符的标记串,比如aaa"'<>bbb,然后看源码里它被转义成了什么、被截断在哪。这一步比背一百个 payload 都管用。
举个具体判断:如果源码里看到value="aaa"bbb",说明双引号没被转义,你可以直接闭合属性再插事件;如果看到value="aaa"bbb",说明引号被实体编码了,硬闭合走不通,得换思路,比如看能不能跳出当前标签。这个“填标记串看回显”的习惯,我从第一关用到现在,是整篇里最值得先养成的一个动作。
3. 反射型关卡:从直接注入到闭合绕过的复现
3.1 最基础的三关:为什么你的 script 不弹
前几关的输入基本原样落在页面正文里,理论上<script>alert(1)</script>就该弹。但很多人卡住是因为浏览器对 script 标签的执行有前提:通过innerHTML插入的 script 不会执行,而服务端渲染进正文的会执行。所以如果你在本地用某种前端方式测试,可能永远看不到弹窗,换成直接请求页面就正常了。这不是靶场的问题,是插入方式的问题。
我一般先用最朴素的 payload 验证通路:
<script>alert(1)</script>如果这关不弹,先别急着换 payload,去看源码里 script 两个字是不是被吃掉了、尖括号是不是被转义了。前几关里有一关会把<script整体替换掉,这时候换大小写或者换标签才有意义。判断顺序永远是:先确认回显位置,再确认过滤动作,最后才改 payload。跳过前两步直接改 payload,就是瞎试。
3.2 属性上下文:闭合引号与事件标签
当输入落在属性值里,script 标签往往不生效,因为它在属性内部只是字符串。这时候要先把属性闭合掉,再引入一个能触发的事件。常见的思路是构造类似这样的结构:
" onmouseover="alert(1)它的逻辑是:前面的双引号闭合了原属性的值,onmouseover成为新的属性,等鼠标移上去就触发。这里有个细节很多人忽略——闭合用的引号必须和源码里包裹属性的引号一致,源码用双引号你就用双引号,用单引号就换单引号。我见过太多人栽在这个“引号不匹配”上,payload 本身没错,就是闭合错了层。
再进一步,如果事件属性也被过滤,可以尝试用onfocus配合autofocus,让元素一加载就触发,不依赖鼠标动作:
" autofocus onfocus="alert(1)参数说明:autofocus让元素自动获得焦点,onfocus在获得焦点时执行。这两个组合在属性上下文里命中率很高,前提是目标标签支持这两个属性,所以还是要回到“先看回显在哪个标签里”这一步。
3.3 编码层级:URL 编码和 HTML 实体不是一回事
打到中段关卡,会遇到输入被 URL 编码或被 HTML 实体编码的情况。这里最容易翻车的地方是把两种编码混为一谈。URL 编码(%3C这种)是传输层的,浏览器在解析 URL 参数时会自动解一次;HTML 实体编码(<这种)是渲染层的,浏览器渲染时解。你 payload 里该用哪种编码,取决于它最终落在哪一层被解码。
我一般的判断方法是:看源码里输入变成了%3C还是<。如果是前者,说明服务端在 URL 层面处理过,可能需要二次编码或者换注入点;如果是后者,说明实体编码发生在输出时,硬绕很难,得找有没有没被编码的输出位置。有一关的经典解法是把 payload 做 URL 编码后放进参数,让浏览器解码一次刚好还原成标签,这个“编码一次、解码一次”的对齐关系,是这类关卡的核心。
# 把 payload 做一次 URL 编码,用于参数层注入 # 原始:<script>alert(1)</script> # 编码后放进 URL 参数,浏览器解析参数时会还原上面这段不是让你照抄,是提醒你:编码不是越多越好,多编一层就多一层解码,对不上就全废。判断依据永远是“目标在哪一层解码”,而不是“我多编几次总有一次中”。
4. 存储型与 DOM 型:持久化注入和前端解析的坑
4.1 存储型:为什么刷新之后还在弹
存储型和反射型最大的区别是 payload 被写进了服务端,之后每次访问都会触发。这类关卡打起来反而简单,因为不用考虑一次性的参数传递,但排查起来更麻烦——你改了半天 payload 没反应,可能是旧的恶意内容还存着,新内容根本没覆盖上去。我踩过这个坑:以为 payload 写错了,其实是上一轮的存储内容还在生效,页面弹的是旧的那条。
处理办法很土但有效:每次改 payload 前,先把存储位置清一遍,或者换一个全新的输入标识。存储型关卡要盯的是“写入点”和“展示点”是不是同一个过滤强度,很多时候写入时过滤严、展示时过滤松,或者反过来。找到那个薄弱的一侧,payload 才有意义。
4.2 DOM 型:源码里没有,不代表没漏洞
DOM 型关卡的特点是服务端返回的 HTML 里根本搜不到你的输入,因为拼接发生在浏览器端。这时候看网络面板的响应是没用的,得看前端脚本怎么处理 URL 参数、怎么往页面里写。常见的危险写法是把location.hash或查询参数直接塞进innerHTML。
排查这类关卡,我一般打开开发者工具,在源码里搜innerHTML、document.write、eval这几个关键字,找到处理输入的那段逻辑,再反推怎么构造能让它执行。DOM 型最容易让人误判成“这关没漏洞”,因为服务端响应干干净净,实际上问题全在前端。判断依据从“看响应”切换到“看脚本”,这是打 DOM 型关卡必须完成的思维转换。
// 典型的危险拼接:把 URL 里的内容直接写进页面 // 排查时重点看这类赋值语句的右侧来源是否可控 var input = decodeURIComponent(location.hash.slice(1)); document.getElementById('out').innerHTML = input;这段代码的关键在最后一行:innerHTML赋值不会执行插入的 script 标签,但会执行事件属性,所以 DOM 型关卡里事件标签往往比 script 标签更好用。参数说明:location.hash取的是 URL 井号后的内容,decodeURIComponent解一次编码,如果你的 payload 在井号里被编码过,这里刚好还原。理解这条链路,比记住某个 payload 有用得多。
4.3 存储加 DOM:两层叠加时先修哪一层
有些关卡是存储和 DOM 叠加的,输入被存下来,又在前端被拼接。这种关卡最容易让人崩溃,因为你不确定问题出在写入过滤还是前端拼接。我的做法是分层验证:先确认存进去的内容原样是什么(看存储后的展示),再确认前端拿到这个内容后做了什么处理。两层分开看,问题就定位了。
如果存进去时已经被转义,那前端再怎么拼也执行不了,得先解决写入层的绕过;如果存进去是原样、前端拼接时被处理,那就攻前端。最怕的是一上来就改 payload,改了半天不知道改的是哪一层。分层验证这个习惯,是我从这类关卡里被反复教育出来的血泪经验。
5. 避坑与排查:打 xss 靶机最常见的五个翻车点
5.1 现象:payload 明明对,就是不弹
原因:最常见的是浏览器缓存或存储残留,页面加载的是旧内容;其次是 payload 落在了不执行的上下文,比如innerHTML里的 script 标签。解决:先强制刷新并清掉存储,再回到源码确认回显位置,确认插入方式是否支持执行。别在没确认上下文的情况下反复改 payload。
5.2 现象:源码里输入被转义成实体,怎么绕都绕不过
原因:实体编码发生在输出层,且编码函数覆盖了关键字符,硬绕基本无解。解决:换注入点,找同一页面里没有被编码的输出位置,或者看有没有二次解码的环节。判断依据是“编码发生在哪一层”,而不是“再试一个 payload”。
5.3 现象:URL 编码后放进去,反而多出一层乱码
原因:编码次数和解码次数没对齐,多编了一层。解决:数清楚目标在哪一层解码,只编到那一层为止。我一般先不编码直接放,看源码里变成什么,再决定要不要编、编几次。这个顺序能避免大部分编码翻车。
5.4 现象:事件标签写了,鼠标移上去也没反应
原因:属性闭合用的引号和源码不一致,或者目标标签不支持该事件。解决:回源码看包裹属性的引号类型,确认标签类型,再选支持的事件。onfocus配autofocus在多数标签上更稳,但也要看标签是否支持。
5.5 现象:存储型关卡改了 payload,页面还是旧行为
原因:旧的存储内容没清,新内容没覆盖,或者写入点和展示点不是同一处。解决:换全新标识重新写入,确认存储位置,分层验证写入和展示两侧的过滤强度。这个坑我踩过不止一次,后来养成“改之前先清”的习惯,省了很多冤枉时间。
6. 打通关之后:把靶场经验变成可复用的判断力
打完这二十来关,真正的收获不是攒了一堆 payload,而是形成了一套固定的判断顺序:先看回显位置定上下文,再看过滤动作定绕过方向,最后才写 payload;编码问题先数清解码层级,存储问题先分层验证。这套顺序放到真实场景里同样成立,因为浏览器解析和服务端过滤的基本规律不会变。靶场只是把这些规律压缩成了一条关卡链,让你在低成本环境里反复撞墙。
我一般会建议打完的人做一件事:把每一关的“回显位置、过滤动作、最终 payload”记成三列,回头翻的时候你会发现,很多关卡的差别只在其中一列。这个记录习惯比收藏一百篇 payload 合集有用,因为它逼你看清因果,而不是记住结果。新手照着这份思路能一关一关推下去,熟手能借它补上自己一直靠试错蒙过去的那部分底层逻辑。
回过头看,我最早打靶场最大的教训就是“重 payload、轻上下文”,看到不弹就换 payload,换到第十个才想起来看源码。从那以后我每次开一关,都强制自己先填标记串、看回显、定上下文,三步走完再动手。这个习惯后来帮我省下的时间,远比多背几十个 payload 多。这份靶场资源适合拿来当练手的起点,搭起来、按分类打一遍、把翻车点记下来,比单纯通关更有价值。希望帮到你。
本文还有配套的精品资源,点击获取