XSS绕过实战指南:从过滤失效到WAF绕过的核心思路
2026/9/15 12:49:46 网站建设 项目流程

1. 为什么过滤函数挡不住XSS:从黑名单失效说起

先聊一个很多人踩过的场景:你把用户输入里的<script>替换成空字符串,自以为万事大吉,结果一个<scr<script>ipt>就给你把代码弹了出来。这种经历基本每个做Web安全的兄弟都有过,也是 XSS 绕过这条路的起点。

1.1 过滤的本质是"猜攻击面",注定有盲区

黑名单过滤的逻辑很简单:我知道攻击者可能发什么,我就把那些危险的东西删掉。问题是攻击面不是一个静态清单,而是HTML、CSS、JavaScript、URL解析等多个标准叠加形成的组合空间。

举一个最基础的例子,服务端如果只过滤了<script>标签,就能拦住所有脚本执行吗?明显不能。<img src=x onerror=alert(1)>不需要script标签,<svg/onload=alert(1)>也不需要,<details open ontoggle=alert(1)>同样不需要。只要HTML里存在能触发事件的元素,就有执行JS的可能。

这就是过滤函数失效的第一个原因:你防的是一个标签,但攻击者可以用整个HTML语言来攻击你。

第二个原因更隐蔽,服务器和浏览器的解析方式经常不一致。服务端过滤时用的是正则匹配,而浏览器解析HTML时有自己的一套状态机逻辑。一个payload在服务端眼里是"无害字符串",到了浏览器那里可能就变成了"可执行代码"。绕过往往就藏在这层解析差异里。

1.2 输入输出上下文的差异:同一个payload两种命运

很多开发者在做过滤时会忽略一个关键问题:用户输入到底会出现在HTML的哪个位置。

同一个<script>alert(1)</script>,如果被插入到<div>标签的内容里,那确实会执行;但如果被插入到<a href="...">的属性值里,你的双引号直接就被转义或者截断了,根本出不来。反过来也一样,有些payload在标签内容区不生效,放到属性里反而能跑通。

所以做XSS绕过之前,第一件事不是堆payload,而是搞清楚三件事:

  • 输入点在哪里(URL参数、POST字段、Header头)
  • 输出点在哪个上下文(标签内容、属性值、script代码区、CSS样式区)
  • 中间有没有经过过滤或编码处理

这三件事确定了,后面所有的绕过思路才有落脚点。不然你就是在黑灯瞎火试payload,试中了也不知道为什么中,没试中也不知道差在哪里。

2. 标签和属性级别的绕过手法拆解

把基础概念理清之后,我们直接从实际的标签绕过手法开始拆。这个层面讲的是:服务端在做过滤时,到底有哪些可以被钻的空子。

2.1 标签绕过三板斧:大小写、空字符、不常见标签

先说说最入门但也最实用的三个方向。

**大小写混写。**有些过滤规则写得很粗糙,正则里只匹配了小写的<script>,那<ScRiPt>自然就绕过去了。在HTML5标准里,标签名是大小写不敏感的,浏览器一律都认。这种方法在现在的大部分WAF面前已经失效了,但在一堆开发自写的过滤函数里依然有生存空间。

**空字符截断与特殊空白符。**HTML解析器对标签内的空白处理比较宽容,<scr后面跟一个tab、换行符、回车符甚至某些控制字符,浏览器都可以正常识别为script标签的一部分。有些过滤正则用的是严格的单词边界匹配,碰到<script<script之间的特殊空白就断了匹配逻辑,但浏览器照样跑给你看。常见的特殊空白符包括tab(%09)、换行(%0a)、回车(%0d),在某些解析环境里甚至空字节%00也能参与。

**被忽略但可用的标签。**很多人死磕<script><img>,其实HTML里能执行脚本的标签远比你想的多。举几个实战常用的:

