DOM型XSS原理与实战:从Kali靶场搭建到payload构造全解析
2026/9/24 18:26:05 网站建设 项目流程

刚把pikachu靶场里最容易被新手绕晕的DOM型XSS关卡打通,正好趁热把整个思路和踩坑过程整理成一篇完整记录。这篇文章不只是贴一个通关payload,我会把从Kali环境搭建、靶场初始化,到DOM型XSS原理剖析、前端代码调试、最终构造利用链的完整过程都过一遍。不管你是刚接触Web安全、准备应付课程实验,还是想搞明白DOM型XSS和其他XSS到底差在哪,这篇实操笔记应该都能帮上忙。

1. 环境准备:先在Kali上把pikachu靶场跑起来

1.1 为什么选Kali系统来跑pikachu

很多初学者会问,pikachu靶场不是随便装个Windows也能跑吗,为什么非要用Kali?其实严格来说不是必须,但我个人强烈建议在Kali里做,理由有几点。

第一,Kali自带大量渗透测试工具,虽然DOM型XSS这一关主要靠浏览器开发者工具就够了,但当你往后做SQL注入、命令执行、文件上传这些关卡时,Burp Suite、sqlmap、dirb这些工具都是现成的,不用再折腾安装环境。第二,Kali基于Debian,部署LAMP环境非常顺滑,apt一条命令就能解决大部分依赖问题。第三,靶场学习的核心在于训练“攻击思维”,在Kali里操作能让你提前适应真实安全测试的工作环境,后面做CTF、项目实战时不会手生。

我选择的方式是VMware虚拟机里装Kali,而不是物理机直接装。原因很简单,虚拟机快照功能太香了——靶场环境折腾坏了,一条命令就能回滚,完全不影响宿主机。VMware里装Kali的流程网上教程很多,镜像从官网下载就好,安装时默认选择“整个磁盘”加“图形界面”就能一路顺畅完成。

1.2 LAMP环境搭建与pikachu部署全流程

pikachu靶场是一个PHP写的Web应用,所以Kali系统上必须先把Apache、MySQL/MariaDB、PHP这套环境跑起来。打开终端,依次执行:

sudo apt update sudo apt install -y apache2 mariadb-server php php-mysql libapache2-mod-php php-gd php-xml

这里要注意,新版Kali默认的PHP版本可能已经到了8.x,而pikachu是比较早的项目,部分代码在PHP 8下会报弃用警告,但通常不影响核心功能运行。我在测试时没有遇到致命错误,如果真的遇到白屏或报错,在后面“常见问题”部分我会给解决方案。

接下来启动服务:

sudo systemctl start apache2 sudo systemctl start mariadb

然后把pikachu源码部署到Web目录。我是直接从GitHub仓库克隆的:

cd /var/www/html sudo git clone https://github.com/zhuifengshaonianhanlu/pikachu.git

如果网络不方便克隆,也可以在宿主机下载源码压缩包,再通过VMware Tools或拖拽方式传到Kali里解压,效果一样。之后需要给源码目录设置写权限:

sudo chown -R www-data:www-data /var/www/html/pikachu sudo chmod -R 755 /var/www/html/pikachu

接着初始化数据库。pikachu有个自动初始化功能,访问URLhttp://127.0.0.1/pikachu/,页面会提示先配置数据库连接。pikachu的配置文件在inc/config.inc.php,打开后把数据库用户名和密码改成你的MariaDB配置。默认情况Kali里MariaDB的root密码是空的,如果你不想动全局配置,可以在MariaDB里专门建一个账号:

CREATE DATABASE pikachu DEFAULT CHARACTER SET utf8; CREATE USER 'pikachu'@'localhost' IDENTIFIED BY 'pikachu123'; GRANT ALL ON pikachu.* TO 'pikachu'@'localhost'; FLUSH PRIVILEGES;

然后在config.inc.php里对应填上pikachupikachu123。填好后刷新Web页面,点击初始化按钮,系统会自动建表并写入数据。看到“初始化成功”的提示,靶场就算搭建完成了。

1.3 宿主机访问虚拟机靶场:网络与防火墙问题

很多人在这一步卡住——Kali里访问127.0.0.1/pikachu没问题,但切换到Windows宿主机浏览器打开http://[Kali的IP]/pikachu就死活访问不了。这个问题九成出在VMware网络模式和防火墙配置上。

先确认Kali的IP地址:

ip addr show

如果你的VMware网络模式是NAT模式,Kali的IP一般是一个192.168.x.x的地址。宿主机必须能ping通这个IP才能继续谈访问。如果ping不通,检查VMware的“虚拟网络编辑器”里NAT模式的子网配置,确保宿主机网卡和虚拟机在同一网段。

