尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

XSS绕过实战指南:从过滤失效到WAF绕过的核心思路

XSS绕过实战指南:从过滤失效到WAF绕过的核心思路 1. 为什么过滤函数挡不住XSS从黑名单失效说起先聊一个很多人踩过的场景你把用户输入里的script替换成空字符串自以为万事大吉结果一个scrscriptipt就给你把代码弹了出来。这种经历基本每个做Web安全的兄弟都有过也是 XSS 绕过这条路的起点。1.1 过滤的本质是猜攻击面注定有盲区黑名单过滤的逻辑很简单我知道攻击者可能发什么我就把那些危险的东西删掉。问题是攻击面不是一个静态清单而是HTML、CSS、JavaScript、URL解析等多个标准叠加形成的组合空间。举一个最基础的例子服务端如果只过滤了script标签就能拦住所有脚本执行吗明显不能。img srcx onerroralert(1)不需要script标签svg/onloadalert(1)也不需要details open ontogglealert(1)同样不需要。只要HTML里存在能触发事件的元素就有执行JS的可能。这就是过滤函数失效的第一个原因你防的是一个标签但攻击者可以用整个HTML语言来攻击你。第二个原因更隐蔽服务器和浏览器的解析方式经常不一致。服务端过滤时用的是正则匹配而浏览器解析HTML时有自己的一套状态机逻辑。一个payload在服务端眼里是无害字符串到了浏览器那里可能就变成了可执行代码。绕过往往就藏在这层解析差异里。1.2 输入输出上下文的差异同一个payload两种命运很多开发者在做过滤时会忽略一个关键问题用户输入到底会出现在HTML的哪个位置。同一个scriptalert(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全部支持detailsontoggle事件打开可自动触发多数现代浏览器marqueeonstart事件页面加载自动触发浏览器兼容性佳video/audioonerror、onloadstart全部支持math可搭配事件属性部分环境可用iframesrcdoc属性内嵌HTMLonload触发全部支持ahref的javascript伪协议用户交互触发这里面details ontoggle和svg onload是我做测试时命中率最高的两个因为它们不需要用户额外操作页面加载或元素渲染时就能触发。2.2 事件属性的存活条件是过滤不够还是解析特殊标签级别的过滤如果做得好剩下的突破口基本都在事件属性上。但事件属性绕过的前提是标签本身没有被完全拦截。比如输入被放到img src[输入点]的src属性里你的输入内容被限制在了引号内。很多过滤规则只关注你有没有或忽略了属性内部本身就可以闭合和构造新攻击面。构造一个经典的属性内XSS onerroralert(document.cookie)如果src的值被拼接到HTML里闭合掉原有的属性onerror变成img标签的新属性双引号包住攻击代码。最终渲染出来的HTML就变成了img src onerroralert(document.cookie)图片加载失败触发onerror脚本执行。这个手法的难点不在于构造payload而在于搞清楚引号要不要被HTML实体编码如果编码了浏览器在解析属性值时会不会解码回来很多过滤方案是把双引号转成quot;但浏览器在HTML属性上下文里会把quot;还原成导致原本的编码保护形同虚设。这种错位经常被忽略。2.3 引用符逃逸与属性截断实战如果你输入的内容里引号被严格转义了连quot;这条路都堵死了是不是就没办法了其实还有一个思路不用引号利用HTML属性在某些场景下的反引号或空格分隔规则。有些过滤代码会过滤单引号和双引号但漏了反引号。在HTML属性里反引号在部分旧版浏览器中是可以作为属性引号使用的所以img src onerroralert(1)这种写法在特定环境下依然能构成攻击链。再往深处走如果连反引号也被处理了你还可以考虑属性截断。逻辑是这样的某些过滤函数会在输入里检测危险单词一旦发现就替换成空但如果危险单词被拆分成两段拼接过滤就失效了。比如输入onerror会被删那就用onerror的拼接方式——但这是拼接型思路通常用于输出点在JS代码上下文里。HTML属性上下文更适合的招式是换行分割。img srcx\nonerroralert(1)在部分浏览器的解析逻辑里属性名和属性值之间的换行会被当作空白处理onerror照样被识别成事件属性。如果你的过滤规则是精确匹配onerror这种连续字符串那这个payload就能穿透。3. WAF正则规则下的三层绕过路径WAF比普通的服务端过滤要狠得多它的规则库覆盖了常见payload、编码形式、组合攻击。想在WAF面前突围得换一个思路不跟WAF的正则硬碰硬而是利用协议解析差异绕过去。3.1 编码层HTML实体、URL编码与二次解码陷阱WAF的核心工作方式是先拿到请求内容然后匹配规则库。所以绕过的第一个方向就是让WAF看到的和服务器最终处理的内容不一样。HTML实体编码是最基础的手法。svg/onloadalert(1)如果被WAF直接拦截可以试试把字母用HTML实体编码svg/onload#97;lert(1)这里的#97;是字母a的十进制实体。浏览器在解析HTML属性值时会先解码实体再当作JavaScript执行。WAF如果只针对明文规则做匹配就会漏掉这种形式。URL编码在GET参数场景下很常见。WAF可能会对URL参数值做一次解码然后再匹配但有些WAF只解码一层。如果服务端还会二次解码那就出现了时间差。举个典型例子参数值是%2526WAF解码一次得到%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 hrefjavascript: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/divscriptalert(1)/script如果回显出现在input value...这样的属性里那思路就变为闭合引号和标签test123svg onloadalert(1)皮卡丘这关通常是不设防的直接scriptalert(1)/script就能过。但你如果只做到这一步那你对绕过的理解其实没有真正建立起来。我会建议把过关标准提高一点假设这里的服务端过滤了script关键字你怎么继续过试试scrscriptipt自拼接、img srcx onerroralert(1)事件触发、svg onloadalert(1)SVG标签然后逐一把这些payload当过关条件来跑。皮卡丘的好处就是环境简单、干扰少适合建立先探测上下文再构造payload的完整思路。4.2 iwebsec关卡的不同过滤强度递进iwebsec靶场在XSS题目的设计上比皮卡丘多了一个维度每一关的过滤强度是递进的模拟了不同安全等级的代码实现。通关的节奏可以这样安排第一关通常只是简单的关键字替换把自己写的拦截正则找出来用大小写混写就能过。第二关可能会过滤onerror这类常见事件名但标签本身没卡死可以尝试details ontoggle或svg onload切换触发方式。第三关连常见标签和事件一起过滤这时候要开始考虑编码绕过了比如HTML实体编码、URL编码看服务端是否存在解码逻辑。第四关假如引入了类似简单WAF的规则那就按前面讲的结构层方法插换行、插注释、混编码逐条测。有一个我自己踩过的坑在iwebsec某一关里payload里同时出现了script和alertWAF检测到alert就拦截当时的绕法是把alert改写成top[alert]或者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的兜底方案即使编码做得再全面也依然有可能因为业务逻辑复杂、拼接点众多而遗漏某个上下文。这时候就需要CSPContent 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.write、innerHTML、location等关键函数的数据流关闭页面缓存、打上断点逐步看。这里最有效的工具就是浏览器开发者工具的Sources面板加Console。第五条每个payload都做最小化。构造绕过payload时不要一上来就整段复杂代码先把目标拆成最小验证单元。比如只验证svg onload1能不能触发弹窗能触发后再加功能代码。最小化能让你快速定位是标签被拦了还是事件属性被拦了还是函数执行被拦了避免把多个变量混在一起排查不出来。XSS绕过的门道说来很多但说到底还是那件事——理解浏览器和服务器对同一段输入的不同理解然后利用这种差异。把这套思维建立起来后面不管碰到什么样的过滤、什么样的WAF你都能顺着上下文快速找到穿过它的角度。这也是这个领域最让人上瘾的地方。
返回列表