标签触发方式浏览器兼容情况
<svg>事件属性如onload、onclick全部支持
<details>ontoggle事件,打开可自动触发多数现代浏览器
<marquee>onstart事件,页面加载自动触发浏览器兼容性佳
<video>/<audio>onerror、onloadstart全部支持
<math>可搭配事件属性部分环境可用
<iframe>srcdoc属性内嵌HTML,onload触发全部支持
<a>href的javascript伪协议用户交互触发

这里面<details ontoggle><svg onload>是我做测试时命中率最高的两个,因为它们不需要用户额外操作,页面加载或元素渲染时就能触发。

2.2 事件属性的存活条件:是过滤不够还是解析特殊

标签级别的过滤如果做得好,剩下的突破口基本都在事件属性上。但事件属性绕过的前提是:标签本身没有被完全拦截。

比如输入被放到<img src="[输入点]">的src属性里,你的输入内容被限制在了引号内。很多过滤规则只关注你有没有<>,忽略了属性内部本身就可以闭合和构造新攻击面。

构造一个经典的属性内XSS:

" onerror="alert(document.cookie)

如果src的值被拼接到HTML里,"闭合掉原有的属性,onerror变成img标签的新属性,双引号包住攻击代码。最终渲染出来的HTML就变成了:

<img src="" onerror="alert(document.cookie)">

图片加载失败触发onerror,脚本执行。

这个手法的难点不在于构造payload,而在于搞清楚:引号要不要被HTML实体编码?如果编码了,浏览器在解析属性值时会不会解码回来?很多过滤方案是把双引号转成&quot;,但浏览器在HTML属性上下文里会把&quot;还原成",导致原本的编码保护形同虚设。这种错位经常被忽略。

2.3 引用符逃逸与属性截断实战

如果你输入的内容里引号被严格转义了,连&quot;这条路都堵死了,是不是就没办法了?其实还有一个思路:不用引号,利用HTML属性在某些场景下的反引号或空格分隔规则。

有些过滤代码会过滤单引号和双引号,但漏了反引号。在HTML属性里,反引号在部分旧版浏览器中是可以作为属性引号使用的,所以<img src=`` onerror=alert(1)>这种写法在特定环境下依然能构成攻击链。

再往深处走,如果连反引号也被处理了,你还可以考虑属性截断。逻辑是这样的:某些过滤函数会在输入里检测危险单词,一旦发现就替换成空,但如果危险单词被拆分成两段拼接,过滤就失效了。比如输入onerror会被删,那就用on+error的拼接方式——但这是拼接型思路,通常用于输出点在JS代码上下文里。

HTML属性上下文更适合的招式是换行分割<img src=x\nonerror=alert(1)>在部分浏览器的解析逻辑里,属性名和属性值之间的换行会被当作空白处理,onerror照样被识别成事件属性。如果你的过滤规则是精确匹配onerror=这种连续字符串,那这个payload就能穿透。

3. WAF正则规则下的三层绕过路径

WAF比普通的服务端过滤要狠得多,它的规则库覆盖了常见payload、编码形式、组合攻击。想在WAF面前突围,得换一个思路:不跟WAF的正则硬碰硬,而是利用协议解析差异绕过去。

3.1 编码层:HTML实体、URL编码与二次解码陷阱

WAF的核心工作方式是先拿到请求内容,然后匹配规则库。所以绕过的第一个方向就是:让WAF看到的和服务器最终处理的内容不一样。

HTML实体编码是最基础的手法。<svg/onload=alert(1)>如果被WAF直接拦截,可以试试把字母用HTML实体编码:

<svg/onload=&#97;lert(1)>

这里的&#97;是字母a的十进制实体。浏览器在解析HTML属性值时会先解码实体,再当作JavaScript执行。WAF如果只针对明文规则做匹配,就会漏掉这种形式。

URL编码在GET参数场景下很常见。WAF可能会对URL参数值做一次解码然后再匹配,但有些WAF只解码一层。如果服务端还会二次解码,那就出现了时间差。

