☰
XSS十八种绕过技巧实战复盘:从alert(1)到在线靶场通关之路
2026/10/21 16:55:30 网站建设 项目流程

玩XSS的人估计都绕不开那道经典的“alert(1)”。从刚开始在输入框里打出<script>alert(1)</script>然后看着页面弹窗的那一刻起,才算真正摸到了Web安全的大门。但等你多刷几道题、多看几个环境就会发现,现实里的XSS从来没有这么温顺——过滤、拦截、长度限制、字符黑名单,各种机制轮番上阵,逼着你从“能弹窗”进化到“想方设法也要弹窗”。

这篇东西就是把我在某个公开在线靶场里刷XSS训练关卡的全部过程复盘了一遍。它不是给你念漏洞定义,而是直接告诉你:一个RAW输入点,从最笨的alert(1)开始,到后面十八种绕过思路是如何一步步被逼出来的。其中穿插着不少当时踩过的坑、想歪的思路,以及最后怎么绕过去的过程记录。如果你正准备系统性搞定反射型、存储型和DOM型XSS,或者想找一份可以直接对着练的实战路线,这篇文章可以帮你省掉不少瞎试的时间。

1. 内容整体设计与思路拆解

1.1 为什么选在线靶场而不是本地环境

很多人上来就折腾本地漏洞平台。搭环境本身不算难,但要额外花时间处理数据库、PHP版本、扩展之类的问题,有些老代码在当前系统上跑起来一堆报错,等环境配完已经没心情刷题了。我用的那个在线靶场最大的好处是关卡隔离、题目明确、难度递增,而且可以直接在浏览器里操作,省掉了本地的环境维护成本。

当然本地环境也不是没有价值——它的优势是可以自己加代码观察执行细节,适合深入调试。但对大多数初学者来说,在线靶场的题做得更快,反馈更直接,你输入什么、输出是什么、被过滤了什么,一眼就能看到。

用在线靶场还有一个容易被忽略的好处:题与题之间的难度跨度设计得比较科学,它不会第一题就让你做深度绕过,也不会一直停留在改改大小写的水平。每道题都有明确的“通关目标”,比如弹窗、获取Cookie、执行特定函数。这种带着目标去试错,比漫无目的地乱插payload高效得多。

1.2 这个训练的核心分层逻辑

整套训练被我拆成四个层次,每个层次对应不同的思维模式:

  • 第一层:无过滤或极弱过滤,目标是理解XSS的触发链路,搞明白“输入在哪、输出在哪、怎么闭合”。
  • 第二层:标签和关键字被过滤,这时候就需要变换标签、变换事件、变换触发方式。
  • 第三层:编码和长度被限制,需要利用各种编码规则和短payload来对抗正则匹配。
  • 第四层:上下文复杂化,比如输出点在标签属性里、在JavaScript变量里、在CSS里,这时候不是“绕过”的问题,而是“怎么在对应上下文里合法执行代码”的问题。

这四个层次其实模拟了真实场景中XSS从简单到复杂的全过程。刷题的时候强烈建议按照这个递进关系去理解,不要只记答案,要记背后的原理。

1.3 用什么标准衡量自己过关了

我的衡量标准很简单:不用工具,纯手工能不能完成理解原因、构造payload、验证执行、总结原理这四步。哪怕只有一个payload能用,只要你说不清楚它为什么能用、过滤为什么会失效,那就等于没过关。

真正刷题的时候,我都会强制自己多问几个问题:

  • 过滤规则是什么?是关键字黑名单、白名单,还是正则替换?
  • 输出点在哪里?HTML标签内、属性内、Script代码块里、还是URL中?
  • 编码处理发生在哪个环节?输入时转义,还是输出时转义?
  • 只需要绕过单层过滤,还是需要绕过多层防护?

这四个问题构成的思路链条,比任何单个payload都值钱。

2. 核心细节解析与实操要点

2.1 先吃透XSS的触发链路

XSS的本质就一句话:用户输入被当成了代码执行。只要输入能控制输出的上下文,就有可能形成XSS。无论反射、存储还是DOM型,最终都是这个逻辑。

