news 2026/10/20 5:47:20

绕WAF的XSS漏洞利用实战:从编码绕过到动态构造

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
绕WAF的XSS漏洞利用实战:从编码绕过到动态构造

做安全评估这些年,我遇到最多的一个误解就是:只要上了WAF,XSS就测都不用测了。实际上,WAF是一道很有价值的“闸门”,但绝不是一堵密不透风的墙。我经常跟团队里新人说,WAF在防护端做得越细,测试端能研究的空间就越大。这篇文章想聊的,就是这个绕WAF做XSS漏洞利用的完整思路,从WAF为什么会拦,到常见绕过手法,再到一次授权测试中的完整实战记录,一次性讲透。

这篇文章适合三类人:刚入门Web安全、想系统理解XSS与WAF对抗逻辑的初学者;已经在做渗透测试或安全评估,想补充绕过思路和编码技巧的工程师;以及写代码时需要自查XSS风险、想搞懂自家WAF规则漏了什么的前后端开发。我会尽量把原理讲明白,也会把实操中踩过的坑直接列出来,方便你直接参考。

1. 先说清楚:为什么XSS会被WAF盯上

1.1 XSS的三种基本形态与触发点

XSS(跨站脚本)说白了就是攻击者把一段可执行的脚本“塞”进页面,让浏览器在用户访问时把这段脚本当成页面自己的代码执行。分三种形态:反射型、存储型和DOM型。

反射型最简单,脚本通常藏在URL参数里,服务端不加过滤直接拼接进HTML返回;存储型更危险,脚本被存在服务端数据库里,任何用户访问相关页面都会被“投毒”;DOM型不走服务端,是前端JavaScript自己把不可信数据写进了页面上下文,比如用了innerHTML或者document.write。

这三种形态对应不同的检测思路。反射型关注请求参数,WAF主要盯URL和Header;存储型关注提交内容,WAF会重点检查POST请求体;DOM型则让传统WAF很头疼,因为服务端看不到完整的执行链路,很多时候要靠浏览器端防护,比如CSP来兜底。

我在授权测试里最喜欢先摸清楚目标站点的XSS“触发点”集中在哪,再决定后面怎么构造。比如一个有搜索框、结果直接回显的站点,多半是反射型;一个评论区支持富文本的站点,存储型概率很大;一个单页应用框架的站点,就要多看前端源码找DOM型。

1.2 WAF拦截的底层逻辑:规则、解码与语义

WAF拦截XSS,核心不是“看到<script>就拦”那么简单。它内部是一条完整的检测流水线:先拿到原始请求,对数据做解码和规范化,再拿处理后的内容去匹配规则库,部分产品还会跑语义分析模型。

