1. 靶场到底在练什么:先看懂XSS-labs的关卡地图
XSS-labs(也有人叫XSS挑战靶场)是我接触过的Web安全练习环境里,最适合用来系统刷一遍XSS技能树的平台。界面确实朴素——一排带序号的关卡,每条输入框构造出不同的过滤场景,但正是这种朴素,让每一关的考察点特别清楚。你提交一个payload,页面的表现只有三种:弹窗、不弹窗、报错。没有多余干扰,这对理解XSS漏洞的本质非常有帮助。
靶场总共20关左右,难度并不是线性上升,而是按“过滤手段”分组递进的。前几关基本不设防,目的是让你体会XSS能出现在哪些输出位置;中间关卡开始引入标签过滤、关键字替换、编码绕过;后半段则涉及HTML实体、空格过滤、DOM型注入等更贴近真实业务场景的组合条件。刷完一套题,你基本能把反射型XSS的常见匹配规则、浏览器解析机制、各类绕过思路都过一遍。
我把整个靶场的考点整理成了一张表,方便你在做题之前先建立整体认知:
| 关卡范围 | 主要考点 | 输入输出特点 | 突破思路核心 |
|---|---|---|---|
| Level 1-5 | 无过滤、属性闭合 | 输入直接拼接到HTML标签内 | 先用无害值观察输出位置,再构造闭合 |
| Level 6-10 | 大小写过滤、事件属性 | 过滤不区分大小写或只过滤单个关键字 | 利用多种标签与大小写混合绕过过滤 |
| Level 11-15 | 请求头注入、DOM上下文 | 输入位置在Referer、UA、Cookie等头字段 | 找到输出位置,在HTTP头字段中构造payload |
| Level 16-20 | 空格过滤、正则替换、编码绕行 | 输入被多层处理,payload需还原原始意义 | 掌握HTML实体、URL编码、解析顺序,组合技绕过 |
这张表看着简单,但它解决了一个关键问题:做题时最容易犯的错,就是拿到输入框就盲目贴一个经典payload,弹不出窗就开始换下一个,完全不看页面给了你什么信息。XSS-labs的每一关其实都在暗示你“这条过滤是怎么写的”,你要做的是根据题目特征推断过滤规则,而不是拿payload试错。读靶场,重点是学会建模:输入在哪个位置?输出是否被转义?转义发生在服务端还是前端?过滤规则是删标签、换关键字还是改编码?把这些问题回答清楚,payload的构造就有了依据。
2. 先垫三层基础:XSS原理、payload构成与浏览器解析
2.1 三种数据流决定三种XSS类型
在开始刷关之前,我建议你先搞清楚XSS的类型划分,否则后面做题容易乱。
反射型XSS指的是,服务端把用户输入直接拼进返回的HTML里,浏览器解析时执行了你的脚本。靶场大部分关卡都是这种类型,特点是payload只影响当前请求的响应。存储型XSS则是输入被存到了服务端数据库,其他用户访问时也会触发,靶场里体现得不太明显,但你在真实业务中遇到评论、留言功能时要重点排查这里。DOM型XSS跟前两者完全不同,服务端完全没参与,是前端JavaScript拿到输入后,把数据写进DOM导致执行。XSS-labs后几关会涉及这种场景,你需要直接看浏览器的开发者工具,去观察输入被脚本处理后的位置,而不是只看源码。
对做题而言,类型判断决定了你排查问题的方向。如果是反射型,抓响应包改请求就能定位;如果是DOM型,必须断点调试JavaScript才能找到漏洞触发点。
2.2 payload不是唯一,能执行才是核心
很多初学者喜欢背一堆经典payload,比如<script>alert(1)</script>、<svg onload=alert(1)>,但我不建议一上来就背。你先拆解一个payload的组成,理解它为什么能执行:
- 标签本身:
<script>是合法script标签,<svg>可以加载外链脚本,<img>、<a>等标签本身不执行JS,但配合事件或属性可以。 - 闭合符号:当输入被放在标签属性值里时,你必须先闭合掉前面的引号和标签,才能注入自己的标签。比如输入被放在
<input value="你的输入">中,你的payload就要以"开头,再跟上>来结束标签,后面才能接新标签。 - 事件属性:
onmouseover、onfocus、onerror这类属性可以直接触发JavaScript执行,通常不需要额外标签。 - 伪协议:
javascript:、data:这类URL协议可以在特定位置执行脚本,但会受到浏览器策略限制。
2.3 浏览器解析顺序,才是绕过的底层逻辑
掌握解析顺序,你能做到“不用背payload也能现场造一个”。简单说,浏览器拿到HTML响应后,要经历多个阶段的解析:
- HTML解析阶段:浏览器把标签、属性、正常文本分别归类。如果你输入
<img src=x onerror=alert(1)>,浏览器会识别出一个img标签,并在加载失败时触发onerror。 - URL解析阶段:如果某个属性的值是URL,浏览器会再次按URL规则解析,比如
href="javascript:alert(1)"。 - JavaScript解析阶段:当标签被识别为可执行脚本时,浏览器才运行JS引擎去解析代码字符串。此时引号、反斜杠、换行等字符都有特殊含义。
一个payload能不能成功,关键看你输入的字符,在它经过每一层解析时,是否会因为编码、转义、过滤而失去原有语义。这也是为什么有些payload看起来和过滤规则“擦边”,却能弹窗。
举个生活化的例子:你给收件人寄快递,地址写错了没关系,只要快递员能看懂并最终把包裹送到正确的人手里就行。XSS也是这个逻辑,你的payload经过层层处理,只要最终能到达可执行的位置,就算成功。
3. 逐关实战:解题思路与可复现payload整理
这一部分我按实际闯关的顺序,把每一关的难点和最终payload整理出来。需要提前说明:以下payload全部用于XSS-labs靶场本地环境验证,请勿对任何未授权的互联网网站使用。
3.1 Level 1-5:从无过滤到属性闭合
Level 1考察的是最基础的反射型XSS。你在输入框提交任意字符串,刷新后能看到它直接出现在页面上。第一步不是直接打payload,而是先输入一个无害值,比如test,然后看页面源码,确认输入被放在了什么位置。确认是原样插入页面后,直接提交:
<script>alert(1)</script>如果弹窗出现,说明靶场验证通过。此时你该意识到:无过滤场景中,script标签是优先级最高的选择,但真实业务很少这么裸奔,很多WAF会拦截它。
Level 2开始有过滤了。输入test后查看源码,你会发现输入被放进了<input value="test">这样的属性里。这里有个关键认知:你怎么知道它在属性里?开发者工具里看DOM结构,输入内容是否被引号包住,旁边有没有value=前缀。确认之后,payload要闭合引号和标签:
"><script>alert(1)</script>第一个双引号用来闭合value属性值,紧接着的>结束input标签,后面注入新标签。这里如果不先闭合,你注入的标签会被整体吞进value属性里变成纯文本,永远不会执行。
Level 3同样把输入放在value属性中,但前面加了个HTML实体编码处理,你把"提交上去,源码里看到的是",也就是说引号被转义成实体了。这时直接闭合引号没用,因为服务端把你输入的引号变成实体,浏览器解析后引号只是文本,不会闭合属性。这一关真正要考察的是:能不能找到绕过HTML实体编码的路径。经过实测,如果输出位置是在input标签的value属性值中,且没有任何检测,你依然可以尝试:
' onmouseover='alert(1)看题目的具体过滤逻辑。Level 3的设计其实是把输入包在双引号里,单引号不会被转义,所以闭合的是单引号场景——你没看错,这关输入点是在单引号包裹的属性里。做题时务必先看源码确定引号状态,再决定payload用什么符号。
Level 4会过滤<script>标签,大小写也被处理过,但过滤规则不完整。试探时提交<SCRIPT>alert(1)</SCRIPT>,大概率能成功。原理很简单:过滤规则可能只匹配了全小写的<script>,没有做大小写归一化。碰到这类过滤,先试大小写变体,成本最低。如果小写、大写都不行,再考虑用其他标签替代,比如<svg onload=alert(1)>。
Level 5过滤了<script>、onerror等字样,但允许你使用src属性。提交一个<script src=...>可能会发现script关键字被删,所以换思路。可以构造一个a标签链接到外部JS(本地起一个简单的HTTP服务承载js文件即可),但靶场环境没有外网文件时,更推荐直接换可执行点:
<svg onload=alert(1)>把重点从script标签切换到事件属性上。Filter拦你的事件名时,再考虑其他事件或者编码。
3.2 Level 6-10:大小写绕过与关键字替换的攻防
Level 6的过滤规则里把script、on、src等关键字都过滤了一遍,但没禁用大小写混合写法。我提交的是:
<sCrIpT>alert(1)</sCrIpT>直接就弹窗了。这一关的启示是:很多开发写正则时没有加i标志(忽略大小写),或者只拦小写,大小写混合是绕过该类过滤的最基础手段。
Level 7把script、on、src这些关键字直接删掉,而且删得比较粗糙。这里有个非常经典的绕过思路——利用删除后的字符拼接。举个例子,如果你提交<scr<script>ipt>,过滤删除掉中间的<script>后,剩下的字符正好拼成<script>。这是“抽词拼接”类绕过,现实中很多过滤确实能用这个思路打穿。
但更稳定的做法是换一个不常见但可执行的标签。我实测可用的是:
<img src=x onerror=alert(1)>如果连onerror都被覆盖删除,就尝试将onerror写成onerror的某个变体,或者改用其他事件。把备选事件池准备充分是很重要的基本功。
Level 8是相对于前几关的一个转折点。输入被拼接到<a href="你的输入">中,并且把javascript关键字的出现做了适配。如果你直接提交javascript:alert(1),会看到javascript被替换成java\0script之类的东西,导致无法执行。
这时候可以用字符实体绕过匹配:比如将javascript中的某个字母换成HTML实体编码形式,就让正则匹配失灵、浏览器却按字符解析成功。不过在href这种URL上下文中,先尝试大小写绕过更稳:
JavaScRipt:alert(1)如果也会被大小写归一化,就试下面的编码写法(在本地验证环境中,将字符串进行实体编码后粘贴进去):
javascript:alert(1)因为浏览器在解析href属性时,会先把HTML实体解码成原始字符再进行URL解析。而服务端的正则拿到的可能是原始字符串,恰好错过一次匹配时机。
Level 9在Level 8基础上要求输入中必须包含http://字样,否则不会进入下一环节。我最初的思路是构造一段注释包裹伪协议,例如:
javascript:alert(1)/*http://*/新增的部分是/*http://*/,用CSS/JS注释的方式让http://只作为注释文本存在,不影响前面的JS执行。这类“塞垃圾数据满足正则”的绕过思路,在真实业务里也很常见,毕竟很多校验只是机械地检查字符串是否包含某个子串,根本不管这个子串的上下文。
Level 10有个小陷阱,表面输出一个隐藏的input框,你的输入会被放进隐藏域。盲打XSS时看不到输出位置,但源码中能找到。核心操作是:根据源码确定参数名,尝试注入额外参数覆盖原值,让隐藏域变成可触发事件的元素。实际提交时可以考虑:
?t_sort=1" autofocus onfocus=alert(1) x="通过autofocus加onfocus,让浏览器进入页面时就自动聚焦并触发事件,不需要用户交互。这一点在真实XSS中价值很大,因为你不可能要求每个受害者都去移鼠标。能降低交互成本,就是提高攻击稳定性。
3.3 Level 11-15:请求头字段里的XSS与DOM型初探
Level 11考察的是HTTP头字段的反射。输入点不是表单,而是Referer头。你需要用Burp Suite或者浏览器开发者工具修改请求的Referer值。先正常发送请求,确认页面中有一个hidden input的value来自Referer,你只需要给Referer填入无害值看回显,然后构造:
"><script>alert(1)</script>如果过滤不严就能直接弹窗。这关的核心是提醒你:输入点并不只在页面表单里,HTTP头字段同样是反射数据源。做渗透测试时,遇到过很多安全运维只关注body参数、忽略请求头的情况。
Level 12和Level 11同理,只不过反射点从Referer换成了User-Agent。UA头有长度限制,但这么短的payload完全没问题。用开发者工具把UA改成payload后刷新即可。这里我踩过一个坑:浏览器或某些代理组件会自动给请求头添加额外字符,但靶场里基本不受影响。做题时注意看清楚参数名再改。
Level 13是Cookie反射。你设置一个自定义Cookie字段,例如user,值为攻击脚本,页面里就会把这个Cookie值输出到某个位置。因为有多个Cookie字段时,过滤逻辑和拼接位置可能不同,建议先设置一个普通值抓包确认对应关系。
Level 14更偏向于外部资源引用。靶场里大致是要求你通过一个URL参数加载外部图片或外链,如果目标允许http://并拼到img src中,可以尝试拼接事件。不过这类关卡依赖外网环境,做题受限时建议直接把思路记录在笔记里,重点理解“URL参数可能控制src”的漏洞模式就行。
Level 15是很多人卡住的一关。它采用了某个前端模板引擎(在知名靶场中常见的是AngularJS的ng-include指令),你可以通过请求一个自定义URL,将参数作为模板路径,最终把指定文件的内容包含进来。观众能成功弹窗的方式通常是把请求换成符合题目要求的URL。如果环境无法运行,也不影响你理解:这本质上是前端模板注入类XSS,跟普通服务端拼接HTML完全不同。你需要在开发调试里看清哪个指令在处理你的输入、它支持哪些外部资源引用协议,然后构造含有恶意内容的资源地址。
3.4 Level 16-20:空格、正则与编码组合,把payload打磨到极限
Level 16过滤了空格,这是一道分水岭。常规payload里到处都是空格,一旦空格没了,整个标签结构会直接失败。你需要找到替代空格的字符。最常见的方法是使用换行符%0a或回车符%0d,还有/和Tab字符%09。在HTML解析中,很多空白字符都能起到分隔属性的作用。我最终通过的payload类似:
<svg%0aonload=alert(1)>如果你在浏览器地址栏做编码时没成功,就换用Burp或开发者工具直接改请求体发送原始字节。编码是否被二次解码,直接决定了%0a能不能变成真正的换行。
Level 17和Level 18的考点比较类似,输入会被拼到两个HTML属性的拼接处,一个出现在标签名位置,一个出现在属性值位置。突破关键还是通过闭合前面的标签与引号,再注入自己的事件属性。我用的payload是:
"><img src=x onmouseover=alert(1)>不过事件触发方式容易受交互限制,所以你在本地验证时,哪怕只是移动到图片上就弹窗也算正确,不需要被“必须点击”误导。
Level 19和Level 20比较复杂。网上很多教程会说这关基于Flash的跨域配置,听起来跟普通XSS没关系。就做题环境而言,现在大部分浏览器都不再默认加载Flash,因此这两关更多是“读代码理解思路”而非实际弹窗。如果你是为了面试或实际防御,建议重点看前面的反射型关卡;如果你是打CTF,也可以尝试用其他标签替代,但价值有限。我个人的习惯是:把这两关的原理笔记保存下来,不做无谓耗神。
4. 做题卡壳时怎么办:排查流程与高频坑位
4.1 先确定真实渲染结果,再判断为什么失败
卡关时,很多人的第一反应是换payload,但我推荐你先打开浏览器的开发者工具,切到Elements面板,查看输入位置的实际DOM结构。比如你提交一个",页面源码里它变成",那说明服务端做了HTML实体转义;如果开发者工具里显示你的引号已经成功闭合了标签但还是不弹窗,那问题可能出在JavaScript解析层,比如引号截断逻辑不对。
我遇到太多人,提交一个<script>alert(1)</script>不弹窗,就直接认定是WAF拦截,完全没意识到自己的输入其实是被放在某个标签的属性值里了,脚本根本没有执行上下文。定位到真实DOM结构,很多问题能当场解决。
4.2 高频报错与对应原因速查
| 现象 | 可能原因 | 下一步操作 |
|---|---|---|
| 脚本不弹窗,但源码中能看到输入的标签 | 输入被当成文本,未进入可执行HTML节点 | 看DOM上下文,补闭合引号和标签 |
标签源码变成<、"等实体 | 服务端做了HTML实体转义 | 尝试大小写、编码、换标签来绕过过滤规则本身 |
javascript:开头的href不行 | 关键字被过滤或浏览器策略限制 | 使用HTML实体编码、外部JS引用等替代方案 |
| 空格被过滤后整个payload失效 | 过滤了空格字符 | 用%0a、%0d、Tab、/替代分隔符 |
| 提交后页面源码没变化 | 输入点不在GET参数,可能在请求头或DOM逻辑中 | 检查Referer、UA、Cookie字段的渲染位置,或打开Network面板 |
4.3 刷完靶场后,我沉淀下来的三条做题习惯
第一条,每次只改动变量中的一项。比如先改闭合方式,再改标签种类,最后改动编码方式。如果你同时改了三个东西,成功了也不知道哪个变量起作用,真的会浪费大量时间。
第二条,用Burp或浏览器开发者工具直接看请求,而不是只改输入框。因为某些输入点会被框架二次编码,你在输入框里看到的字符串,和实际发送到服务端的字节不一样。看请求体才能确认最终形态。
第三条,把每个过关的payload记录下来,并附上“为什么这个能过”。不要只存一份最终答案。我自己的笔记里会写“这一关是过滤了onerror,所以用了onmouseover,且属性前用/代替空格”。一旦养成记录原因的习惯,下次碰到类似场景,你甚至可以从笔记里反推过滤规则,而不是每次都从头试起。
5. 实战之外:学习XSS的正确姿势
把XSS-labs刷完,你只是建立了基本的XSS攻击面认知,距离真正的实战还有很大的距离。我做这类靶场练习已经比较长的时间,个人最有价值的体会是:靶场是在帮你建立“输入点-处理过程-浏览器解析-触发执行”的思维模型,而真实业务里的XSS远比这些关卡复杂。
你需要额外补的知识点包括:CSP策略如何影响XSS利用(如果响应头里带着Content-Security-Policy,即使你成功注入了标签,也可能被策略拦截而无法执行);HttpOnly与Cookie的关系(HttpOnly能防止脚本读取Cookie,但不是XSS的根本防御手段);浏览器自带XSS保护机制在哪些场景会自动拦截部分payload;以及服务端过滤的常见实现方式(黑名单、白名单、正则替换、HTML实体处理)。
如果你还想继续往下深化,建议做这几件事:把XSS-labs与Burp Suite联动,自己尝试抓包分析每一次请求,观察payload在被服务端处理前后发生了哪些变化,这是任何教程都替代不了的“手感”训练。用靶场加深对DOM型XSS的理解,尤其是前端JS对location.hash、document.referrer、window.name这些数据源的处理逻辑,这部分在真实前端框架里经常出现。最后一个建议是,把你收集到的payload整理成自己的笔记库,按“绕过场景”分类,而不是按“第几关”分类,以后碰到新场景可以快速查阅。
我个人做题时养成的一个小习惯是:每过一关,就在本子上画一遍“我的输入经过什么过滤、在什么位置触发、为什么能执行”。画得多了,很多能力就在不知不觉中长在身上。希望这篇心得能帮你在XSS这条路少走一点弯路。