拿最经典的反射型来说,用户提交的参数在URL里,服务端接收后未经过滤或转义就直接拼接入HTML响应中。你提交?keyword=<script>alert(1)</script>,页面就在那个位置原样输出了一段可执行脚本。整个过程经过了“参数提交 → 服务端拼接 → 浏览器解析执行”三条链路。

DOM型和反射型的区别在于,服务端可能没有直接参与拼装,而是前端JS从URL或存储中读取内容,再用innerHTML、document.write、eval等方式写进页面。这种情况下,后端日志可能一片干净,但前端已经中招了。

存储型则是把payload持久化在服务端(如留言、评论、昵称),任何一个用户只要访问到渲染该内容的页面就会触发。攻击半径更大,危害程度也更高,训练时尤其要体会三者之间“持久性”和“触发源”的区别。

2.2 alert(1)为什么是通关标准

很多没接触过XSS训练的人会问:为什么每道题都要alert(1),弹alert(document.domain)不行吗?答案其实很简单——alert是一个足够短、足够标准、能被各种过滤规则针对性测试的触发原语。

大多数过滤脚本会专门把alert列入黑名单,这就迫使你思考如何绕过关键字检测。如果用console.log,很多浏览器的控制台输出不够显眼,不方便快速判定执行成功;如果用fetch,代码太长,会被长度过滤直接拦掉;用prompt和confirm虽然视觉反馈更弱些,但在某些过滤规则下确实可以作为alert的替代品。

另外alert(1)这个字符串本身很短,很多长度限制关卡(比如限制输入50个字符、25个字符)只有短payload才能过关。从alert(1)出发,才能逼出你构造“极短无效字符”和“极短事件触发”的能力。

2.3 分析绕过入口的通用方法

不管在哪道关卡,先不要急着试payload。找到输入点之后,按这套方法走:

  1. 先在输入框提交一个无意义但独特的标记,比如zzyzx123。
  2. 看页面源代码,确认这个标记出现在什么位置——是直接出现在<p>zzyzx123</p>、出现在<input value="zzyzx123">、还是出现在<script>var x="zzyzx123"</script>。
  3. 推测过滤逻辑:标记有没有被转义?被转义的是尖括号还是引号?有没有被过滤掉部分字符?
  4. 根据输出上下文,选择闭合方式,构造payload。

这个方法的关键在于:先定位、再推断、后构造。盲目粘贴网上的一大堆payload不是不行,但很难理解为什么有的能用、有的不能,最后变成碰运气。

3. 实操过程与核心环节实现

3.1 第一关:基础<script>alert(1)</script>直出

这关几乎没有过滤,最简单的payload直接就能过。但是别高兴太早,确认两件事才算有收获:

第一,页面对尖括号、单引号、双引号有没有做编码处理?如果输出的位置在<script>标签内部,就不需要拼标签,直接闭合变量再执行就行;如果输出位置在标签属性内部,只需闭合引号和标签即可。

第二,确认有没有长度限制。有的关卡虽然不限制标签,但限制长度,这时候<script>alert(1)</script>这种24个字符的payload常常能过,但一旦长度限制在20以内就会立刻失效。

一个常见的实操细节是:你输入的<script>标签中间如果刚好被别的属性分隔开来,比如变成<script src=x>这种,很多设置不当的过滤仍然会放行,后面再接上alert(1)</script>还是能执行。这也是为什么不要只盯着“整段过滤”,要看“分段过滤”的情况。

3.2 从标签逃逸到事件触发

第二类关卡开始过滤<script>标签,常见的对策就是改用事件触发标签替代。比如<img src=x onerror=alert(1)>,原理是:图片加载失败触发onerror事件,事件处理器执行JavaScript代码。src=x就是一个不存在的地址,必然加载失败。

这里的常见坑点有三个:

  • 过滤规则可能是大小写不敏感的,也可能是大小写敏感的。如果是后者,<ImG sRc=X oNeRrOr=alert(1)>就能轻松绕过。
  • 如果过滤了onerror,可以换成onload(但需要图片能成功加载,所以src要填一个真实存在的图片地址)、onmouseover(但需要交互)、onfocus(需要配合autofocus)。
  • 标签不止img一种。<svg onload=alert(1)>是经典,<body onload=alert(1)>在部分场景下也能用,<details open ontoggle=alert(1)>适合无图片环境的场景,<video><source onerror=alert(1)>在富文本场景下很实用。

