
1. 项目概述当“过滤”遇上“绕过”在Web安全领域跨站脚本攻击XSS就像是一个经久不衰的“猫鼠游戏”。开发者在输入点竖起一道道正则表达式Regex的过滤高墙而安全研究者或攻击者则绞尽脑汁寻找墙上哪怕最细微的裂缝。这个项目标题“XSS-过滤特殊符号的正则绕过”精准地指向了这个攻防对抗的核心战场。它探讨的不是一个宽泛的XSS概念而是一个非常具体且实战性极强的技术点当防御方试图通过正则表达式过滤掉、、、、等特殊符号来阻断XSS攻击向量时攻击方如何利用正则表达式本身的特性、浏览器的解析差异以及编码技巧实现“曲线救国”最终成功执行恶意脚本。这不仅仅是CTF比赛中的常客更是真实渗透测试和代码审计中必须掌握的技能。很多初级防护措施往往依赖于一个简单的preg_replace或类似的字符串替换函数试图“一劳永逸”地清除危险字符。但正如我们即将看到的这种思路存在诸多盲点。理解这些绕过技巧对于开发者而言意味着能写出更健壮、更深度的防御代码对于安全从业者而言则是评估一个应用XSS防护是否“纸老虎”的关键。接下来我们将深入拆解这个“矛与盾”的游戏从正则过滤的原理出发一步步揭示那些看似严密的防线是如何被巧妙绕过的。2. 正则过滤的常见思路与固有缺陷在深入绕过技巧之前我们必须先理解防御方通常是怎么想的。基于正则表达式的XSS过滤其核心逻辑是“黑名单”或“基于模式的拦截”。开发者会定义一个或多个正则模式匹配他们认为危险的字符或字符串组合然后进行删除、转义或替换。2.1 典型的“过滤特殊符号”正则模式一个非常常见但脆弱的防御代码可能长这样以PHP为例function filter_xss($input) { $pattern /[\\\]/; return preg_replace($pattern, , $input); }这个函数意图很明确删除输入字符串中所有的尖括号,、双引号、单引号和符号。在开发者看来没有了尖括号就无法构造HTML标签没有了引号就无法闭合属性值没有了就无法使用HTML实体。逻辑似乎无懈可击。另一种稍微复杂点的模式可能针对script标签$pattern /script.*?.*?\/script/is;这个模式试图匹配并删除完整的script.../script标签及其内容i标志表示不区分大小写s标志让.也能匹配换行符。2.2 为什么这种过滤天生脆弱这种基于正则的过滤方法从设计之初就埋下了几个致命的缺陷过度依赖单一层级的过滤它通常只在数据进入应用的某个节点如接收$_GET[‘input’]时执行一次过滤。但数据在应用中可能流经多个上下文HTML标签内、属性值、JavaScript字符串、CSS样式、URL每个上下文对特殊字符的解析规则都不同。一次性的、针对HTML的过滤无法应对其他上下文。正则表达式的局限性正则引擎本身是“模式匹配”工具不是“语法解析器”。它无法理解HTML或JavaScript的复杂语法结构。攻击者可以利用正则引擎的匹配逻辑如贪婪/非贪婪、回溯和浏览器的容错性之间的差异来制造绕过。“黑名单”思维定势列出“坏”的字符永远会有遗漏。新的HTML特性、JavaScript API、浏览器解析怪癖层出不穷黑名单难以穷尽。编码与解码的时机错位这是绕过中最常见的突破口。如果过滤发生在URL解码或HTML实体解码之前那么攻击者提交的经过编码的载荷就能轻松绕过过滤最终在浏览器端被解码还原成恶意代码。理解了防御的薄弱环节我们就可以像一位耐心的锁匠开始寻找锁芯里的每一处机关。3. 核心绕过技巧深度解析绕过“过滤特殊符号”的正则本质上是寻找一条从“用户输入”到“浏览器执行”的路径这条路径上至少有一个环节过滤逻辑是缺失、错误或可以被欺骗的。我们将从易到难剖析几种经典的绕过思路。3.1 利用编码与多重编码这是最基础、也最有效的绕过方式。核心原理是过滤函数处理的是“字面量”字符而浏览器解析的是“解码后”的字符。HTML实体编码绕过 假设过滤函数删除了和。我们提交的载荷不是scriptalert(1)/script而是lt;scriptgt;alert(1)lt;/scriptgt;对于那个简单的/[\\\]/正则来说这个字符串里没有字面上的或只有、l、t等字符。如果过滤函数也删除了那这个载荷也会被拦截。但很多初级过滤只删尖括号和引号会放过。当这个字符串被输出到HTML页面时浏览器会将其解码为scriptalert(1)/script从而执行。实操心得测试时不仅要测试lt;和gt;还要测试十进制和十六进制实体编码。有时过滤可能只拦截了命名实体而忽略了数字形式。URL编码绕过 如果输入点最终会被放入一个URL上下文比如a href”INPUT”那么URL编码就可能生效。例如构造javascript:alert(1)被过滤了可以尝试javascript%3aalert%281%29如果服务器端在过滤前没有进行URL解码那么%3a冒号和%28左括号就不会被识别为特殊字符。当这个字符串被设置为href属性并点击时浏览器会对其进行URL解码还原出可执行的javascript:协议。多重编码与混合编码 这是对付“过滤后解码”逻辑的利器。假设防护逻辑是先过滤危险字符然后对剩余内容进行HTML实体编码一种错误但存在的逻辑。攻击者可以提交双重编码的载荷amp;lt;scriptamp;gt;alert(1)amp;lt;/scriptamp;gt;第一层过滤看到的是amp;lt;里面没有放行。然后系统对其进行HTML编码变成amp;amp;lt;被编码成amp;。输出到浏览器后浏览器第一次解码得到lt;第二次解码就得到了绕过成功。注意多重编码的成功与否极度依赖于服务器端处理数据的顺序。在实战中需要通过模糊测试Fuzz来探测数据处理流水线。3.2 利用正则表达式本身的特性正则引擎不是万能的它的匹配方式可以被利用。换行符绕过 回顾那个匹配script标签的正则/script.*?.*?\/script/is。s标志让.可以匹配换行符但如果我们遇到一个没有s标志的正则呢比如/script.*?.*?\/script/i。这时.不能匹配换行符。我们可以构造script alert(1) /script或者更隐蔽地在script和之间插入换行script alert(1)/script这样正则模式script.*?可能因为.无法跨越换行而匹配失败导致整个过滤失效。非贪婪匹配的陷阱 模式.*?是非贪婪匹配它会匹配尽可能少的字符。考虑这样一个场景和载荷过滤正则/script.*?\/script/i攻击载荷scriptalert(1)/scriptscriptinnocent/script非贪婪匹配可能会匹配到第一个/script就结束即匹配到scriptalert(1)/script并将其删除。然而如果正则替换函数使用不当例如preg_replace默认替换所有匹配项它仍然会继续匹配并删除第二个script标签。但如果替换逻辑是“只删除第一次匹配”那么第二个scriptinnocent/script就会被保留而我们的恶意代码alert(1)实际上已经在前一个被删除的标签中执行了不这里有个关键点脚本执行发生在DOM解析时而不是正则过滤时。如果过滤后页面变成了scriptinnocent/script那确实没有执行恶意代码。这个例子更常用于演示“绕过删除特定标签”的逻辑例如过滤script但不过滤img我们可以用img srcx onerroralert(1)。但非贪婪匹配的脆弱性在于它可能被标签嵌套或特殊结构欺骗。回溯限制绕过PCRE 这是一个比较高级的技巧。PHP的PCRE正则引擎有回溯次数限制。可以构造一个超长的、复杂的字符串使得正则引擎在尝试匹配时耗尽回溯次数从而匹配失败让恶意载荷溜过去。例如一个包含大量交替选择(a|a)*的模式在匹配特定字符串时可能产生指数级回溯。但在实际XSS绕过中这种技巧应用场景相对较窄更常见于DoS正则引擎本身。3.3 利用浏览器解析的容错性与怪异模式浏览器HTML解析器的容错能力极强这为绕过提供了肥沃的土壤。省略闭合标签或使用无效标签 某些情况下浏览器并不严格要求标签闭合。例如scriptalert(1)!--或者利用某些标签的自闭合特性但使用错误语法img srcx onerroralert(1) //这里的//充当了JavaScript的单行注释避免了需要闭合引号或尖括号的麻烦。如果过滤函数执着于寻找成对的尖括号或引号这种畸形写法可能被放过。利用属性解析规则 HTML属性解析有多种方式。过滤函数可能只检查了用双引号包裹的属性却忽略了单引号或无引号的属性。双引号img src”x” onerror”alert(1)”单引号img src’x’ onerror’alert(1)’如果过滤了双引号但没过滤单引号无引号img srcx onerroralert(1)如果过滤了所有引号但允许空格和等号 更进一步属性值可以包含换行符和制表符这有时也能干扰简单的正则匹配。JavaScript上下文中的绕过 当输入点位于script标签内部或事件处理器如onerror中时规则完全不同。这里过滤的是JavaScript元字符。闭合字符串与语句假设输出点在scriptvar a ‘INPUT’;/script。过滤了单引号。我们可以输入\’; alert(1);//。最终代码为var a ‘\’; alert(1);//’;。这里的\’转义了第一个单引号使其成为字符串的一部分然后我们用自己的单引号闭合字符串接着执行新语句并用//注释掉后面的内容。模板字符串在现代JS中如果输出点使用反引号可以尝试闭合它。eval或setTimeout动态执行如果字符串最终被传递给eval或setTimeout那么编码、字符串拼接等技巧就有了更大的发挥空间。3.4 利用事件处理器与伪协议当直接插入script标签被严防死守时事件处理器Event Handlers和伪协议Pseudo-protocols是两大备选入口。事件处理器onload,onerror,onmouseover,onfocus,onblur等。这些属性可以挂在很多标签上如img,body,svg,input。例如img srcinvalid onerroralert(1)。即使过滤了script和javascript:这些事件属性也可能被忽略。JavaScript伪协议主要用于a href或iframe src等。如a href”javascript:alert(1)”click/a。绕过时需要对javascript:这个协议声明进行编码或变形例如Java#x53;cript:alert(1)HTML实体编码javascript:%61%6c%65%72%74%28%31%29URL编码利用IE等老旧浏览器的特性如jAvAsCrIpT:大小写混淆或javascript:插入换行符某些浏览器会忽略。4. 实战绕过流程与案例拆解让我们通过一个模拟的实战场景将上述技巧串联起来。假设我们面对一个简单的留言板其后端过滤函数如下function sanitize_input($data) { // 移除所有 字符 $data preg_replace(/[\\\]/, , $data); // 将 script 标签替换为空不区分大小写 $data preg_replace(/script.*?.*?\/script/i, , $data); return htmlspecialchars($data, ENT_QUOTES, ‘UTF-8’); // 注意这里又进行了一次转义 }初看之下这个函数做了三件事1. 删除危险符号2. 删除script标签3. 使用htmlspecialchars进行HTML实体转义。似乎很安全但顺序是关键攻击流程推演信息收集我们提交一个测试载荷‘“观察输出。发现页面上显示为#039;quot;lt;gt;amp;。这说明htmlspecialchars生效了并且是在过滤之后执行的。这意味着我们提交的任何字符只要逃过了第一层的preg_replace就会被第三层转义成实体从而无害化。所以主攻方向是第一层正则删除。分析第一层正则/[\\\]/。它删除字符,,,,。我们的目标是让一个可执行的XSS向量通过这些过滤。尝试编码绕过直接提交lt;img srcx onerroralert(1)gt;。第一层正则看到的是lt;img srcx onerroralert(1)gt;它包含字符所以被删除字符串变成lt;img srcx onerroralert(1)gt;。这显然不行。尝试无符号事件处理器构造img srcx onerroralert(1)。第一层正则会删除,,两边的空格不正则只删除列表里的字符空格和字母不会被删。所以过滤后变成img srcx onerroralert1。onerror变成了onerroralert(1)变成了alert1括号被删了。失败。利用正则删除后的字符串拼接这是一个关键思路。如果我们能让过滤后的字符在浏览器解析时重新组合成危险字符呢考虑HTML实体编码的另一种形式#xHH;十六进制或#DDD;十进制。如果我们提交#x3c;img srcx onerroralert(1)#x3e;第一层正则会删除和;吗正则模式是/[\\\]/它只删除不删除;。所以过滤后变成#x3c;img srcx onerroralert(1)#x3e;。这仍然不是有效的HTML实体。换个思路聚焦属性值假设输出不在标签内容区而是在一个标签的属性值里并且属性值没有用引号括好这是开发者的错误。例如后端代码是input value?php echo sanitize_input($_GET[‘input’]); ?。 我们提交1 onfocusalert(1) autofocus。经过第一层过滤单引号双引号尖括号和都被删但这个字符串里没有这些字符所以原样通过。最终生成的HTML是input value1 onfocusalert(1) autofocus。浏览器解析时会将value的值视为1然后看到onfocus属性并将其作为事件处理器绑定。autofocus属性使输入框自动获得焦点从而触发onfocus事件执行alert(1)。绕过成功这个案例的要点在于过滤逻辑只删除了特定的几个字符但XSS载荷onfocusalert(1) autofocus完全由字母、数字、空格、等号和括号组成不在黑名单内。开发者在输出时没有给属性值加上引号valueINPUT使得我们的输入可以轻易地“逃逸”出属性值的范围引入新的属性。htmlspecialchars在最后一步转义但因为它接收到的输入里已经没有”‘这些字符了第一层删掉了所以它无事可做。如果属性值有引号呢如果代码是input value”?php echo sanitize_input($_GET[‘input’]); ?”。我们提交1” onfocus”alert(1)双引号会被第一层正则删除变成1 onfocusalert(1)然后被放入value”…”中最终结果为input value”1 onfocusalert(1)”。onfocus成了value字符串的一部分不会被执行。这时我们需要闭合引号。但引号被过滤了。我们可以尝试用quot;HTML实体吗不行会被过滤。所以在这个特定过滤函数下如果属性值被正确引号包裹且过滤了引号直接注入事件处理器会非常困难。这时可能需要寻找其他注入点如标签内容区、CSS、URL参数等。案例总结这个实战推演展示了绕过思维的核心——精确理解数据流和上下文。我们不仅需要知道过滤了什么更需要知道过滤后的数据被用在了哪里什么标签、什么属性、是否有引号包裹、是否在JS块中。一次成功的绕过往往是特定过滤漏洞与特定输出上下文漏洞的结合。5. 防御之道从源头杜绝绕过分析了这么多绕过技巧作为开发者应该如何构建真正有效的XSS防护呢答案是放弃单纯依赖正则过滤特殊符号的黑名单思维转向白名单和上下文相关的输出编码。输出编码Output Encoding是王道原则在数据输出到特定上下文时对其进行正确的编码或转义而不是在输入时进行“消毒”。HTML内容上下文使用如htmlspecialchars($data, ENT_QUOTES, ‘UTF-8’)将,,,”,’转义为HTML实体。确保设置正确的字符集如UTF-8并始终使用ENT_QUOTES标志以转义单双引号。HTML属性上下文同上使用htmlspecialchars。务必为所有属性值加上引号单双皆可保持一致这是防止属性逃逸的生命线。JavaScript上下文将数据放入script标签或事件处理器属性值时不能只用HTML编码。应使用专门的JavaScript编码函数如json_encode()用于生成JS字面量或确保数据被放在引号内并进行正确的转义如将\转义为\\’转义为\’换行转义为\n等。URL上下文在将数据拼接到URL如href,src时使用urlencode()或rawurlencode()。CSS上下文极少需要将用户输入放入CSS如果必须需进行严格的CSS编码。使用成熟的防护库不要自己造轮子。使用经过严格安全审计的库如OWASP的Java Encoder Project、PHP的HTML Purifier用于富文本HTML、.NET的AntiXSS Library现并入System.Web.Security.AntiXss等。这些库提供了针对不同上下文的编码方法。内容安全策略CSPCSP是一个终极的深度防御策略。它通过HTTP头告诉浏览器只允许加载和执行来自特定来源的脚本、样式、图片等。即使网站存在XSS漏洞攻击者也无法注入并执行不在白名单内的脚本。例如一个严格的CSP头可以是Content-Security-Policy: default-src ‘self’; script-src ‘self’ https://trusted.cdn.com;。这能极大缓解XSS的影响。输入验证与白名单在接收输入时根据预期的数据类型进行严格的白名单验证。例如如果是电话号码字段只允许数字、加号、括号和短横线如果是姓名字段可以限制字符集和长度。这能在最早阶段阻断畸形数据。避免危险的操作绝对避免将用户输入直接传递给eval(),setTimeout(string),innerHTML,outerHTML,document.write()等可以执行字符串代码的API。如果必须使用innerHTML务必先对不可信数据进行严格的HTML编码或者使用textContent属性。6. 常见问题与排查技巧实录在研究和测试XSS绕过时你一定会遇到各种奇怪的现象。这里记录一些典型的“坑”和排查思路。问题1载荷明明构造对了为什么弹窗不出现检查点1浏览器控制台Console。打开开发者工具查看是否有JavaScript错误。常见错误是语法错误比如因为过滤导致括号缺失、字符串未闭合等。检查点2网络请求Network。查看提交的载荷是否被后端修改了可能有多层过滤、WAFWeb应用防火墙拦截。检查点3源代码Elements。右键查看页面源代码不是检查元素看看你的输入被最终渲染成了什么样子。检查HTML实体是否被正确解码标签是否被浏览器修正。检查点4CSP限制。检查响应头是否有Content-Security-Policy。如果有它可能阻止了内联脚本unsafe-inline的执行。这种情况下基于标签或事件的XSS可能失效需要寻找其他利用方式如CSP绕过或利用允许域上的脚本。问题2如何高效地Fuzz测试过滤逻辑手动测试从简单的‘“开始观察输出。然后系统性地测试各种编码HTML实体命名、十进制、十六进制、URL编码、Unicode编码。大小写变换ScRiPt。插入非常规空白script/xscript%0a。尝试无闭合标签、畸形标签。使用工具Burp Suite的Intruder或Turbo Intruder配合强大的载荷字典如SecLists中的XSS字典。可以自动化地发送大量变种载荷观察响应差异。理解上下文最重要的还是判断输出点在哪里。是HTML标签之间属性值里JavaScript字符串里还是CSS里针对不同上下文使用不同的测试载荷。问题3遇到WAFWeb应用防火墙怎么办识别WAF发送一些恶意载荷如scriptalert(1)/script观察响应状态码如403、是否有特殊的响应头如X-WAF-Info、返回的错误页面是否包含WAF厂商信息如Cloudflare, ModSecurity等。绕过技巧编码混淆WAF通常基于正则匹配多层编码或使用罕见编码如UTF-7可能绕过。语法变形利用HTML/JS的容错性如标签和属性之间加换行、制表符使用JavaScript:伪协议的各种变体。分块传输利用HTTP分块传输编码Chunked Transfer Encoding技术将恶意载荷拆分成多个数据块可能绕过一些基于完整请求体检测的WAF。研究已知绕过关注安全社区对特定WAF如Cloudflare, AWS WAF, ModSecurity核心规则集的绕过研究。问题4在CTF或靶场中如何系统性地解题第一步信息收集。查看页面源码找输出点。尝试输入普通文本看它出现在页面的哪个位置是直接输出还是放在标签属性里。第二步探测过滤。输入一系列测试字符 ‘ “ /等看哪些被过滤、删除、转义或编码。第三步判断上下文。根据输出位置确定是HTML内容、HTML属性、JavaScript还是其他上下文。第四步尝试通用绕过。根据上下文尝试本章提到的各种编码、事件处理器、伪协议、畸形语法等。第五步利用提示。CTF题目往往有提示比如题目名“过滤了特殊符号”那就明确指向了编码或利用未过滤字符构造事件处理器等技巧。第六步利用浏览器特性。某些题目可能依赖特定浏览器如老版本IE的怪异解析模式。一个排查案例在某次测试中输入img srcx onerroralert(1)被转义成了img srcx onerroralert(1)但弹窗依然没出现。查看控制台发现错误“alert is not defined”。原来页面运行在严格模式strict mode下并且alert函数被重命名或未定义。这说明即使脚本执行了环境也可能受限。最终通过window.alert或top.alert来调用成功。这个案例告诉我们成功注入脚本标签或事件处理器只是第一步确保其中的代码能在当前页面上下文中正确执行同样重要。理解XSS过滤与绕过是一场关于细节的战争。它要求你对前端技术栈HTML、JS、浏览器、后端处理逻辑编码、解码、过滤顺序以及正则表达式都有深入的理解。对于开发者这意味着必须采用“安全默认”的原则在任何地方输出不可信数据时都问自己一句“这个上下文我编码对了吗”对于安全人员这意味着需要像浏览器一样思考像解析器一样分析才能发现那些隐藏在看似无害字符背后的致命裂缝。