还需要检查一个非常隐蔽的问题:新版Kali默认防火墙可能拦截了宿主机对Apache的访问。虽然Kali默认并没有启用ufw防火墙,但为了保险起见可以执行:

sudo ufw status sudo ufw allow 80/tcp sudo ufw allow 443/tcp

如果ufw是inactive状态,就不用管。但Apache监听地址也需要确认,默认监听所有接口是没问题的。我在实际操作中遇到的是VMware NAT模式下网段冲突问题,宿主机VMnet8网卡自动获取到了另一个网段,手动改成和Kali同一网段后,宿主机的浏览器瞬间就能打开靶场首页了。

2. 认识DOM型XSS:先搞清楚它到底“坏”在哪里

2.1 三种XSS的区分:反射型、存储型和DOM型

很多教程喜欢直接给XSS分类,然后举几个例子,但读完还是分不清。我换个思路,从“恶意脚本藏在哪里、服务端有没有参与”这个角度去看,一下就清晰了。

反射型XSS,也叫非持久型XSS,payload是放在URL参数里的,服务端接收到参数后,直接把参数内容拼进HTML响应返回给浏览器。特点是一次性的——URL里带着payload,服务端“反射”一下,payload就出现在页面上,改一个字符都不会生效。

存储型XSS则是把payload存到服务端数据库里,之后任何用户访问到加载这条数据的页面,都会触发脚本。留言板、评论区、个人信息编辑这种功能最容易出这种问题。

DOM型XSS跟前两者最大的区别在于:服务端完全不参与payload的渲染。服务端返回的HTML本身是干净的,但因为页面里的JavaScript代码操作了DOM,把URL参数或者其他可控数据直接写进了页面,导致脚本有机会执行。也就是说,问题的根源不在服务端,而在浏览器端的JavaScript逻辑。

这个区别非常关键,我用一个表格来对照:

类型服务端是否参与输出payload存储位置判断特征
反射型参与,服务端拼接到响应里只存在于当前URL中查看响应报文能直接看到payload
存储型参与,服务端持久化保存数据库/文件等存储介质刷新后payload依然存在
DOM型不参与URL中和浏览器端JS逻辑里响应报文里看不到payload,但页面DOM被改变

2.2 DOM操作背后的危险入口

要理解DOM型XSS,必须先知道浏览器里有哪些“危险DOM函数”容易被恶意利用。JavaScript代码天生就有读写页面DOM的能力,其中一些API可以直接把字符串当作HTML渲染到页面上。一旦这些API接收了用户可控的数据,且没有做任何过滤转义,漏洞就产生了。

最常见的高危入口包括:

  • document.write()/document.writeln()
  • element.innerHTML
  • element.outerHTML
  • element.insertAdjacentHTML()
  • eval()(虽然这是代码执行,不直接是DOM操作,但常和XSS配合使用)

其中innerHTML是DOM型XSS的“重灾区”,因为它接收包含HTML标签的字符串,会直接把内容解析成页面元素。pikachu靶场的DOM型XSS关卡,核心逻辑就是用innerHTML把输入内容插入到页面中。

另外经常被忽略的还有location对象——location.hreflocation.searchlocation.hash这些都可能成为用户输入的入口。比如DOM型XSS常发生在带#锚点的页面里,服务端不处理锚点,但JavaScript会读取location.hash来做页面逻辑,这就绕过了服务端的所有过滤。

2.3 为什么说DOM型XSS的payload不会出现在响应包里

判断DOM型XSS最直观的方法就是抓包。打开Burp Suite或浏览器F12的Network面板,访问一个携带payload的URL,然后看服务端返回的HTML响应里有没有包含payload字符串。如果是反射型或存储型XSS,响应源码里立刻能看到payload;但如果是DOM型XSS,响应源码干干净净,payload只在浏览器的内存DOM里“奇迹般”地出现了。

理解这个原理对做安全测试很重要。有些新手拿工具扫描发现响应里没有反射特征,就判定“没有XSS”,结果漏掉了DOM型漏洞。我后来在真实项目里挖洞的经验是,凡是看到前端代码里有innerHTMLdocument.write这种调用,就多长个心眼,沿着数据流向去追踪是否用户可控。

3. 靶场通关实操:一次完整的DOM型XSS利用

3.1 定位漏洞入口:找到DOM型XSS关卡

靶场首页进入XSS模块,看到列表里有“反射型XSS(GET)”“反射型XSS(POST)”“存储型XSS”“DOM型XSS”等入口。点击“DOM型XSS”进入关卡页面。