真正到了复杂关卡,事件触发方式的选择逻辑是这样的:优先选择无需用户交互的事件(onerror、onload、ontoggle),实在不行再考虑需要交互的事件(onclick、onmouseover、onfocus),并且在构造时尽量让触发条件自动满足。

3.3 编码类绕过的关键细节

编码绕过是这套训练里最让人头疼但也最让人长本事的一部分。过滤正则大多是按明文关键字来匹配的,所以把你的payload改写成另一种编码形式,就有可能躲过匹配,而浏览器在执行时又会自动解码回原始语义。

先说HTML实体编码。当输出点位于HTML标签内部或标签之间时,某些字符会被浏览器自动解析为对应符号。例如&#x3C;script>在HTML解析阶段会被还原成<script>,如果过滤脚本只在字符串层面查找<script>而没做HTML实体解码,这条payload就会直接命中。

但要注意一个关键前提:HTML实体编码只在HTML上下文中生效。如果输出点是在<script>代码块里,实体编码不会被JavaScript解析器还原,反而会变成语法错误。这一点是很多人在刷题时反复踩坑的原因。

然后是JavaScript的Unicode转义。在JS字符串中,\u0061lert(1)等价于alert(1),因为JavaScript解析器会先识别Unicode转义再执行。如果过滤规则匹配的是alert这个字符串,而你的输入在进入JS解析器之前压根没有出现完整的alert字样,就能躲过关键字匹配。

还有一个经常被忽视的是URL编码。当输出点在href属性里时,javascript:alert(1)中的字符可以先URL编码再让浏览器解析,比如javascript&#58;alert(1)在某些场景下就能生效。这个需要结合具体的过滤顺序来分析——是先编码后过滤,还是先过滤后编码,不同的顺序决定了你能不能用编码绕过。

实操中我的建议是:多做“双重编码”测试。比如先将<编码为%3C,再将%编码为%25,提交%253C,如果后端只解码一次而浏览器又解码一次,就能形成二次编码绕过。真实环境中这种案例不少,训练时不专门练一下很容易漏掉这个思路。

3.4 引号与括号的逃逸艺术

在输出点位于属性内或JS代码块内时,引号和括号的闭合是整个payload构造的关键。

举个例子:输出点在<input value="这里">内部,你提交123" onmouseover="alert(1),渲染出来的HTML就会变成<input value="123" onmouseover="alert(1)">。这里的关键是先用一个"闭合原来的属性值,然后插入新的属性,再用一个"包裹后续内容让它不破坏整体结构,这就是最基础的属性逃逸。

如果过滤规则把双引号转义成&quot;,单引号转义成&#39;,那就可以试试不需要引号的事件写法。onmouseover=alert(1)这种写法在HTML属性里完全合法,因为属性值可以不加引号——对于长度过滤很严格的情况,这招非常实用。

如果过滤了括号,事情就麻烦一些了。alert(1)里的(和)一旦被过滤,最简单的替代方案是用反引号包裹函数参数:alert在某些JavaScript引擎中支持反引号模板字符串调用,能到达差不多的执行效果。有些时候也可以借助throw语句:onerror=throw alert这类写法在极少数场景下能用,但兼容性一般,不建议作为首选。

3.5 长度限制下的短payload构造

有些关卡把输入长度控制在极短的范围内,比如20字符、25字符,这时候完整写一个<script>alert(1)</script>肯定不行。短payload的构造思路是:用尽可能少的字符实现“标签 + 事件 + 函数调用”三个功能。

经典的短payload比如<svg/onload=alert(1)>,总共才20多个字符。这里/不是标签内容,而是自闭合标记,onload事件在SVG加载完毕时触发,省去了闭合</svg>的字符开销。还有<img src=x onerror=alert(1)>,但在长度极短时连这个都嫌多。