举个典型例子,参数值是%2526,WAF解码一次得到%26,没匹配上任何规则,但服务端如果又解码了一次,实际拿到的是&,这时拼接进HTML的属性语法就变了。这种双重编码的技巧在排错时最容易混淆,但也是绕过WAF最常用的手段之一。

Unicode编码和进制混用也值得关注。javascript:alert(1)可以写成java&#115;cript:alert(1),甚至用八进制&#116;、十六进制&#x74;混着来。某些WAF规则只拦截了一部分编码形式,换一种就能过。

3.2 结构层:在奇怪位置插入token让正则对不上

WAF的正则通常基于常见payload模式构建,所以从结构上破坏它的匹配条件是一个高效方向。核心思想是:在payload里插入一些无害的字符,让WAF的正则无法完成连续匹配,但浏览器在执行时又能正确解析。

一个经典的例子是换行。某些WAF的正则严格要求onerror后面直接跟=,那你就在中间加一个换行符:

<svg/onerror =alert(1)>

HTML的解析规则允许属性名后、等号前后存在空白,所以浏览器可以正常解析,但WAF的规则在onerror=之间遇到换行就匹配断了。

类似的方法还包括在关键字中间插入注释符。这个在JavaScript上下文里更常见,比如al/*注释*/ert(1),浏览器在JS解析时会忽略注释,但常规正则匹配会把这段当成两个孤立单词,规则直接落空。

另外还有一种思路是利用标签的重复属性。有些WAF会去检查<img>标签里有没有onerror,如果你写上两个<img>标签,第一个只承载src,第二个内嵌事件属性,正则可能只会锁定第一个标签而放过第二个。这种手法属于简单粗暴但偶尔有奇效的范畴。

3.3 协议层:javascript伪协议的各种歪路

无论标签怎么变,XSS绕不过去的核心始终是JavaScript代码如何被执行。而JavaScript伪协议是连接HTML和JS的一座关键桥梁。

<a href="javascript:alert(1)">就是一个典型的伪协议利用。点击链接时执行JS代码。问题是现在很多WAF对javascript:做了规则匹配,怎么突破?

常见的思路是混淆协议关键字。例如在协议和代码之间插入HTML实体或注释:

  • java&#x73;cript:alert(1)通过实体编码绕过
  • java\nscript:alert(1)利用解析差异绕过
  • javas&#99;ript:alert(1)继续混合编码

另一种思路是利用空格和制表符。在部分浏览器的URL解析逻辑里,javascript:后面跟一个tab或换行符,协议识别依然成立,但正则如果匹配的是javascript:后面直接跟非空字符,这里就出现了差异。

还有一种是大小写与字符编码的叠加JaVaScRiPt:javas%63ript:在部分URL解析器里都能被还原成合法协议。WAF的单条正则往往只覆盖一种变形,多变形组合就能提高穿透率。

我倾向于把WAF绕过理解成一个"维度博弈"问题:普通过滤防的是输入内容,WAF防的是协议的合法性、结构的合理性、载荷的可疑性,而你要做的就是利用协议解析的灰色地带,让WAF认为"这没问题",让浏览器认为"这必须执行"。

4. 靶场实战模拟:从皮卡丘反射型到iwebsec通关

理论讲再多,不落地都是空的。这一节把前面讲的手法放进具体的靶场环境里演练一遍,看看真实通关过程是什么样的。

4.1 皮卡丘靶场反射型XSS的绕过细节

皮卡丘靶场的反射型XSS一般是这样设计的:一个搜索框,输入内容后提交,服务器把输入回显到页面上。多数题目的过滤逻辑很简单,但因为过滤实现在服务端,所以需要先观察回显的位置才能判断payload能不能通。

我的习惯是先输入一个无害的探测值,比如test123,然后看页面源码里这个值出现在哪里。这很关键。

如果回显直接出现在<div>标签内容里,那第一层的探险方向是闭合标签:

test123</div><script>alert(1)</script>

如果回显出现在<input value="...">这样的属性里,那思路就变为闭合引号和标签:

test123"><svg onload=alert(1)>

皮卡丘这关通常是不设防的,直接<script>alert(1)</script>就能过。但你如果只做到这一步,那你对绕过的理解其实没有真正建立起来。我会建议把过关标准提高一点:假设这里的服务端过滤了script关键字,你怎么继续过?

试试<scr<script>ipt>自拼接、<img src=x onerror=alert(1)>事件触发、<svg onload=alert(1)>SVG标签,然后逐一把这些payload当过关条件来跑。皮卡丘的好处就是环境简单、干扰少,适合建立"先探测上下文,再构造payload"的完整思路。

4.2 iwebsec关卡的不同过滤强度递进

iwebsec靶场在XSS题目的设计上比皮卡丘多了一个维度:每一关的过滤强度是递进的,模拟了不同安全等级的代码实现。

通关的节奏可以这样安排:

第一关通常只是简单的关键字替换,把自己写的拦截正则找出来,用大小写混写就能过。第二关可能会过滤onerror这类常见事件名,但标签本身没卡死,可以尝试<details ontoggle><svg onload>切换触发方式。第三关连常见标签和事件一起过滤,这时候要开始考虑编码绕过了,比如HTML实体编码、URL编码,看服务端是否存在解码逻辑。第四关假如引入了类似简单WAF的规则,那就按前面讲的结构层方法,插换行、插注释、混编码,逐条测。

有一个我自己踩过的坑:在iwebsec某一关里,payload里同时出现了<script>alert,WAF检测到alert就拦截,当时的绕法是把alert改写成top["al"+"ert"]或者eval.call的形式。这个层面已经进入JS代码混淆的范畴,但要通关必须掌握,因为很多靶场最后一关的设计思路就是既检查标签又检查危险函数

通关的意义不只是填一个flag,而是你在每一关切换过滤策略时,会逐步形成一套自己的测试方法论:先测标签,再测事件,再测协议,最后测函数执行。这套方法论比任何单个payload都值钱。

5. 绕过之后的反向产物:加固与修复清单

攻防是一体两面。把绕过手法研究透了,反向去看就是一套很清晰的修复清单。这也是我在实战里最深的体会:能不断绕过过滤方案的人,往往也是最能把防御方案设计明白的人。

5.1 真正有效的输出编码方案

黑名单过滤之所以反复被绕过,根源在于它在"猜测攻击者的形态"。而真正有效的防御思路是输出编码——不管输入是什么,在输出到HTML的不同上下文时,全部转义成无害实体。

  • 输出到HTML标签内容时,<转成&lt;>转成&gt;&转成&amp;
  • 输出到HTML属性值时,除了上面三个,还要把双引号转成&quot;,单引号转成&#x27;
  • 输出到JavaScript字符串时,要做JS语境下的转义,比如把引号、反斜杠、换行符转成Unicode转义序列。
  • 输出到URL属性时,要做URL编码或协议白名单校验,防止javascript:等伪协议混入。

很多开发觉得htmlspecialchars一个函数就搞定了,但它默认只转义& < > ",单引号如果参数不用ENT_QUOTES,照样放行。另外,htmlspecialchars处理完了再拼接进JS字符串里也可能出问题,因为JS上下文的安全规则和HTML上下文根本不是一回事。

一个稳妥的建议是:在不同上下文使用不同的编码函数,不要指望一个函数通吃所有位置。这套思路如果落地了,你会发现黑名单过滤有没有都无所谓了,因为不管攻击者发送什么内容,输出时都变成了普通文本,浏览器不再把它当作代码来解析。

5.2 CSP与HttpOnly的兜底方案

即使编码做得再全面,也依然有可能因为业务逻辑复杂、拼接点众多而遗漏某个上下文。这时候就需要CSP(Content Security Policy)来做兜底。

