刚把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 pikachulocalhost IDENTIFIED BY pikachu123; GRANT ALL ON pikachu.* TO pikachulocalhost; FLUSH PRIVILEGES;然后在config.inc.php里对应填上pikachu和pikachu123。填好后刷新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也叫非持久型XSSpayload是放在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.innerHTMLelement.outerHTMLelement.insertAdjacentHTML()eval()虽然这是代码执行不直接是DOM操作但常和XSS配合使用其中innerHTML是DOM型XSS的“重灾区”因为它接收包含HTML标签的字符串会直接把内容解析成页面元素。pikachu靶场的DOM型XSS关卡核心逻辑就是用innerHTML把输入内容插入到页面中。另外经常被忽略的还有location对象——location.href、location.search、location.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型漏洞。我后来在真实项目里挖洞的经验是凡是看到前端代码里有innerHTML、document.write这种调用就多长个心眼沿着数据流向去追踪是否用户可控。3. 靶场通关实操一次完整的DOM型XSS利用3.1 定位漏洞入口找到DOM型XSS关卡靶场首页进入XSS模块看到列表里有“反射型XSSGET”“反射型XSSPOST”“存储型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不行很多人第一反应是输入scriptalert(1)/script但点了提交发现页面毫无反应明明写入了DOM但脚本就是没执行。这个坑我当初也踩过原因涉及浏览器的一个安全约束通过innerHTML插入的script标签不会自动执行。这是HTML5规范明确规定的浏览器解析innerHTML时脚本元素会被插入到DOM树中但不会执行其中的JavaScript代码。这是浏览器为了防止某些注入攻击所做的设计。但注意并不是说innerHTML就绝对安全了因为还有很多不用script标签就能执行JavaScript的方式。最经典的就是事件属性和伪协议。观察这里拼接的位置是a href...整个HTML结构是a href用户输入what do you see?/a如果我在输入框里输入# onclickalert(1)最终拼接出来是a href# onclickalert(1)what do you see?/a这个链接被点击时onclick事件会被触发弹出提示框。但这需要用户点击链接交互成本高。更直接的思路是使用javascript:伪协议输入javascript:alert(1)拼出来的结果是a hrefjavascript:alert(1)what do you see?/a点击链接时浏览器会执行javascript:后面的JavaScript代码弹窗立即出现。这种方式的优点是只要用户点击链接就会触发不需要额外的键盘操作在演示和验证时非常直观。我在实际测试时还想验证一下单引号闭合是否可行于是输入了img srcx onerroralert(document.cookie)拼接后变成a hrefimg srcx onerroralert(document.cookie)what do you see?/a这里的onerror事件是img标签加载不到图片时触发srcx故意制造一个加载失败从而执行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其实并不玄乎无非是数据与代码的边界没守住而已。