更极限的可以写成<svg/onload=alert(1)(最后一个>在某些浏览器渲染中可以不写),或者用<a href=javascript:alert(1)>截断到极致。这个需要在不同浏览器里挨个测试,因为浏览器的容错解析规则并不完全一致。

我实际刷题时发现,长度限制关卡反而最能训练思维压缩能力。它迫使你砍掉所有不必要的字符,留下最核心的执行链路,同时也要充分考虑浏览器的容错特性。以后在真实场景里遇到WAF限制请求体大小的情况,这种能力会特别有用。

3.6 存储型XSS的持久化操作

存储型XSS和反射型最大的区别在于:payload被服务端持久化存储下来,任何访问该页面的用户都会中招。训练环境里的存储型,一般做成留言板或评论区的形式。

存储型XSS的难度通常体现在两个方面。第一,输入输出之间可能隔了多层处理,例如输入时可能做了htmlspecialchars转义,输出时又套了一层过滤函数,两层叠加之后原本的payload可能就被彻底无害化了。第二,存储型每次刷新页面都会重新触发,调试起来不容易区分“这次为什么没触发”和“上一次的缓存是不是影响结果”。

实操时建议每次测试都携带一个唯一标记,比如xss-test-9527,再配合一个基础payload。刷新页面后先看标记是否还在、是否被转义、出现在什么位置,再决定下一步的绕过方向。这个过程相当于把存储型XSS的整条“写入-存储-读出-渲染”链路看个透。

3.7 DOM型XSS的独特绕过思路

DOM型XSS跟前两者在思路上差异很大。后端可能根本没动你的输入,过滤也未必发生在服务端,而是前端JS代码把location.hash、document.referrer、window.name这些不可控来源的数据直接塞进了innerHTML、outerHTML、document.write或eval里。

训练环境通常会把DOM型关卡设计成“URL片段里有一个参数,前端读取后直接拼进页面”。location.hash有个特点:它不会发送到服务端,因此服务端日志里什么都看不到,服务端过滤也拿它没办法。所以DOM型最常见的场景是:后端一切正常、无任何过滤,但前端代码写得糙,直接就把可控数据当HTML渲染了。

DOM型最经典的绕过场景是:前端代码用了innerHTML。innerHTML不会执行<script>标签,所以很多人会下意识说“innerHTML是安全的”——大错特错。innerHTML虽然不执行script标签,但图片的onerror、SVG的onload、iframe的srcdoc等都可以照常触发。训练时最好把<svg/onload=alert(1)>这类payload在DOM型关卡里多测几遍,感受一下“标签不行不等于事件不行”的差异。

3.8 十八种绕过技巧的完整拆解

这部分是整个训练的核心清单,我按实际刷题时踩过的顺序把它列一遍,每一条都会说明原理和适用场景。

  1. 大小写混合:<ScRiPt>alert(1)</sCrIpT>。适用于过滤脚本只做了精确匹配或只针对小写字符串的情况。

  2. 空格变体:用/、Tab、换行、回车符替代空格。正则里如果预期的是空格分隔属性,就会被这种变体欺骗。

  3. 标签替换:script不行就用img、svg、body、details、video、marquee。核心思路是:执行JavaScript不只是script标签的专利,事件属性才是真正干活的角色。

  4. 事件替换:onerror不行就换onload、ontoggle、onfocus、onmouseover、onclick。优先无交互触发。

  5. 编码绕过:HTML实体编码、URL编码、Unicode转义、双重编码。关键是确认过滤和解码的顺序。

  6. 引号逃逸:属性场景下闭合前后引号插入新属性。适用于过滤了标签但没过滤引号或事件属性的关卡。

  7. 无引号属性:onmouseover=alert(1)不写引号,绕过过滤引号的规则,同时节省字符。

  8. 反引号调用:某些情况支持onerror=alert利用反引号执行代码,作为括号被过滤时的备选。

  9. 伪协议:javascript:alert(1)放进href属性。适合过滤了标签但没限制属性协议的场景。

  10. iframe srcdoc:<iframe srcdoc="<script>alert(1)</script>">。适合外层过滤严格但内层渲染宽松的情况。

  11. svg载体:<svg><script>alert(1)</script></svg>。HTML解析器对SVG内部的解析规则与普通HTML不同,有时能绕过普通标签的黑名单。

  12. details/ontoggle:<details open ontoggle=alert(1)>。适合过滤了图片事件但没过滤details标签的关卡。

  13. autofocus触发:<input autofocus onfocus=alert(1)>。适合需要无交互触发的场景。

  14. form/input组合:利用表单的自动提交机制间接触发,适合需要绕过复杂事件过滤的场景。

  15. JavaScript实体编码绕过:在script上下文中利用\u0061lert之类的Unicode转义。适合只做字符串黑名单匹配的关卡。

  16. 过滤括号时的函数代换:alert被滤就换confirm、prompt、print,甚至fetch替代。

  17. 正则替换后的嵌套构造:有的过滤会把script替换为空字符串,那<scrscriptipt>被替换后正好变成<script>。这招百试百灵,关键要先确认替换是否递归执行。

  18. Dom Clobbering辅助:通过构造与全局变量同名的DOM元素来影响前端代码逻辑,间接控制执行流程。这个偏进阶,适合DOM型且前端逻辑存在弱点的关卡。