这个页面和前面的XSS练习页面不太一样,它是一个简单的提交表单,页面上有一个输入框,旁边是一段说明文字和一个“插入”类的按钮。输入框上方提示大致意思是:这里有一个功能,用户输入的内容会被插入到页面中,看看你是否能触发XSS。

这时候不要急着输入payload瞎试,先做一次常规的功能测试。我在输入框里输入一个普通的字符串,比如test2024,点击提交后,页面下方出现了一段文本,内容是刚才输入的字符串。这段文本的出现方式很关键——它没有触发页面刷新,也没有服务端跳转,就像是在原地凭空多出来了一段内容。

这一步的观察经验很重要,我截图记下了URL的变化。注意到URL里并没有携带刚才输入的参数,这个细节让我立刻猜测:内容完全是前端JavaScript处理的,很可能使用了DOM操作。

3.2 浏览器F12分析前端代码:找出DOM操作逻辑

接下来是重头戏。按F12打开开发者工具,切换到“Elements”面板,找刚才插入内容的位置。你会看到页面上多了一个元素,比如一个<p>标签,里面写着刚才的输入内容。这说明JavaScript确实是修改了DOM。

但要定位到具体是哪行代码干的活,需要在“Sources”或“Elements”里找到控制这个行为的脚本。我在“Elements”里搜索包含输入框的源码,或者直接在页面里右键点击刚才生成的那段内容,选择“检查”,就能看到它所在的父节点以及相邻元素。

接下来找到页面里的script标签。pikachu这一关的JavaScript逻辑写很简单,核心代码类似:

function domxss() { var str = document.getElementById("text").value; document.getElementById("dom").innerHTML = "<a href='" + str + "'>what do you see?</a>"; }

注意看这两行代码:第一行取出输入框的字符串,第二行把这个字符串直接拼接到一个<a>标签的href属性里,然后通过innerHTML赋值给ID为dom的节点。

到这里漏洞的本质已经清楚了——str完全没有经过过滤和转义,直接进入HTML上下文中。我输入的字符串会被当作HTML解析,而且它拼接的位置是href属性内。这意味着我可以闭合属性,甚至直接插入一个全新的标签。

3.3 构造payload:为什么直接写<script>不行

很多人第一反应是输入<script>alert(1)</script>,但点了提交发现页面毫无反应,明明写入了DOM,但脚本就是没执行。

这个坑我当初也踩过,原因涉及浏览器的一个安全约束:通过innerHTML插入的<script>标签不会自动执行。这是HTML5规范明确规定的,浏览器解析innerHTML时,脚本元素会被插入到DOM树中,但不会执行其中的JavaScript代码。这是浏览器为了防止某些注入攻击所做的设计。

但注意,并不是说innerHTML就绝对安全了,因为还有很多不用<script>标签就能执行JavaScript的方式。最经典的就是事件属性和伪协议。

观察这里拼接的位置是<a href='...'>,整个HTML结构是:

<a href='用户输入'>what do you see?</a>

如果我在输入框里输入#' onclick='alert(1),最终拼接出来是:

<a href='#' onclick='alert(1)'>what do you see?</a>

这个链接被点击时,onclick事件会被触发,弹出提示框。但这需要用户点击链接,交互成本高。更直接的思路是使用javascript:伪协议:

输入:javascript:alert(1)

拼出来的结果是:

<a href='javascript:alert(1)'>what do you see?</a>

点击链接时,浏览器会执行javascript:后面的JavaScript代码,弹窗立即出现。这种方式的优点是只要用户点击链接就会触发,不需要额外的键盘操作,在演示和验证时非常直观。

我在实际测试时,还想验证一下单引号闭合是否可行,于是输入了:

'><img src=x onerror=alert(document.cookie)>

拼接后变成:

<a href=''><img src=x onerror=alert(document.cookie)>'>what do you see?</a>

这里的onerror事件是img标签加载不到图片时触发,src=x故意制造一个加载失败,从而执行alert(document.cookie)。这种方式更为通用,因为即使没有用户点击链接的动作,只要页面渲染到img标签,脚本就会自动执行。

最终验证时我选用的是javascript:alert(document.cookie),输入后点击“插入”按钮,页面出现了一个链接,我点击链接,弹窗成功弹出并显示了当前页面的cookie信息,通关成功。

3.4 利用链复盘:从输入到执行的完整路径

通关之后别急着收工,我习惯做一次完整复盘,理清这条攻击链路。

第一步,用户通过输入框输入payload。第二步,页面里的JavaScript获取输入值,拼接字符串。第三步,字符串被赋值给innerHTML,浏览器把值解析成HTML元素。第四步,元素被插入到页面DOM树中。第五步,用户触发某些动作(点击链接,或图片加载失败触发事件),JavaScript被执行。

