ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

XSS过滤绕过实战:从标签变形到WAF绕过的完整方法论

XSS过滤绕过实战:从标签变形到WAF绕过的完整方法论 干活这么多年测过的Web站没有一千也有八百XSS这玩意儿永远是最容易出、也最容易被忽略的那一环。很多朋友拿了一个漏洞报告里面写着“存在反射型XSS”高高兴兴提交了事但真到实战、真到补测、真到写渗透报告需要证明影响范围的时候往往会卡在一个点上——参数明明有回显但payload一发就被拦截要么被后端过滤函数剥掉标签要么被WAF直接403要么过滤后页面干干净净什么都弹不出来。这篇文章我想用一个过来人的角度把XSS过滤绕过、标签绕过、WAF绕过这件事完整拆一遍。里面不会讲太虚的理论更多是我在实际测试中踩过的坑、摸出来的路子。无论你是刚入门的安全爱好者还是正在做等保测评、红蓝对抗的测试人员只要你需要真正把XSS验证到“出效果”这一步这篇内容应该能帮你省不少时间。需要提前声明清楚所有绕过手法的前提是你获得了目标系统的合法测试授权或者是在自己搭的靶场、CTF题里做练习。未授权测试在任何场景下都不是技术问题是性质问题。这个底线我在带人的时候反复强调在这篇文章里也先立住。1. 先搞清楚你面对的是什么XSS过滤与WAF的检测逻辑1.1 三种XSS类型的检测差异做绕过之前第一步不是急着拼payload而是先判断当前触发点属于哪种类型的XSS不同检测逻辑和利用方式差别很大。反射型XSS最常见于搜索框、URL参数回显、错误信息展示等场景特点是payload随请求发出响应中直接带回来。这种类型的过滤通常加在服务端入口处比如Java的Filter、PHP的htmlspecialchars、Python的cgi.escape或模板引擎的自动转义。它的核心检测逻辑是“如果输入内容包含危险关键字就替换或删除”。存储型XSS多发于评论区、昵称、签名、表单提交这类写库再读库的场景。过滤逻辑和反射型类似但因为数据要过数据库、过编码转换、过前端渲染绕过空间往往更大。很多开发对存储型XSS的过滤只做了一层入库时过滤了出库时没处理或者入库时做了htmlspecialchars但前端渲染用的是v-html、innerHTML这类危险接口等于直接把转义后的实体重新解码再解析防护形同虚设。DOM型XSS是最容易被人忽略、也是现在前端框架普及后占比最高的一种。它的特点是payload不经过服务端完全发生在浏览器端。举例来说一个页面从location.hash或URLSearchParams里取参数直接拼进document.write或innerHTML服务端日志里根本看不到攻击痕迹。DOM型XSS的过滤往往不在服务端而在前端JS代码里很多开发会用replace过滤script、过滤on开头的事件字符但因为DOM操作本身发生在浏览器解析之后过滤点如果选错了对象很容易被绕过。区分方式用一个简单标准用浏览器的F12看网络请求如果payload出现在请求URL里且响应HTML中直接回显是反射型payload第一次请求可能没问题二次访问才触发是存储型服务端请求里完全没有payload但页面行为异常是DOM型。1.2 过滤与WAF常见的拦截思路过滤和WAF的检测逻辑本质上是两个方向一个靠字符串匹配一个靠语义分析。字符串匹配是后端代码最常见的方式。它的实现一般是维护一个黑名单列表像script、alert、onerror、javascript:这些词直接出现在输入里就替换或拦截。这种过滤的绕过思路非常成熟核心就是想办法让输入在“到达敏感解析位置”前不呈现黑名单里的原始形态但经过HTML解析或JS解析后又能恢复成可执行代码。WAF的检测逻辑要复杂一些现代WAF基本都引入了语义分析能力比如通过解码、归一化、特征规则集的方式识别攻击载荷。它在监听HTTP流量时会尝试对请求做一次或多层解码然后匹配攻击特征。遇到WAF单纯的scriptalert(1)/script基本必死但WAF也不是万能的它的弱点在于“解码层数有限”“对协议解析存在差异”“规则集覆盖不全”。绕过WAF的核心就两点一种是让流量形态超出WAF的检测规则覆盖范围另一种是让WAF对内容的解析结果和浏览器不一致。这里要区分一个关键概念WAF绕过不是全通。很多时候同一个payload在一个位置被拦截换一个参数位置、换一种编码方式、换一个提交方法就放行了。我在实际测试中经常遇到的情况是POST请求里带payload被拦截但同样的payload改成JSON格式提交就绕过GET被拦截改成multipart/form-data分块提交就绕过。这些现象背后都是WAF对不同协议字段的解析差异理解这个逻辑比背样本集重要得多。2. 标签绕过从最基础的标签开始逐层拆解2.1 基础标签的使用与限制XSS注入的最终目标是让浏览器执行JS代码而浏览器执行JS的唯一入口是HTML解析器。也就是说无论你用什么标签前提是它能够触发脚本执行。经典的script标签是最直接的方式但也是过滤最严的因为它是所有安全设备的第一识别目标。当script被过滤后需要考虑的标签分为几类一类是支持事件属性的HTML标签比如img onerror、svg onload、body onload、iframe onload一类是支持src或href属性的标签可以用伪协议引入代码比如iframe srcjavascript:alert(1)、a hrefjavascript:alert(1)还有一类是利用HTML解析器的容错特性比如svgscript嵌套、mathmtext等SVG和MathML命名空间的特殊标签。这里需要理解一个底层原理HTML解析器对标签的识别基于的是HTML5规范里的“tag open state”状态机。浏览器在解析HTML时如果遇到字符会进入标签开始状态接着判断后续字符是否构成合法的标签名。只要标签名合法解析器就会把它当作一个标签处理无论你外面的产出代码如何拼接过滤。这就意味着很多过滤函数如果只过滤了s、c、r等字符的拼接形式而没处理标签整体的出现就有机会通过变形绕过。实操中我把常用的标签按适用场景整理过一份对照表测试的时候先按表里的顺序试一轮能省下不少时间。标签触发方式适用场景说明script直接执行过滤较宽松最优先尝试被过滤后直接放弃imgonerror事件src加载失败时触发最常用不依赖用户交互自动触发svgonload事件或内嵌script事件被过滤时备选SVG标签在旧版浏览器过滤不全bodyonload事件页面主框架注入时需要标签能插入到body区域iframesrc伪协议或srcdoc伪协议检测待定场景可用srcdoc属性内嵌完整HTMLmathhref伪协议或事件SVG被过滤时MathML命名空间容易被忽略detailsontoggle事件需要用户交互也可接受时Open属性默认展开可自动触发ahref伪协议需用户点击钓鱼类验证自动化验证场景不太推荐textareaautofocus onfocus事件属性被过滤可绕过极少用到但有时能出奇效表格里的textarea值得单独说一句。autofocus属性可以让元素在页面加载时自动获得焦点从而触发onfocus事件而onfocus的过滤往往没有onerror那么严很多黑名单只拦了img的onerror没拦textarea的autofocus组合这个字段在实站测试里救过我几次。2.2 事件属性绕过思路事件属性是XSS绕过里最灵活的一类但也是WAF和过滤器重点盯防的对象。常见的过滤策略有两种一种是把on开头的事件名直接删掉比如把onerror变成error还有一种是把等号后面的JS执行部分剥离。针对第一种情况如果只是删掉onerror字符串本身可以利用双写。比如输入oonnerror过滤删掉中间的onerror后剩下的恰好是一个完整的onerror。这个思路在开发只做一次str_replace不做循环递归时非常有效。我在一个Java写的后台管理系统里遇到过类似情况参数经过两层Filter第一层删on第二层删error我构造onnnerrorrr形态的payload每层删掉一个关键字后依然完整最终成功触发。如果过滤的是事件名的一部分比如把error替换为空那可以尝试改变大小写如onError、oNerror。HTML属性名是大小写不敏感的浏览器能正常解析但很多过滤器用strtolower之后才做匹配大小写绕过已经失效。所以更推荐的是用formaction、autofocus这类不包含敏感关键字的事件搭配。针对事件属性整体被过滤的情况需要换一种思路找非事件属性的执行方法。HTML里除了事件属性还有几个“隐形执行点”。一个是iframe的srcdoc属性里面可以嵌入任意HTML等价于在子文档里执行代码一个是svg和math命名空间下的href属性支持javascript:伪协议还有一个是object标签的data属性可以加载data:text/html内容。还要考虑引号被过滤的情况。很多开发会把单双引号都过滤掉这时事件属性通常写不了因为onerroralert(1)里的字符串需要用引号包裹。但要注意HTML属性值本身不一定需要引号img srcx onerroralert(1)在浏览器里面是完全合法的属性之间用空格分隔即可。如果JS函数名也被过滤可以通过反引号代替括号调用函数比如onerroralert1这种形态在某些浏览器版本里是可以执行的不过现代浏览器对反引号作为函数调用的兼容性已经改了很多实战价值有限这里提一下作为知识储备。2.3 引号、括号、关键字被过滤时的变形处理绕过越做越深遇到的过滤条件会越来越苛刻这里把几个典型的“变形处理”场景拆开讲。括号被过滤是最让人头疼的一种情况。现代开发框架尤其喜欢做这类防护比如把(和)全局替换。括号过滤后传统的alert(1)、prompt(document.domain)都失效了。绕过思路有几条路第一利用throw语句。throw可以作为表达式的一部分配合onerror事件可以做到不带括号执行常见形态是onerrorthrow 1但throw后面必须跟一个Expression所以通常需要构造类似img srcx onerrorthrow/**/1的形态。这里插一句throw后面通常需要处理换行或注释符否则会被解析器当成语句结束实测中加注释符/**/最稳定。第二利用模板字符串。在支持ES6的现代浏览器里alert1这种写法是合法的反引号把字符串模板传给了alert函数等于alert(1)。这个技巧在部分过滤了括号但没过滤反引号的问题中可以直接用。第三利用onerror的默认传参。onerror事件处理函数被执行时浏览器会自动传入错误信息、错误文件、行号等参数这意味着onerroralert可以直接接收第一个参数作为alert的内容从而实现无括号执行。这是老技巧了但至今在很多过滤场景下仍然有效。关键字被过滤是另一种常见的困境。alert被过滤时可以换成prompt、confirm这俩和alert功能类似都是弹窗验证。都过滤时可以改用console.log利用控制台输出验证虽然不会弹窗但配合F12调试一样能证明执行点存在。如果连function、eval这类词都被过滤可以考虑用atob结合编码的方式比如eval(atob(YWxlcnQoMSk))形态可以绕开对函数名的直接匹配。再极端一点所有字母都被过滤如果目标允许Unicode编码可以利用JS的Unicode转义序列把alert写成\u0061\u006c\u0065\u0072\u0074但这个手法的前提是过滤器不认识Unicode转义适用面越来越窄。3. WAF绕过从编码、解析到传输层的思路3.1 编码绕过的多样化处理WAF对请求的检测核心流程是先解码再匹配。也就是说WAF要先对请求内容做URL解码、HTML实体解码、Unicode解码等操作把数据还原成最原始的形态再去和攻击特征库比对。绕过的思路就藏在这个解码机制里用WAF不认识的编码方式或不会处理的层数来包裹payload。URL编码是最基础的。WAF通常会把%3C还原成把%73%63%72%69%70%74还原成script所以单层URL编码在现代WAF面前几乎没有价值。但如果是重复编码比如%25253C这种形态第一次解码得到%253C第二次解码才得到%3C第三次才还原成如果WAF只做了一层解码就会漏掉。HTML实体编码是另一种常见手法。把写成#60;、把写成#62;或者用十六进制形态#x3C;。HTML解析器在处理属性值时是支持实体解码的这在标签属性内部尤其有用。比如一个输入场景里系统把script直接过滤了但属性值内部的HTML实体没有被处理等浏览器解析到事件属性时实体被还原成原始字符代码就执行了。Unicode编码主要用于JS代码层面的混淆。JS字符串里的\u003c会被还原成但WAF的特征匹配如果只看ASCII形态就发现不了这个特征。这个技巧在DOM型XSS里效果很好因为DOM操作的取值经常发生在JS运行时服务端根本看不到完整payload。下面这张表是我在实际绕过中整理出来的编码层级对照可以作为测试时的优先级参考编码方式绕过效果WAF识别难度适用场景单层URL编码基本无效极低不用试双重URL编码中等中过滤函数只decode一次时可过HTML实体编码十进制中等中属性值注入、标签内注入HTML实体编码十六进制中等中十进制被过滤时Unicode/JS转义较高高DOM型XSS、JS上下文注入混合编码叠加较高高在WAF解码层数被限制时3.2 利用解析差异与等价替换WAF和浏览器最大的区别在于WAF是正则匹配文本浏览器是状态机解析语义。这就导致了一个现象WAF认为不是攻击的文本形态浏览器解析后可能是合法可执行的代码。MIME类型混淆是典型场景。WAF在检测时如果发现Content-Type是text/plain或者application/json可能直接跳过对响应内容的检测因为它认为这不是HTML页面。但如果目标网站在输出时没有严格遵循Content-Type声明或者前端用fetch拉取后通过innerHTML渲染这个内容最终仍然可能被当成HTML执行。实际测试中可以修改请求的Accept头或Content-Type头观察响应中有没有被WAF标记如果发现响应内容不变但WAF行为变化就说明检测规则和内容类型挂钩。浏览器解析容错性差异也是一类很有意思的绕过。比如浏览器会把img srcx onerroralert(1)这种缺少右尖括号的标签正常解析因为HTML5解析器在遇到标签未闭合时会自动处理。WAF的特征匹配往往要求闭合完整面对这种半截形态反而识别不出来。另外在标签名和属性之间插入换行、Tab、空字符浏览器可能正常解析但正则匹配的\s在某些规则集里不包含\v、\f这些垂直制表符造成绕过。等价替换的核心是利用“功能相同但特征不同”的代码形态。比如alert(document.cookie)和alert(document[cookie])功能相同但后者用方括号代替了点号如果WAF特征匹配的是document.cookie这个精确字符串后者就能绕过。同样window.location可以写成location[href]String.fromCharCode可以配合atob完成编码执行。函数调用的形式也可以替换eval可以换成Function构造函数。我在测试中积累的一个经验是WAF的规则集无论如何更新都覆盖不了JS语言本身的表达多样性换一种表达式往往就能躲过精确匹配。3.3 传输层面的WAF绕过方法传输层面的绕过经常被忽略但效果很多时候比编码绕过更明显。原因是很多WAF在解析HTTP协议时存在缺陷对某些内容类型、某些传输方式没有做完整的检测。分块传输编码是经典手法。HTTP/1.1的分块传输Transfer-Encoding: chunked允许把body分成多个chunk发送每个chunk前面用十六进制标注长度。如果WAF只检测了Content-Length模式下的body没解析chunked格式payload就能被完整发送过去。实际操作中要注意每个chunk的长度值要和实际数据字节数匹配不能乱填否则服务端解析会出错。利用Content-Type解析差异也值得一试。把请求的Content-Type从application/x-www-form-urlencoded改成multipart/form-data格式变为boundary分隔字段很多WAF的规则集对multipart里的内容解码不完整或者只检查了name值没有检查filename值。这个办法在文件上传功能点测试时特别好用filename里面塞XSS payload后端如果直接把文件名回显或拼进页面就能触发XSS。参数污染Parameter Pollution是一个容易被忽略但非常经典的传输层绕过方式。同一个参数名提交多次后端取值的逻辑和WAF取值的逻辑可能不一致。常见的后端取值优先级有取第一个、取最后一个、拼接所有值几种。WAF如果取第一个值检测发现没问题但后端取最后一个值拼进页面就会绕过。用payload和正常值的组合多组提交看回显位置取的是哪个参数然后反向构造即可。这个技巧在真实站点的测试中成功率很高因为我发现相当数量的Java框架取参数默认取第一个而PHP默认取最后一个WAF在规则设计时往往只按一种协议栈行为做适配。传输层的绕过还需要注意HTTP方法对WAF规则的影响。有的WAF只检测GET请求的query string对POST的body检测力度较弱有的则相反。我的习惯是同一个payload分别用GET、POST、PUT、HEAD、OPTIONS轮着试一遍经常能发现某一种方法完全不在检测规则里。4. 实战流程从探测到构造PoC的完整过程4.1 环境准备与授权说明下面用一个虚构的测试环境演示完整的绕过流程。假设我们在本地搭建了一个名叫“某后台内容管理系统”的测试平台该系统有一个文章标题搜索功能参数为keyword服务端做了基础过滤把script标签中括号和onerror替换为空部署了WAF云防护对常见的img onerror组合有拦截规则。这个场景是测试中最典型的一种配置本地代码层过滤云端WAF双重防护。先强调一个实操原则所有测试开始前先确认测试范围和授权文件。我在内部培训和带新人时会先花五分钟讲这个技术上不管多简单多复杂的站没授权就是不能碰。自己搭环境也好、在授权的靶场里练也好都不会影响你对技术的掌握程度反而能让你更从容地分析流量、调试payload。环境准备上建议至少准备两样工具一个是Burp Suite用来抓包、改包、重放请求另一个是浏览器DevTools的Console和Network面板用来观察页面行为、截获JS执行结果。还需要掌握一个基础技能用Console手动执行document.cookie、document.documentElement.outerHTML这类命令验证XSS是否真正触发以及影响范围。4.2 分步探测流程整个探测流程分成四步走走完这一步再进下一步不要跳步。第一步是摸清过滤规则。我习惯先从最简单的payload开始发逐个查看响应变化。先发scriptalert(1)/script看页面原样返回、过滤后返回、还是直接拦截。再发img srcx onerroralert(1)观察过滤点。如果响应里“img”还在、“onerror”没了说明过滤了事件关键字如果整个img标签都没了说明过滤了标签名如果响应状态是403或406说明触发了WAF规则。这一步的信息量很大能帮你快速判断是代码过滤器还是WAF在起作用以及两者各自过滤了哪些关键字。第二步是根据探测结果调整标签和事件。假如发现script被过滤但img没被过滤onerror被替换为空那就用双写思路试oonnerror。假如双写也被过滤试大小写变体OnError。假如大小写也不行考虑换标签和事件组合比如svg onload、body onload、a hrefjavascript:等。执行JS的函数名如果被过滤用prompt(1)或confirm(1)替换alert(1)。第三步是处理WAF层的拦截。如果在探测过程中出现403响应说明WAF已识别到攻击特征。先把请求改得“干净”去掉明显特征再用编码分层次地包裹payload。从单层URL编码开始逐步增加深度每次只改变一层直到WAF不再拦截。然后再看服务端回显确认过滤后的内容是否还能被浏览器二次解析。第四步是验证DOM型执行。很多时候payload过了一切检测但在页面上没有反应。这时要打开DevTools的Sources面板搜索payload在哪个JS文件里被引用看它拼到DOM的什么位置。如果被拼到HTML属性里事件触发方式可能和拼到文本节点里完全不同。如果被赋值给innerHTML属性浏览器会重新解析子节点但你拼接的标签如果放在一个已有的标签内部解析器可能会把结构重新调整造成payload变形这时需要根据实际渲染结果不断微调标签结构。4.3 验证与修复验证当payload在浏览器中成功执行后不要急着收工。一个合格的报告至少需要覆盖三块触发点位置、执行效果、修复建议。触发点位置要明确到具体参数和上下文。比如“搜索页keyword参数反射到h3标签的文本内容里”。执行效果要写清楚证明方式常用的是弹窗截图、Console输出截图、Cookie获取验证。如果是在授权范围内的测试可以用document.cookie是否成功回传来说明影响但这里不讨论任何具体系统的收集利用细节仅做防御性验证。修复验证这一步容易被忽略。我的习惯是提交漏洞报告后隔一段时间再回去测试一次修复效果。很多开发的修复只是简单加了一个过滤函数没有覆盖所有输入点或者只在入库时做了处理出库时没用。你第一次测试时的payload可能已经被拦截了但换一种编码方式又通了这就是修复不彻底的表现。做安全测试不只是发现问题还要验证修复的有效性这也是一个测试人员的专业分水岭。5. 常见问题与排查技巧实录5.1 常见拦截原因排查这些年帮人排查过不少XSS绕过不成功的问题总结下来最常见的就那么几类原因列成表格方便对照现象可能原因排查建议payload被完整返回但不弹窗内容被转义为纯文本查看响应中是否被加了lt;或amp;标签前半部分正常后半部分丢失服务端做了正则截断检查响应中标签的闭合状态弹窗内容被替换成其他内容内置了CSP策略查看响应头里Content-Security-Policy部分浏览器弹窗部分不弹事件触发方式依赖用户交互换成onerror等自动触发方式直接返回403或406WAF拦截更换编码层数、参数位置、提交方式本地访问正常线上不弹CDN或云WAF缓存污染清缓存或用无痕窗口测试payload只在控制台报错不执行JS语法被过滤干扰将payload粘贴到Console检查语法错误CSP这个问题值得单独说。近几年越来越多的后台管理系统接入了CSP响应头如果页面设置了Content-Security-Policy: default-src self这类策略即使XSS点在内联的script代码也不会执行。遇到CSP限制时需要优先看有没有可用的CSP绕过点比如响应头里是否允许unsafe-inline是否存在可用的script-src白名单域名或者是否有JSONP接口可以帮助加载外部脚本。这些内容展开说又是一大篇这里先记住看到页面被CSP拦截时不要死磕内联脚本先把CSP配置读一遍。5.2 实用调试技巧最后分享几个调试层面的个人习惯都是在实际测试中沉淀下来的。第一个习惯每次改payload只改一个变量。很多新手喜欢一次改三个地方编码、标签、事件全部换掉通了也不知道为什么通不通过也不知道为什么不过。正确做法是保持其他元素不变单点调试。第一次加一层URL编码通了说明过滤点在解码层第二次改为双写事件名通了说明过滤的是关键字替换。这样每步改动的测试结果都能直接对应到防护逻辑。第二个习惯在Chrome的Console里提前调试JS表达式。所有准备放入payload的函数、属性访问、字符串拼接先在当前页面的Console里跑一遍确认语法没问题、函数名没拼错再放进HTTP请求里测试。这样做能省很多来回发包的功夫因为很多payload不执行的原因是JS语法错误根本不到触发环节。第三个习惯分类管理payload库。我自己维护了一份本地payload清单按标签类型、事件类型、编码方式、绕过场景分目录整理。遇到新环境时从清单里选最匹配的组合穷举而不是每次都现场拼。花半天时间整理这份清单后续可能省你几十个小时的测试时间。第四个习惯善于利用无痕窗口和不同浏览器测试。同一个payload在Chrome、Firefox、Edge里可能有完全不同的解析和渲染结果尤其是涉及HTML5新标签、SVG、MathML命名空间、旧特性和过时API的时候。兼容性测试不只是开发的事做XSS验证时多换几个浏览器你会发现新世界。还有一个小技巧是观察HTTP响应头。WAF和云防护产品在拦截时往往会在响应头里留下标记字段比如X-WAF-Rules、Server: AliyunWAF这类指纹信息。提前从响应头识别出WAF厂商和版本在构造绕过时可以参考该产品公开的绕过案例和研究文章有的放矢地调整策略比盲猜高效得多。做安全测试这件事经验积累很重要但比经验更重要的是思路体系。XSS过滤绕过走到最后拼的不是payload样本量而是对浏览器解析机制、对服务端过滤逻辑、对WAF检测原理的底层理解。底层逻辑通了遇到没有见过的过滤规则也能快速推导出可行的绕过路径。这篇文章整理的是我的实战方法论希望能帮你在测试路上少走几步弯路也希望大家每一次点击和验证都发生在合法授权的边界之内。
返回列表