3.9 在线靶场的解题节奏

刷在线靶场时,我给自己定了一个流程,推荐你也按这个节奏走:

先花两分钟审题,看题目描述和模拟场景,判断大概是什么类型(反射、存储、DOM)。不急着动手,先用标记字符探路。然后在源码里搜这个标记,找到输出位置。接着根据输出位置和源码中能看到的过滤函数,写出“理论上可行”的payload,再提交测试。每次失败后,回看过滤规则,判断是关键字过滤、编码问题还是输出上下文理解错了,修正思路再试。

这个过程看起来简单,但很多人做不到,因为他们在“试payload”这一步就开始闭着眼粘贴。说实话,粘贴别人的payload最大的问题是:一次成功,你也不明白为什么成功;一次失败,你也不知道怎么改。真正有效的方法是每次都从输出上下文出发去思考。

4. 常见问题与排查技巧实录

4.1 为什么payload明明正确却不弹窗

这个问题出现频率最高。大多数人第一反应是“被过滤了”,但实际原因往往更基础:

  • 浏览器的XSS过滤器(特别是浏览器内置的反射型XSS防护)拦截了你的请求,尤其是在Chrome里,URL中的<script>字样很容易被直接拦截。这时候可以先在本地起一个测试页,或者换一个浏览器再测。
  • 输出位置和你的预期不一致。你以为输出在<p>标签内,其实被框架转义后变成了纯文本,没有进入HTML解析上下文。
  • 输入在到达输出点之前做了编码转换,比如尖括号被转成了&lt;。
  • 代码里存在语法错误。payload本身没问题,但漏了闭合引号或多了一个字符,导致JavaScript引擎直接报错。

排查方法也简单:按链路一步步查。先用标记确认输出位置,再单独测每个特殊字符是否原样保留,最后测试payload的最小可执行片段是否触发。

4.2 在线靶场怎么确认自己真的绕过了

通关弹窗之后不要急着庆祝,点击“查看源码”或者“查看响应”确认一下payload在响应中的真实形态。如果页面源代码里显示的是&lt;script&gt;,说明你看到的弹窗可能是之前缓存或别的机制导致的;如果能直接看到<script>alert(1)</script>原样躺在响应里,那才说明真的绕过了。

另外还要区分“触发成功”和“绕过成功”。如果payload能弹窗但URL中被浏览器拦截了,那在实际利用时会很不稳定,因为目标用户的浏览器环境不可控。训练时最好在多浏览器里都测一遍,至少保证核心payload在Chrome和Firefox里都能稳定触发。

4.3 十八种技巧在真实场景中的使用优先级

训练是训练,现实是现实。真实场景里的XSS防护通常不止一层,最常见的是输入过滤 + 输出编码 + 浏览器自带的XSS防护组合在一起。因此真实场景下的绕过思路和训练环境非常不同。