CSP的核心思路是:告诉浏览器哪些来源的脚本可以执行。一个常见的配置是:

Content-Security-Policy: default-src 'self'; script-src 'self'

这个配置的意思是:所有内容只能从同源地址加载,脚本也只允许执行来自同源的资源。这样即使攻击者成功注入了<script>标签或事件属性,浏览器也会因为在执行外联脚本或内联脚本时无法通过CSP检查而被拦截。

事件属性这种内联脚本在script-src 'self'的策略下会被直接阻止,javascript:伪协议也同样受限。

HttpOnly则是针对Cookie的防护。设置了HttpOnly属性的Cookie,在document.cookie里是读取不到的,这能挡住一部分以盗取Cookie为目标的XSS攻击。但要注意,HttpOnly防不住所有XSS的后果,如果攻击者能借XSS执行敏感操作(比如改密码、发私信),即使拿不到Cookie也能造成破坏。所以HttpOnly只是一个环节,不能当作全部。

还应该配套的是输入长度限制内容安全审计。XSS payload大多不长,但事件属性加编码很容易变得很长,对输入长度做一个合理的上限,可以压缩攻击者的操作空间。另外,定期用自动化扫描器配手动渗透测试来复查所有输入输出点,比等攻击者找上门再修要靠谱得多。

我在实际做加固时还会额外做一步:把用户可控输入单独存起来,输出前统一走一个专门的渲染函数,所有拼接都经过这个函数处理。这个设计看起来很死板,但它确实能把"漏改一个输出点"的概率降到最低。

6. 我自己测试XSS时固定用的几条实战习惯

分享几条这些年攒下来的实操习惯,不算什么高深理论,但能少走很多弯路。

第一条,永远先看回显位置,不急着试payload。拿到一个XSS测试点时,先随便传一个长一点的唯一字符串,打开浏览器开发者工具,搜索这个字符串出现了几次、分别在哪里、有没有被转义,再决定用哪种绕过思路。这一步花30秒,能省后面一小时的盲目尝试。

第二条,准备一份自己的payload字典,但一定要懂每条payload为什么能过。网上随便能搜到上千条XSS payload,但如果你不知道它针对的是哪种过滤、依赖哪个浏览器的解析特性,那换个环境就抓瞎。我的习惯是把payload按"上下文类型+绕过维度+适用浏览器"分类维护,每一条都备注触发条件和适用场景。需要过某类WAF时,优先从对应类别里挑选变形,而不是随机试。

第三条,观察过滤回显的表现形式。有的过滤是删除危险字符,删除后剩下的HTML会拼接成新结构;有的是替换成安全写法,比如<script>换成[removed];有的直接返回400。删除型过滤可以用自拼接绕,替换型过滤可以用双重编码绕,拦截型过滤就得换结构。根据回显表现反推过滤逻辑,比盲试高效得多。

第四条,不要忽视POST型和DOM型XSS。很多人在GET型XSS上研究得透彻,到了DOM型就不知道怎么调试了。DOM型XSS的关键在于追踪JavaScript里document.writeinnerHTMLlocation等关键函数的数据流,关闭页面缓存、打上断点逐步看。这里最有效的工具就是浏览器开发者工具的Sources面板加Console。

第五条,每个payload都做最小化。构造绕过payload时,不要一上来就整段复杂代码,先把目标拆成最小验证单元。比如只验证<svg onload=1>能不能触发弹窗,能触发后再加功能代码。最小化能让你快速定位"是标签被拦了,还是事件属性被拦了,还是函数执行被拦了",避免把多个变量混在一起排查不出来。

XSS绕过的门道说来很多,但说到底还是那件事——理解浏览器和服务器对同一段输入的不同理解,然后利用这种差异。把这套思维建立起来,后面不管碰到什么样的过滤、什么样的WAF,你都能顺着上下文快速找到穿过它的角度。这也是这个领域最让人上瘾的地方。

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

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

立即咨询