资讯详情

XSS绕过实战指南:从编码原理到防御全解析

📅 2026/10/21 5:51:19 | 华诺云谱 👁 阅读
XSS绕过实战指南:从编码原理到防御全解析
写这篇的原因是我自己早期学XSS的时候走了太多弯路。网上大多数资料只会丢payload列表告诉你“这样打就完事了”但没人讲清楚为什么能绕过、什么时候无效、以及真正决定绕过成败的那几步判断到底怎么做。后来做了一段时间的实际测试和防护审计才慢慢整理出一套自己的判断逻辑。这篇把多年积累的XSS绕过姿势、触发原理和排错思路全部摊开来讲从零基础视角切入但内容深度也适合已经入门的同学用来补全自己的武器库。如果你眼下正卡在“明明payload没错但就是不弹窗”或者“过滤规则一变就不知道怎么打了”这篇就是给你准备的。1. 认知起点绕过的本质不是对抗过滤而是争夺解释权很多初学者有一个误区觉得绕过XSS过滤器就是不断堆更复杂的payload像是魔高一尺道高一丈的拼刺刀。其实不是。绕过的本质比这个优雅得多你和防御系统在争夺对同一段字符串的最终解释权。一段数据进入Web应用之后至少要经过几层处理才落到浏览器里。服务端可能先做输入校验、过滤、编码、截断输出时又有各种模板引擎的转义浏览器收到之后还要按HTML解析器、URL解析器、JavaScript解析器的规则分别去解释。每一层都有自己理解的“语言”关键问题在于你的payload最终被哪一层解释用什么字符集解释解释完剩下什么东西。理解这一点之后所有绕过姿势都可以归结为同一个思路找一个过滤规则没覆盖到的解释层把恶意代码藏进去等更内层的解析器把它还原出来。举个直白的类比。一座仓库门口有安检检查员只检查纸箱外有没有写“危险品”三个字。你往纸箱里塞一个贴着“易碎品”标签的小盒子小盒子里面才是真东西。检查员放行了仓库内部的人拆开小盒子才知道原来是危险品。服务端过滤器是门口检查员浏览器内部的各个解析器是仓库里负责拆箱的人。你每次要做的就是找到那个“检查员不查但拆箱人会认”的包装方式。理解了这一点下面所有姿势就不是死记硬背的payload集合而是你自己可以推演出来的方法分支。遇到任何新过滤器你都可以先问三个问题这个过滤器作用在哪一层它过滤的是原始输入、解码后的数据、还是输出渲染后的数据我输入的数据随后会进入哪个上下文HTML标签里、属性里、JS字符串里、URL里还是CSS里浏览器从接收到渲染要经过哪几次解码有哪些解码结果过滤器没算到这三个问题才是XSS攻防的真正主线。后面的每一节都是这条主线上不同节点的展开。2. 编码绕过所有编码都是包装纸关键是过滤器拆到哪层停手编码类绕过是入门XSS时最早接触、也最容易被低估的一类姿势。我见过的初学者常犯的错误是把所有编码方式堆在一起乱试%3C,\u003c,#x3c;,#60;,lt;……一把梭问为什么不生效没人能答上来。这就是典型的没有建立“解码链路”观念。2.1 先搞懂你面对的是哪种解码器Web里有几套互不相通的编码体系而且它们只在特定的解析器里生效。这里列一个简洁的对应关系建议先刻在脑子里编码形式示例生效位置URL百分号编码%3C%22URL地址解析阶段比如a href、跳转、服务端路由参数HTML实体编码#60;#x3c;lt;quot;HTML解析器文本节点和属性值里都会被解码JavaScript Unicode转义\u003c\x3cJavaScript字符串、标识符、正则里被JS引擎解码CSS转义\3c注意后面要跟空格或终止符CSS解析器比如style属性、CSS文件里的内容每一种编码只有在对应解析器里才有意义。你把\u003c直接放进HTML文本节点浏览器不会把它当成来解析你把#60;直接放进JS字符串里它也不会自动变成。所以编码绕过的第一步不是选编码而是先判断你的数据落点落在哪个解析器管辖范围。有次我测一个搜索框输出被原封不动反映到页面的title标签里试了一整轮%3Cscript%3E和\u003cscript\u003e都没反应当时差点以为过滤很强。后来才反应过来title是HTML文本解析上下文URL编码和JS转义在那一层根本不会被还原只有HTML实体编码才会被解析器翻译成尖括号。2.2 多层解码的叠罗汉效应真正的难点在于很多数据不是只经过一次解码。服务器可能在接收时对输入做一次URL解码模板引擎又对输出做一次HTML实体转义浏览器随后再做一次HTML实体解码。如果其中有任何一层过滤器只检查解码前的原始形态或者只检查解码后的最终形态就可能留下解码链路中的时间差漏洞。实际操作中我总结了一个固定套路对你的payload做双重或三重包装让最外层的包装属于过滤器认识但也容易被内层解码还原的那一类。经典的例子是#x25;3Cscript#x25;3E这里的#x25;是%的HTML实体编码。浏览器解析HTML时先把它还原成%于是整个值在后续URL解析阶段又变成%3Cscript%3E再被URL解码还原成script。如果过滤器只查了一层看原始输入时发现没有尖括号和script字样直接放行等于你成功用叠罗汉骗过了安检。类似的组合还有在JS上下文中把字符做\u转义之后再对反斜杠本身做HTML实体编码。过滤系统如果只做正则去匹配\u003c字样就可能被#92;u003c这类形态混淆因为浏览器先把#92;还原成\JS引擎随后才把\u003c还原成。2.3 编码绕过的三个实操判断节点这里给出我每次遇到编码场景时都会走的检查清单用于快速判断哪些编码组合值得尝试先确认输出点上下文。文本节点、属性值、JS字符串、URL参数、CSS值这五类上下文决定了初始编码方向。再确认到达输出点之前经历了哪些转换。去看服务端有没有urldecode、htmlspecialchars、stripslashes这类操作模板有没有二次转义数据库存储是否做了额外的字符集转换。每一层转换都是一次可以用来藏东西的机会。最后做验证时不要只试一次。正确的验证方式是逐层手动解码把payload按照你推测的解码顺序亲手解一遍如果最终能还原出script、javascript:或onerror那这条链路就是通的。编码绕过的本质不是“多套几层总有侥幸”而是你能够复现浏览器完整的解码流程图。能复现你就能找到过滤盲区不能复现你只是在掷骰子。3. 标签与属性姿势从“找标签”变成“找解析器的宽容度”很多人一提到XSS就只会说script和img onerror但实际测试里你会发现script是最容易被拦的img的老套payload也早被各种黑名单盯上了。标签层面的绕过真正有价值的是站在HTML解析器视角去思考哪些标签的解析方式天然给攻击者留了空间3.1 不带script字样也能执行JS的标签家族HTML里能触发脚本的标签远不止script。常见的可执行脚本标签包括img、svg、video、audio、body、input、details、math等关键在于配合事件属性或特殊的自动触发机制。为什么有些标签反而好打因为它们不需要用户交互就能触发或者触发条件在解析阶段就满足了。一个很典型的场景是黑名单过滤了常见的事件名比如把onerror、onload都换掉了。这时候details配合ontoggle往往有用因为很多过滤器根本没有把ontoggle写进黑名单。同样的逻辑marquee的onstart、video的oncanplay、audio的onplay也都能用。原理很简单黑名单永远追不齐所有事件属性而HTML规范允许的事件属性有上百个。3.2 属性值的闭合与“不闭合”策略属性闭合的经典形态是svg/onloadalert(1)这行的意思是提前用双引号闭合前面的属性再用关闭当前标签然后自己构造一个新的svg标签并配合事件属性执行脚本。这是最基础的思路但实际测试中经常会遇到两种情况让它失效第一种开发者做了引号转义把双引号变成了quot;。此时可以尝试单引号或者尝试不闭合引号而是利用某些属性允许“非法字符”被解析器自动跳过。比如 autofocus onfocusalert(1) x这段在不闭合引号的情况下通过autofocus让元素自动获得焦点onfocus立刻触发后面的x只是用来吞掉原来的闭合引号。这种技巧在过滤引号的场景里特别好用因为攻击者根本不需要输出一个干净的引号。另一种情况更隐蔽过滤器把尖括号也处理了属性值里不能构造新标签。这时候就不能把攻击点放在创建新元素上而是利用当前元素本身的属性和事件。比如输入被插到一个div属性链里input value用户输入 idx如果输入没做输出编码可以直接构造 onmouseoveralert(1)输入里的引号闭合了value后面补了事件属性再补一个引号让属性链继续闭合。整个过程没有新标签、没有尖括号只靠拼接事件属性完成注入。这种“在原标签内部做文章”的思路在过滤器对尖括号严防死守时非常重要。3.3 语法噪音也能救命空格、换行、注释的解析规则HTML解析器在标签和属性名的识别上有相当高的宽容度大量不可见字符和格式字符可以混进标签结构里而浏览器根本不在乎。这意味着过滤器里的正则如果写得太严格很容易被这些噪音字符拆碎绕过。常见的可插入字符包括水平制表符\t、换行符\n、回车符\r、以及各种Unicode空白字符。比如svg\nonloadalert(1)标签名和属性之间的正统语法是空格但换行符同样被HTML解析器当作合法分隔符。过滤器如果只匹配svg onload这种带空格的模式而对换行版本没做映射这一条就直接穿过去了。我早期做渗透测试时遇到过一次过滤规则相当严格的情况把常见标签和事件属性全部替换成空。我当时的突破点就是用注释符!--切断了标签名构造出这种形态scr!-- --iptalert(1)/scr!-- --iptHTML解析器在读取标签名时看到scr随后进入注释状态跳过中间内容直到遇到后恢复最终解析出的标签名依然是完整的script。过滤器的正则匹配的是原文它看到的script被注释切成了两半自然匹配不上浏览器却不这么认为它在语法分析时把注释部分当作token之间的空隙忽略掉了。这个思路后来被我扩展到属性名和事件名上只要HTML允许的空白和注释位都能做文章效果出奇地好。值得注意的是注释绕过对现代浏览器的部分标签场景不一定总是生效因为不同标签的解析容错细节有差异所以测试时必须现场验证不能看完一篇资料就认定某个姿势在所有浏览器里都通用。4. 脏数据与大写小写、字符集的边角料利用很多人在过滤器面前容易犯“洁癖”payload看起来不够干净就觉得不该试。实际上大量绕过恰恰是靠脏数据完成的。脏数据本身的恶意代码不多但它本质上是用来扰乱过滤器判断的噪音信号。4.1 大小写混写真的过时了吗有一种说法是大小写绕过早就被所有过滤器防住了。这个说法不完全对。确实大部分现代过滤器都加了strtolower一类的规范化步骤对sCrIpT这种直接大小写混写做了处理但问题在于过滤器的规范化经常只做一层而且不一定覆盖到所有解析阶段。我实际测过不少系统虽然服务端做了小写化但输出模板在渲染时使用的是原本输入。也就是说如果过滤发生在A阶段而模板最终输出的是B阶段的原始值大小写混合依然能钻空子。另外事件属性名在HTML里也是大小写不敏感的oNeRrOr、ONERROR、OnError都能触发完全没必要只写小写。所以我的建议是大小写混写不必作为首选姿势但是不能把它从测试字典里删掉尤其是遇到自定义黑名单过滤器的场景先花30秒试一遍大小写组合的成本非常低。4.2 空白字符和控制字符的“隐形武器”XSS过滤器的本质大多是基于正则表达式的黑名单匹配。黑名单的弱点在于它只能匹配预先写好的字节序列遇到“长得像但不是它记住的那串”就会失手。而HTML规范对字符的宽松程度正好给这种失手提供了海量机会。举个例子。HTML标签属性之间的分隔符按规范其实是“空白字符”whitespace而不是单纯的空格。哪些字符算空白字符空格、换行、回车、制表符还有Unicode里一大票像\u000B垂直制表符、\u000C换页符、\u00A0不间断空格之类的隐形字符。如果过滤器只挑空格和Tab做分隔符校验用这些冷门空白字符替换一试就知道结果了。控制字符也有类似效果。有些系统会直接过滤/、等符号但如果你用HTML实体把等号编码成#61;再用前面说的多层解码链路还原过滤器可能完全没感觉。脏数据绕过的核心就是让过滤器的正则匹配不到一个“干净”的恶意token但浏览器的解析结果依然能还原成那个token。4.3 引号的“换一种写法”思路过滤引号是常见防护措施尤其当开发者知道自己的输出点在属性里时他们总会对双引号和单引号做转义。但引号本身并不是属性的必需字符HTML规范允许属性值不加引号。例如input valueuserinput idx如果输入是onmouseoveralert(1)这样的内容并且前边有空格分隔它会被解析成新的属性。虽然没有引号包裹很多浏览器照样能正常触发事件。当然这要求输入位置恰好能构成属性分隔的形态实际操作中需要根据输出上下文微调payload。与此对应的是现代浏览器对反引号在JS中字符串的识别让一些变体变得可用。比如事件属性里可以写onmouseoveralert(1)这里用反引号完全避开单双引号而且模板字符串在很多浏览器里等同于普通字符串alert(1)照样能弹。如果过滤器把alert(1)这类经典形态全部拉黑可以试着在参数里做点文章比如alert(11)、alert(11)、alert(document.domain)这些变体都不是黑名单能覆盖完的。4.4 大小写之外同形字符与全角半角还有一个经常被忽略的向量是Unicode同形字符。字母a、s、c这些字符有各种看起来一模一样的全角形式比如。大多数过滤器的正则只匹配ASCII字符遇到全角字符直接放过。但是如果服务端和浏览器的字符集处理环节存在转换比如UTF-8转UTF-16再转UTF-8全角字符可能在某个转换环节被标准化成ASCII导致你的全角payload最后被浏览器还原成真正的标签名。这个技术点在实际利用中有时受到浏览器和中间件的多种条件限制稳定性不一定好但它属于那种“测一下不亏试出来就是大突破”的姿势。对于初学者我的建议是不必把这些脏数据姿势背到滚瓜烂熟而是建立一张“过滤器可能不会算进去的字符表”每次卡住了就挨个替换效率远高于死磕单一payload。5. 语义分割与无中生有把长payload化整为零长度限制和敏感词过滤经常联手出现很多系统直接限制输入长度导致完整的svg onloadalert(1)根本贴不进一个字段。这种时候有效的方法不是压缩payload而是利用HTML解析器的容错机制把payload分拆到多个字段或多次请求里。5.1 利用form属性实现跨元素关联HTML5引入了form属性允许一个表单控件比如input、button挂在页面上的任意表单之下只要form属性的值与目标表单的id一致。这个特性有一种很刁钻的用法把真正的可执行片段分散在多个输入点再利用浏览器支持跨元素关联的特性把它们串成一个整体。考虑一个场景站点允许多个字段分别输入但每个字段都有长度限制和敏感词过滤任何单一字段都无法承载完整的XSS payload。你可以把payload拆成两部分一个输入点写入idx和表单关联信息另一个输入点写入事件触发器。虽然单看每一段都不完整但浏览器解析后会把它们拼在一起触发完整逻辑。5.2 利用标签的自动闭合特性拆分属性某些标签的属性在解析时遇到特定字符会自动闭合或被忽略攻击者可以借此把payload放在一个看似无害的属性值里。比如svg/onloadalert(1)这里svg后面紧跟/再跟onload属性。/在标签内表示自闭合标记浏览器允许svg/后直接接属性然后遇到时完成标签闭合并触发onload事件。整个payload短、没有空格、没有引号非常适合长度受限和空格被过滤的场景。类似的短payload还有svg/onloadalert(1)//结尾的//在HTML里代表注释开头实际上它只是让标签在语法上显得更完整。这种形态是我在长度限制比较严格的情境下经常优先测试的。5.3 多次存储二次拼接还有一种思路在存储型XSS里特别有用利用站点的多个存储点把payload分成几段每段自身是安全的、中性的文字但在特定页面渲染时几段的边界正好拼成一个完整的攻击载体。举例来说某个评论区允许用户输入URL和昵称服务端分别把它们插入到同一个a标签的不同位置。昵称作为链接文字URL作为href值。如果昵称控制在“闭合属性”的位置URL控制在“事件属性”的位置两个字段单独看都是无意义的字符串合在一起就是一个完整的注入。这种攻击链不依赖任何危险字段的长度只需要你对目标页面的输出结构足够了解。我遇到过一次真实情况某个功能把用户输入的“标题”输出在页面的a title...里“描述”输出在其他属性位。我利用标题字段插入右引号和空格再用描述字段补上事件属性成功拼出了一个不落地在任何一个字段里的完整注入。那次之后我意识到存储型XSS的绕过对象不是过滤器而是整个页面组装逻辑。5.4 过滤alert等函数名时怎么办很多防护系统把常见函数名如alert也拉进了黑名单。此时可以通过间接调用来绕过。最简单的思路是使用confirm、prompt这些同样能弹窗的函数因为黑名单往往只覆盖alert这一个最知名的名字。如果没有弹窗需求只是想确认XSS存在也可以用console.log、fetch、甚至throw 1来触发可见行为。另外可以通过给函数名补充无害的语法成分来避免直接命中黑名单alert(1) alert (1) alert(11) (window.alert)(1) top[alert](1)这些变体一次一次试总有一个会绕过只看固定字样的过滤器。值得留心的是有些过滤器会做“去除空白再检查”的预处理所以alert (1)这种中间带空格的形态不一定有效但表达式拼接的方式比如top[alert]在字符串真正执行之前不会拼出alert所以对很多黑名单来说反而是硬杀器。6. 输出位置决定打法上下文专项绕过很多初学者最大的盲区是不知道同一条payload在不同上下文里的命运完全不一样。如果不先判别输出位置直接套payload模板经常会白白浪费时间。这一节我把最常见的几个上下文场景拆开来讲每个场景的绕过策略差异非常大。6.1 HTML元素内的文本节点如果用户输入直接出现在div、span、p等标签的内部那么最直接的目标就是跳出文本区域创建新标签div这里插入用户输入/div此时payload可以是svg onloadalert(1)但如果尖括号被编码或过滤就不能走这个方向需要另想别的路。文本节点的本质是除非能闭合当前标签并开新标签否则事件属性不会生效。若无法突破尖括号可以确认是否存在其他方式触发危险操作如果确实没有那么这里唯一能做的就是尝试前面提到的各种HTML实体编码或脏数据组合看能否骗过过滤器还原尖括号。6.2 HTML属性内部属性上下文比文本节点更灵活因为你不一定需要创建新标签只要把当前标签改造成一个可执行事件的形态即可。比如input typetext value用户输入如果用户输入被原样输出在value属性值里可以构造 autofocus onfocusalert(1) x这样autofocus让输入框自动聚焦onfocus立刻执行最终实现无交互点击的触发。属性上下文中最大的敌人是引号被转义所以无引号属性值的利用思路尤其重要。如果开发者使用htmlspecialchars把双引号转义成quot;HTML解析器会把它还原成引号攻击者依然可以利用但如果服务端在存储层把引号替换成了全角引号或其他字符那这条路就不通。6.3 JavaScript代码字符串内部这是最容易出成果也最容易翻车的上下文因为浏览器执行JS时对转义字符的宽容度很高而对过滤器的判定却常常留下缺口。一个常见输出形态是var name 用户输入;如果输入没有被正确地JSON编码或转义直接可以闭合字符串并拼接语句;alert(1);//很多时候开发者会把双引号和反斜杠做转义但没处理/script闭合标签的问题。于是我们可以不走JS拼接的路而是直接闭合当前script标签// 输入/scriptscriptalert(1)//这种“先闭合再开新块”的技巧在过滤器只针对JS关键词做检查时非常高效因为尖括号和标签名往往不在JS过滤器关注的范围内。JS上下文里Unicode转义是绝对的宝藏。因为JS引擎会在字符串被解析之前先处理\u开头和\x开头的转义序列而过滤器的正则如果只匹配可见的危险词看到\u0061lert时是匹配不到alert的。下面是一段实际测试中很好用的形态\u0061lert(1)需要注意的是这种转义方式只在JS字符串和JS标识符解析阶段有效不能直接用在HTML属性里。6.4 URL字段与javascript:协议当用户输入被直接放进a href...或location.href时绕过的关键就是构造javascript:协议。常见的基础payload是a hrefjavascript:alert(1)很多过滤器会拦截javascript:这个关键词可以用大小写、URL编码、换行制表符来拆分。一个被广泛使用的技巧是java#x73;cript:alert(1)HTML解析器在读取属性值时先做实体解码把#x73;还原成s随后URL解析器和JS解析器才看到完整的javascript:。如果过滤发生在HTML实体解码之前它就拦不到这个形态。另外href属性在接受HTML实体之后还会经过URL解析而URL解析器允许很多字符被忽略比如java%0ascr%0aipt:alert(1)在部分浏览器的历史版本中%0a这类URL编码换行符会被忽略导致最终识别为javascript:协议。现代浏览器对这个粒度的容错有所收严所以这条不一定通但它揭示了URL上下文特有的测试方向不能只看服务端过滤还要看浏览器URL解析器自己的“清理规则”。6.5 CSS上下文与样式属性里的旁门左道CSS上下文相对冷门但对绕过者来说很有价值因为很多系统的CSS过滤器形同虚设。如果用户输入被放进style属性可以尝试div stylebackground:url(javascript:alert(1))现代浏览器大多不允许CSS里的javascript:协议执行但历史版本的浏览器或某些老旧Web视图组件仍可能触发。更实际的是利用CSS表达式仅在旧版IE中生效div stylewidth:expression(alert(1))这个在今天的测试中基本不推荐因为现代环境很难复现。比较有现实意义的是CSS上下文可以用来做钓鱼和数据窃取比如通过url()外带信息。从XSS角度看CSS上下文通常不是首选但它提醒我们任何输入位置都值得花时间推导而不是只盯着HTML和JS。7. 存储生命周期与二次触发别只盯着一次反射反射型XSS的整个生命周期很短输入、输出、触发一气呵成。存储型XSS则完全不同它的payload可以进入数据库等到其他用户访问某个页面时才被组装、输出、触发。这中间的延迟和组装过程为绕过提供了更多操作空间。我第一次做存储型XSS测试时卡了很久。原因是服务器端有很强的输入过滤直接在提交阶段就把尖括号、引号全部转义了。按照反射型的思路我在提交接口怎么试都不弹窗。后来我仔细梳理了数据的生命周期发现过滤发生在“写入数据库之前”但输出到页面时用的模板只做了简单的拼接并没有再次做HTML实体转义。也就是说攻击者只要能在提交阶段让数据“活着”进入数据库后续的渲染环节根本不再设防。这个场景下最有用的姿势就是前面讲过的双重编码把尖括号、引号做HTML实体编码让过滤器盯不到“原生弹药”但只要数据通过实体解码输出到页面浏览器自然会把它们还原成可执行的标签和属性。过滤器在输入端拦的是、、它完全没考虑HTML实体的还原问题。存储型XSS的二次触发还有一个反射型没有的优势payload不一定要在提交的当下看起来完整。很多系统会把用户输入分段存储到不同字段一个字段填“前半个payload”另一个字段填“后半个payload”最终页面渲染时二者恰好拼接成一个完整的执行链。这种“拼接型绕过”极度依赖页面结构但对过滤非常有效因为单段数据可能在语义上完全无害。做这种测试时我养成了一个习惯发现有多个输入字段时不要挨个测而是先花时间把目标页面的输出模板完整逆向出来弄清每个字段分别插到哪个标签的哪个位置。只有模型清楚了才能设计出跨字段拼接的最优形态。8. 常见问题与排查技巧实录为什么我的payload死活不弹到这一节我写几个自己反复踩过、也最常见于初学者的问题直接给排查思路。把这些问题对照着看能让你的测试效率高很多。8.1 我的payload在本地HTML文件里能弹放到目标站点就不行这是最常见的困惑。首先要意识到本地HTML文件的上下文是纯净的没有经过服务端过滤、模板转义、字符集转换、浏览器安全策略干预。放到目标站点不弹不代表你的payload思路是错的而是你的payload没能在完整链路中维持到最后。排查步骤打开浏览器开发者工具查看最终渲染的HTML源码确认你的输入被输出到哪个位置被加了什么前缀和后缀。用“右键查看源码”而不是“检查元素”因为这个页面可能是动态DOM拼接的结果源码才能反映服务端真正的输出。把源码截下来手写一遍“浏览器将会如何解析它”找出你没想到的转义或编码。8.2 过滤了script关键词但我拼不出有效payload优先检查过滤方式。如果只是简单替换成空字符串比如preg_replace(/script/i, , $input)可以用双写绕过scrscriptiptalert(1)/scr/scriptipt替换掉中间的script后剩下的正好是完整的script标签。如果过滤是替换成“安全”的占位符双写就不奏效了需要换用其他标签或属性方向。8.3 单引号双引号都被转义属性闭合打不进去这种时候不要继续硬磕引号转向无引号属性或者实体编码引号。无引号属性形态推荐先试svg/onloadalert(1)如果事件属性名也被过滤就用事件属性字典轮换测试。如果有长度限制试着把payload拆开利用同标签多属性和存储拼接。8.4 事件属性被执行了但alert被拦截拦截函数名是现代过滤器的常见策略。解决办法是换函数prompt、confirm在大多数浏览器同样弹窗或者用表达式拼接svg/onloadtop[alert](1)这种形式在执行之前[alert]还没被拼成字符串字面量过滤器如果只查完整词就匹配不到。8.5 输出在textarea或title里标签注入无效这两个上下文的特殊之处在于浏览器会进入“特殊元素”解析状态不会识别标签内部的事件属性直到遇到闭合标签才会回到普通HTML解析状态。所以正确的操作顺序是先闭合当前标签再输出新的标签和payload/textareasvg onloadalert(1)如果过滤器对/textarea做了处理可以尝试HTML实体编码闭合标签或者利用前面提到的多层解码链。8.6 用CSP挡了内联执行还有什么思路现代站点如果配置了严格的CSPContent-Security-Policy很多内联payload会直接被策略拦下这时传统标签事件属性的招数基本失效。能走的方向包括寻找CSP白名单里允许的外部JS文件域名利用JSONP接口或上传可控JS文件如果CSP里允许unsafe-evaleval相关链路还有机会实在不行就标记为“CSP限制下暂不深入”转去测其他无CSP的页面。做XSS测试时先看响应头的CSP字段比什么都重要。8.7 速查表过滤特征与应对方向过滤特征优先尝试的方向拦截script双写、HTML实体编码、其他标签svg/img/details拦截尖括号无新标签方案属性闭合事件属性JS上下文注入拦截引号无引号属性、反引号、JS字符串拼接拦截事件属性名冷门事件ontoggle/onstart/oncanplay过滤函数名alert换confirm/prompt、表达式拼接、间接调用强制长度限制拆分标签、跨字段拼接、自动闭合短payloadCSP拦截内联执行寻找白名单域、JSONP、跳转可执行JS的页面输入入库时转义输出未编码双重编码存储靠输出端实体解码还原9. 配套的防御收口打完记得帮自己补漏洞绕过了这么多东西反过来看防御其实也能很清楚。每次我做完一轮测试都会顺手帮目标把防御短板列出来这既是对自己工作的负责也是理解绕过的另一种训练方式。第一道防线是输出编码。不要依赖输入过滤来防XSS因为过滤总有盲区而且维护黑名单的成本极高。正确做法是在任何动态内容输出到HTML上下文时根据上下文选择正确的编码方式。HTML实体编码用于HTML文本节点HTML属性编码用于属性值URL编码用于URLJS字符串的JSON编码用于JS上下文。上下文错了编码就白做了。第二道防线是白名单。输入校验尽量采用白名单思维而不是黑名单。例如只允许数字、字母和有限符号比“禁止尖括号”可靠得多。标签白名单配合事件属性白名单做过滤能挡住绝大多数依赖事件属性的注入。第三道防线是CSP。即使你的编码在某处遗漏了CSP也能作为兜底层拦截内联脚本和远程脚本。把CSP的头设置好把unsafe-inline、unsafe-eval都关掉攻击面会立刻减小一大截。一个配置得当的CSP经常比前三层过滤器加在一起都管用。最后是定期用自动化工具加手工测试做安全自查。绕过姿势更新太快靠记忆维护黑名单根本不现实。把目标应用当成自己的实验室测完立刻修修完再测这是唯一能保持安全水位的方式。我个人做了这么久安全测试最大的体会是绕过与被绕过的关系永远在动态演化没有一招鲜的万能payload只有对解析链路、上下文模型和浏览器规范的持续理解。当你能够熟练地判断一段数据会经历哪些解码、落进哪个解析器、最终被浏览器还原成什么你的测试思路就完全不一样了。如果这篇对你有帮助建议不要只收藏了事。找个本地环境把里面的payload按上下文分类一个一个亲手测一遍观察每种过滤下哪些姿势失效、哪些姿势还能用。实践中踩过的坑才是你自己的武器库。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