我的建议是:训练时要把十八种技巧都练熟,但真要分析一个真实站点,优先按这个顺序排查:

  1. 先定位输出上下文,判断能不能直接闭合,这是最基础的问题。
  2. 再看输出编码的类型,是HTML实体编码、JavaScript转义还是URL编码。这决定了你能不能利用编码类技巧。
  3. 然后看有没有长度限制、有没有关键字过滤。
  4. 最后才是考虑复杂的事件链和组合payload。

对初学者来说,最容易犯的错是第二层还没搞清楚就跳到第四层。输出编码没绕开,用再多高级标签也是白搭。

5. 升级路径与防御视角

5.1 刷完关卡后下一步可以做什么

如果你已经能在在线靶场里独立完成80%的关卡,那下一步我建议做三件事:

第一,试着在本地搭一个带数据库的留言板Demo,自己给每个输入点分别加上htmlspecialchars、js转义、URL编码三层防护,然后在不同位置测试绕过效果。这个过程能让你把训练环境里的技巧映射到真实代码层面。

第二,把同样的payload放到不同浏览器、不同版本里测试。同一个payload,在Chrome里能触发,在Firefox里可能因为解析差异而失效;反过来也一样。这种“环境差异”的知识,只有实测才能积累。

第三,培养“防御者思维”。当你提交payload之后,看一眼服务端的日志、源码里真正执行的过滤代码,思考一下如果自己是开发人员,要怎么写才能拦住当前这个payload。这比单纯研究攻击技巧更能提升整体的安全能力。

5.2 顺带理解主流防御机制

既然花了这么多时间研究绕过,顺手把防御机制搞清楚是很划算的。现代防御XSS的标配是“三层防御”:输入侧做白名单校验、输出侧做上下文相关编码、应用层再挂内容安全策略(CSP)。训练里的十八种绕过技巧,绝大多数在“输出编码正确 + CSP严格”的组合下都会失效。

但在真实环境里,这三层防御经常因为开发人员对“上下文”理解不到位而有缝隙:比如同一套转义函数用在HTML属性、JavaScript字符串、URL三个不同上下文里;或者CSP配置成了script-src 'unsafe-inline',等于没配。所以说,理解绕过技巧并不是为了“打穿”谁,而是为了知道防守时真正的薄弱点长什么样。

5.3 如何把这一套方法沉淀为自己的思维模型

到了这个阶段,你脑子里应该存的不再是18个孤立payload,而是一套“XSS上下文分析模型”。我自己的方法是把训练中的经验浓缩成一张思维流程图:拿到输入点后,先判断输出上下文;然后看过滤函数做了什么;再判断哪些字符能帮我闭合、哪些代码能帮我触发;最后根据长度限制压缩payload。

刷完这套训练后,我曾经回头把每一道题的payload重新整理了一遍,按“上下文类型、过滤函数、绕过原理、关键字符”四个维度做成了表格,留在本地当速查手册。遇到新项目时直接翻表格找相近场景,再根据当前代码微调payload,效率比每次从零开始构造高了不少。

6. 写在最后的个人体会

刷XSS训练关卡这个事,表面上是学“怎么绕过防护弹窗”,实际上练的是“如何精确理解一段代码的执行环境”。你把<script>换成<svg>,把alert换成confirm,把引号换成反引号,背后的核心始终是同一个:看懂输入和输出之间的关系,找出信任边界上的裂缝。

这套十八种绕过技巧,单独拎出任何一条都不复杂,难的是在一道又一道关卡里把它们融会贯通。我个人经验是,刷题的时候不要太在意“通关数量”,而是每过一关都回头多问一句:这关的过滤为什么拦不住我的payload?它是漏了标签,还是漏了事件属性,还是压根没有转义?把这个问题想透,比你多刷十道题都管用。

后续你可以试试把这些技术投射到真实的代码审计里,找一个本地开源项目,把用户输入点挨个过一遍,看看哪些地方存在XSS的可能。到那个时候,你会发现自己看代码的眼光已经和从前完全不一样了——不再是“有没有漏洞”的焦虑,而是“为什么这里安全、那里危险”的清晰判断。这种判断力,才是刷完这十八种技巧后最值钱的东西。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询