规则匹配是最传统也最常见的手段。规则库里存了大量特征,比如<script、onerror=、javascript:、alert(等等。但这些特征是静态的,攻击者只要稍微换一种编码或表达方式,就可能让特征匹配失效。

解码是一个隐藏关键。很多WAF不会只看原始字节流,它会尝试做一次或多次URL解码、HTML实体解码、Unicode解码。比如%3Cscript%3E会被还原成<script>再匹配。但问题来了:当应用层代码也存在多层解码时,WAF的解码次数和应用的解码次数不一致,就会出现“WAF看到的是A,应用拿到的是B”的绕过窗口。

语义分析是近几年的趋势。它不再死抠关键字,而是看代码结构,比如检查这段内容最终是否形成了可执行的脚本上下文。这类WAF对拼接型和编码型绕过的识别能力明显更强,但也带来了更高的误报率和性能开销,很多中小站点未必开全。

理解了这些,你就能明白绕WAF不是“变个花样骗过正则”,而是要找到WAF的检测盲区,并且确保你构造的payload经过应用自身处理之后仍然能形成可执行的脚本。

2. 从零拆解WAF的常见检测机制

2.1 关键字正则匹配:最基础的一道闸

规则库里最常见的是正则匹配。典型规则包括:检测<script标签、检测on开头的事件属性、检测javascript:协议、检测alert、confirm、prompt这些敏感函数名。

这类规则的好处是速度快、维护成本低,坏处是太依赖“特征”。特征一旦被拆开、变形、编码,规则就可能失明。比如<script>换成<scr<script>ipt>,有些正则没做递归匹配就漏了;再比如alert(1)拆成al\ne rt(1),正则里没考虑空白符也会漏。

所以在测试时,我会先判断目标WAF拦截到什么程度:直接拦<script>?还是连onerror都拦?然后就能反推它大概是哪一类规则引擎。这个“踩点”过程很重要,能省下后面很多无效尝试。

2.2 解码链与规范化:容易被忽视的隐藏层

WAF拿到请求后,通常会做一系列“规范化”操作,把所有数据还原成最原始的形态再匹配。比如:URL百分号解码、HTML实体解码(&lt;还原成<)、Unicode规范化(半角转全角、异体字统一)、压缩重复空白字符等。

但这层规范化是有“次数”和“顺序”的。某些WAF只做一次URL解码,但应用代码里做了两次,比如先urldecode再拼接进HTML,这时WAF看到的%253Cscript%253E解一次是%3Cscript%3E,不能命中规则,应用解两次却还原出了<script>。这就是经典的“二次解码绕过”。

实操中我最常做的就是反复编码测试:把<编码成%26lt%3B,再编码成%2526lt%253B,逐层观察WAF是否失明。每次增加一层编码,都等于给WAF增加一次决策成本,只要它少解了一层,绕过就成立了。

2.3 语义分析与行为检测:新一代WAF的杀手锏

语义分析型WAF会尝试重建请求内容的结构。比如你提交"><svg onload=alert(1)>,它会分析这是否是HTML上下文中的标签注入;你提交';alert(1)//,它会分析这是否是JavaScript上下文中的语句闭合。

这类WAF对“拼凑型”payload的识别率极高,因为它不看你有没有alert关键字,而是看你构造的token序列是不是一条可执行语句。比如<svg onload=alert(1)>就算把alert写成al\u0065rt,语义分析依然能从解码后的token里识别出函数调用结构。

对抗语义分析的手段主要是“上下文拆解”和“动态计算”。比如把payload分布到多个请求参数里,前端再拼起来;或者用JavaScript的String.fromCharCode动态生成函数名,让WAF在静态分析时看不到完整调用链。但这需要应用本身支持多参数拼接,不是所有场景都能用。

所以结论是:不要上来就堆编码,先判断目标WAF的“智力水平”。规则型WAF重编码变形,语义型WAF重拆解与动态化,对症下药才高效。

3. 绕过思路与核心编码技巧:实操重点来了

3.1 大小写混写与关键字拆分

大小写混写是最基础的绕过手法,对付那些规则没加i修饰符的正则很有效。比如<ScRiPt>alert(1)</sCrIpT>,HTML标签名不区分大小写,但正则只认小写时就被骗过了。

不过现在大部分WAF的正则都加了大小写不敏感标志,单独用这招基本没戏。我更常用的是“关键字拆分”:把一个完整的关键字拆成两段,中间插入会被浏览器忽略但能被正则“打断”的字符。

比如<scr<script>ipt>这种嵌套写法,常用于绕过那些只做一层简单匹配的规则。浏览器解析HTML时会从<script>标签重新开始,最后仍能执行脚本。还有在javascript:协议里插入注释符,比如java\nscript:,某些浏览器解析时换行会被当成空白忽略,但正则里的.匹配不到换行就会出现空隙。

这类手法的本质是“利用解析器与正则引擎的差异”。WAF是正则思维,浏览器是解析器思维,两者对相同字符流理解不同,差异就是绕过点。

注意:大小写、拆分这些手法只对规则型WAF有效。遇到语义型WAF时,它的tokenizer会把标签拆成token再重建,嵌套和大小写基本都会被还原,这时要转向3.3和3.4的思路。

3.2 各类编码的双重与多重编码利用

编码是绕过WAF最重要的武器库。常见编码手段包括:

  • URL百分号编码:%3C%73%63%72%69%70%74%3E
  • HTML实体编码:&#x3C;script&#x3E;
  • Unicode转义:\u003cscript\u003e
  • JavaScript字符串转义:\x3cscript\x3e
  • base64编码后在运行时解码(用于动态构造场景)

关键的技巧是“堆叠编码”,也就是把上面几种编码组合使用。比如把<做HTML实体编码后,再对整个字符串做URL编码,形成%26lt%3B。WAF如果只做一层URL解码,看到的是&lt;而不是<,规则不命中;应用如果先做URL解码再做HTML实体解码,最终还原成<。

我在测试反射型XSS时,经常会在参数里构造多层编码链,然后用一个本地小脚本模拟目标应用的解码顺序,逐个匹配哪些组合能逃过规则。编码组合的调试本质是“排列组合”,必须系统性地试,不能靠灵感。

要特别提醒的是:编码绕过高度依赖目标应用的解码逻辑。如果应用本身只做一层解码,你堆再多层也没用,反而会把payload废掉。所以在编码之前,务必要先确认服务端语言和框架的默认解码行为,比如某个常见Java框架默认只解一次URL编码,PHP则依赖配置,这些都直接影响payload设计。

3.3 注释符、换行符与空白字符的巧用

注释符和空白字符是绕过正则的“润滑剂”。在HTML解析器眼里,<scr<!-- -->ipt>中的注释会让标签名变成scr,所以这个手法一般不行。但在JavaScript上下文里,注释符和换行符非常有用。

典型的例子是:<script>alert(1)</script>被过滤了alert,可以写成<script>al\u0065rt(1)</script>,这是Unicode转义,JavaScript在解析标识符时会先做转义还原;或者写成<script>window['al'+'ert'](1)</script>,利用字符串拼接绕开关键字匹配。

在HTML标签事件里,换行符也很有价值。比如<svg onload=alert(1)>被拦,可以试<svg\nonload=alert(1)>。有些正则用\sonload匹配事件属性前的空白,如果属性前直接换行可能不匹配,但浏览器的HTML解析允许属性名前面有各种空白字符,包括换行。

还有零宽字符(如%E2%80%8B)和制表符、回车符组合,也常被用来干扰正则的字符类匹配。但这些手段在语义型WAF面前效果会打折扣,因为tokenizer会把这些不可见字符统一归为空白。

经验:当WAF把onload、onerror这些事件名全部拉黑时,不要死磕事件属性,可以考虑把脚本放到<svg>的href、xlink:href里,或者用<details open ontoggle>这类冷门事件,很多规则库对冷门标签和事件覆盖不全。

3.4 动态构造与脚本替代方案

动态构造的核心思路是:在最终执行前,把被WAF识别的关键字“拼”出来。最典型的就是alert被过滤时,用window['al'+'ert']或eval(atob('...'))代替。

eval(atob(...))是实战中非常有用的组合。把整段恶意脚本Base64编码后放在参数里,前端再用atob解码给eval执行。WAF在请求里只看到一堆Base64字符,没有明显的<script>或alert特征,很难拦。

另一个好用的替代方向是:放弃“弹窗函数”,改用能直接表达攻击效果的API。比如不用alert(1),改用location='//xxx'做跳转,验证反射型XSS是否执行;或者玩document.cookie读取,把会话信息外带。这样WAF规则库里那些专门针对alert的规则就直接失效。

如果是存储型XSS,还可以考虑把payload拆成多段,分别藏在不同的字段或参数里,利用应用自身的拼接逻辑让脚本在运行时组装。这种手法对抗语义型WAF尤其有效,因为它打破了WAF单请求分析的视角。

4. 实战记录:一次授权测试中的完整绕过过程

4.1 信息收集与点位判断

在一次授权安全评估中,我拿到一个某资讯类站点的测试范围。这个站点部署了商业WAF,请求一提交,只要参数里出现<script或alert(,立刻返回拦截页面,响应码是典型的403,页面还带着WAF厂商的标识。

我先把站点整体过了一遍,找到了几个疑似XSS的输入点:一个站内搜索框,结果页回显搜索词;一个文章评论接口,评论内容会展示在页面上;还有一个用户昵称字段,个人主页会渲染昵称。这三处都是典型的XSS候选点。

搜索框的结果页回显是URL参数直接带入HTML的,属于反射型;文章评论和昵称是存储型的。我决定先从搜索框下手,因为反射型测试反馈最快,一次请求就能看到是否拦截。

踩点阶段我做了三组试探请求:第一组直接提交<script>alert(1)</script>,确认被拦;第二组提交<scr<script>ipt>alert(1)</script>,也被拦;第三组提交%3Cscript%3Ealert(1)%3C/script%3E,URL编码后依然被拦。这基本说明WAF具备URL解码能力,而且规则覆盖了最基本的编码形式。于是我把主攻方向转向“多重编码 + 标签拆解 + 动态函数名”的组合。

4.2 绕过尝试过程记录

第一轮,我尝试了纯HTML实体编码:请求参数提交%26lt%3Bscript%26gt%3Balert(1)%26lt%3B/script%26gt%3B。这个payload在WAF侧先做URL解码,得到&lt;script&gt;alert(1)&lt;/script&gt;,规则库里没有直接匹配&lt;开头的特征,放行通过。但到了应用侧,搜索框只做了一次HTML转义输出,没有解析实体,所以页面显示的是<script>文字本身,没有形成可执行的标签,这条payload等于没绕进去。

第二轮考虑“标签拆解 + 事件属性”:提交<svg/onload=alert(1)>,被拦;提交<svg\nonload=alert(1)>,还是被拦。看来事件属性是关键匹配点,规则里不只是查onload,还把svg和onload的组合也纳入了特征。

第三轮我把重点转到“图片标签 + 动态函数名”:提交<img src=x onerror=al\u0065rt(1)>。WAF在解码后的字符流里看到了al\u0065rt,它不是纯alert字符串,所以规则没拦;而JavaScript解析器在处理事件属性值时,会先把\u0065还原成e,最终执行alert(1)。这一次请求通过了WAF,页面弹窗成功。

这条payload之所以有效,核心是利用了“WAF和浏览器对Unicode转义的处理不一致”:WAF的正则按字面量匹配,它不解析JavaScript转义序列;浏览器HTML解析器在赋值事件属性时会识别\uXXXX序列。这个差异就是绕过窗口。

4.3 payload落地与验证

搜索框的点位打通后,我又验证了存储型评论接口。评论接口过滤更严格,不只是WAF拦截,应用层还做了htmlspecialchars式的标签过滤,直接提交任何含<的内容都会被转义。这种情况下,纯靠WAF绕过不够,还得应用层存在过滤缺陷才能继续。

我测试后发现,评论接口对<和>做了编码,但单双引号没有完全过滤。结合前端渲染逻辑,我构造了一个不依赖标签的DOM型payload:提交"/onmouseover="alert(1),利用应用把参数拼进某个HTML属性的机会,闭合属性后注入新的事件。这条payload不包含尖括号,WAF规则库里针对标签的规则全部落空,应用层过滤也没有拦到,最终在真实用户鼠标悬停时触发。

验证阶段我不仅看弹窗,还看payload是否在DOM树中“名副其实”。比如用document.cookie做个外带,或者改个DOM内容,确认执行上下文是可控的。弹窗只能证明有XSS执行,但外带数据和DOM篡改更能反映实际危害。

整个测试下来,我最大的体会是:绕WAF不能只依赖一个“万能payload”,每个目标站的应用逻辑和WAF规则都不同,必须基于实际回显不断调整。

5. 常见问题与排查技巧速查

5.1 WAF不拦直接弹窗,先别高兴太早

第一次测试时,我提交<script>alert(1)</script>,WAF直接放行,页面也弹窗了。当时我以为是目标没开防护,后来才发现这个WAF对application/json类型请求的解析有缺陷,根本没检查JSON body。

这种“假绕过”很容易误导判断。正确做法是:先用几个已知被拦截的恶意特征做“对照组”,比如提交<script>alert(1)</script>看是否被拦,确认WAF确实工作之后,再判断放行是漏洞还是误报。

5.2 绕过后payload不生效的几类原因

payload过了WAF但页面没有执行,最可能的原因是应用层把关键字符转义了。比如服务端把<转成了&lt;,你的payload在浏览器里变成了纯文本,当然不执行。解决思路是寻找能被应用“放行”的上下文,比如属性值、URL跳转参数、JavaScript变量赋值等。

另一个常见问题是编码过度。我见过有人把payload编码了好几层,WAF是绕过去了,但应用只解码一次,最终执行时字符串全是乱码。事前确认应用的解码层数,比盲目堆编码更重要。

还有一个容易被忽略的点是长度限制。某些参数有最大长度,比如搜索框限制50字符,长payload会被截断。这时优先用短payload,比如<svg onload=alert(1)>,或者用外链脚本`

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 5:44:47

Llama 2提示词工程实战:从对话模板到思维链的调优全攻略

我第一次接触这个模型时&#xff0c;最大的教训就是把别的闭源模型提示词原封不动地搬过来。结果模型要么回答得飘忽不定&#xff0c;要么频繁重复&#xff0c;要么完全无视我设定的输出格式。折腾一晚上之后才意识到&#xff1a;Llama 2提示词工程的第一道门槛&#xff0c;不是…

作者头像 李华
网站建设 2026/10/20 5:43:57

无源码IPA安全加固:越狱检测、防篡改与重签名实战

上个月接手一个外包遗留的 iOS 项目&#xff0c;原团队解散后只留下一台编译机&#xff0c;能从里面导出的最终交付物就是一个整包 IPA。客户的要求很明确&#xff1a;“这个 App 连越狱检测和防篡改都没有&#xff0c;帮我把基础安全处理补上。”没有源码&#xff0c;不能重新…

作者头像 李华
网站建设 2026/10/20 5:43:57

Sidecar与后训练:本地大模型如何越用越懂你

这两年跟同行聊本地大模型&#xff0c;绕不开一个话题&#xff1a;模型跑起来了&#xff0c;然后呢&#xff1f;下载一个开源基座模型&#xff0c;在本地部署、量化、接进自己的工具链&#xff0c;一开始确实兴奋&#xff0c;但用一个礼拜就会发现&#xff0c;它永远在用最初那…

作者头像 李华
网站建设 2026/10/20 5:42:36

工业液冷工作站选型指南:从算力账到验收单

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 5:42:17

C/C++理论基础知识题库解析:内存、指针与类型陷阱

我工作这些年&#xff0c;有个感受特别深&#xff1a;真正能把C/C基础理论选择填空答得又快又稳的人&#xff0c;往往不是把语法书背了多少遍&#xff0c;而是对底层机制和语言设计意图有自己的一套清晰模型。这份“C/C理论基础知识选择填空题库2”并不是简单罗列知识点&#x…

作者头像 李华
网站建设 2026/10/20 5:42:04

逆向拆解六款开源RAG产品:自研检索增强生成系统的工程实践与蓝图

1. 为什么我要花两周时间逆向拆解六款开源RAG产品去年下半年&#xff0c;我所在的团队接到一个内部知识库问答系统的需求。当时摆在面前的有两条路&#xff1a;一是直接调用市面上成熟的商业RAG服务&#xff0c;二是基于开源方案自研一套。商业方案看起来省事&#xff0c;但数据…

作者头像 李华