从这条链路可以看到,服务端在整个过程中完全“隐身”,payload从未经过服务端,也永远不会出现在服务端日志和响应报文里。这也是为什么这种漏洞很难被传统的WAF检测——WAF检测的是HTTP请求内容,而DOM型XSS的攻击行为完全可以隐藏在浏览器本地JavaScript逻辑中。

4. 常见问题与排查技巧实录

4.1 弹窗死活不出来的4个原因

这个问题绝对排在DOM型XSS实操排行榜的第一位。我总结下来,弹窗不出来往往不是“没有漏洞”,而是payload选错了上下文。

第一个原因是上下文判断错误。payload需要根据拼接位置设计——如果拼接在href属性里,就要考虑伪协议或事件属性;如果拼接在<div>标签内容里,<img onerror>这类标签逃逸更有效;如果拼接在JavaScript变量里,可能要闭合引号再构造表达式。不先看代码就直接乱试payload,成功率很低。

第二个原因是标签被过滤或实体编码。有些页面做了基础的过滤,比如把<>替换成&lt;&gt;,或者限制输入长度。这时候需要观察过滤后的页面输出,寻找可绕过的数据流。pikachu这个关卡没有过滤,但真实靶场或SRC挖掘中过滤是常态。

第三个原因是被innerHTML的脚本限制坑了。前面提到的<script>不执行就是典型例子,很多人栽在这里就轻易宣布“无漏洞”,实际上换一种触发方式就通了。

第四个原因是浏览器编码问题。URL编码、HTML实体编码、JavaScript Unicode转义在多层解析中会互相转换,有时候payload写对了,但因为编码层数不对导致执行不了。我常用的办法是把payload放在浏览器控制台里模拟拼接后的结果,看最终HTML结构是否符合预期。

4.2 用F12“模拟执行”排查DOM操作问题

排查DOM型XSS不执行的问题,一个高效的办法是在控制台手动模拟代码执行。

先把document.getElementById("text").value赋值为你的payload,然后在控制台执行页面源码里的DOM操作语句,看页面会出现什么。如果模拟执行后页面上多出来的元素结构不正确,就说明payload拼接有问题;如果结构正确但脚本不执行,那就要检查是不是事件触发条件没满足。

这个方法也适用真实环境的前端代码审计。遇到复杂的前端代码时,我不会直接盲打payload,而是把关键变量打印出来,把拼接后的字符串在控制台里先拼一次,确认结果再上payload。

4.3 “响应包里看不到payload”的正确理解

我在实战中遇到过团队里有人把DOM型XSS误判为“无漏洞”的情况,问题就出在看包习惯上。

很多安全测试者的习惯是,在URL里输入payload,然后去Burp或浏览器Network里看HTTP响应是否包含payload。对于反射型、存储型这是一套有效流程,但DOM型是这套流程的盲区。因为payload不在服务端响应中,服务器返回的HTML是正常的、干净的,只有在浏览器解析执行完脚本之后,payload才真正出现在内存DOM里,这个过程网络层是看不见的。

所以判断DOM型XSS的正确姿势是:直接观察浏览器页面。输入payload后看页面元素有没有变化、有没有多出可控的标签、有没有执行事件。网络请求只能作为辅助参考。

4.4 DOM型XSS排查速查表

排查项操作手法预期结果
定位DOM操作代码F12 -> Sources,搜索innerHTML/document.write/outerHTML找到关键JS逻辑
确认数据入口查看location.search/location.hash/getElementById等取值方式找到用户可控的输入点
分析拼接上下文在控制台手动拼接字符串,模拟最终HTML确定闭合方式和注入位置
构造payload根据上下文选择伪协议/事件属性/标签逃逸弹窗或触发脚本执行
判断服务端是否参与查看Network响应报文中是否含payload若不含,高度怀疑DOM型

5. 一点实操心得

DOM型XSS在三种XSS里逻辑最绕,但搞懂一次之后,再碰前端代码审计会通透很多。我个人习惯在看完pikachu这一个演练关卡后,主动去总结“数据从哪里来、经过什么处理、最终插入到什么上下文”这条链路,因为几乎所有前端XSS漏洞都逃不开这个框架。pikachu里还有反射型和存储型两个模块,建议都亲手走一遍,横向对比三者在数据流上的差异,理解会更扎实。接下来可以把思路延伸到真实项目的功能点测试上,比如搜索框、收藏夹、用户签名这些场景,用同样的链路分析方法去排查,判断服务和客户端各自过滤了哪些环节。练多了你就发现,XSS其实并不玄乎,无非是数据与代码的边界没守住而已。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询