DVWA存储型XSS攻防实战:从原理到绕过与防御
1. 项目概述:从靶场实战到存储型XSS的深度剖析
如果你正在学习网络安全,尤其是Web安全,那么DVWA(Damn Vulnerable Web Application)这个靶场你一定不陌生。它就像是一个专门为安全爱好者搭建的“练功房”,里面包含了SQL注入、文件上传、XSS等十几种常见的Web漏洞。今天,我们不谈别的,就聚焦在其中一个既经典又极具危害性的漏洞上:存储型XSS(Stored Cross-Site Scripting)。
为什么单独拎出来讲?因为相比反射型XSS,存储型XSS的危害等级要高得多。反射型XSS就像一把需要你亲手递给攻击者的刀,攻击链依赖用户点击一个精心构造的恶意链接。而存储型XSS,则像是攻击者提前在网站的水源里下了毒,所有后续来喝水的用户都会中招,攻击是持久且广泛的。DVWA的XSS(Stored)模块,就完美模拟了这种场景:一个简单的留言板或评论系统,用户输入的内容被永久存储在服务器数据库里,并在其他用户访问页面时被加载执行。
网上有很多“DVWA通关教程”,告诉你每一步点哪里、输入什么Payload。但这远远不够。知其然,更要知其所以然。这篇文章的目的,就是带你超越简单的“通关”,深入理解每一次攻击背后的代码逻辑、防御机制的演变,以及我们作为攻击者(或防御者)的思考过程。我们会从Low级别毫无防护的“裸奔”状态开始,一步步闯过Medium、High级别的防线,最终分析Impossible级别是如何从根本上杜绝漏洞的。在这个过程中,你会掌握XSS Payload的构造技巧、前端的绕过思路,以及最重要的——建立一种面对黑盒目标时,如何系统性地测试和挖掘XSS漏洞的思维模型。
2. 靶场环境与攻击核心原理拆解
2.1 DVWA环境搭建与核心模块定位
在开始“攻击”之前,我们必须先有一个稳定、可控的“战场”。DVWA的搭建并不复杂,通常需要一个集成了Apache、MySQL、PHP的环境,比如XAMPP、PHPStudy或直接使用Kali Linux自带的LAMP堆栈。这里不赘述详细的安装步骤,但强调几个关键点,这些点往往是被新手忽略的“坑”:
- PHP版本兼容性:DVWA对PHP版本有一定要求,建议使用PHP 5.4 - 7.x的版本。PHP 8.x可能会导致部分功能异常。在
config/config.inc.php文件中,你需要正确配置数据库连接信息。一个常见的错误是MySQL密码为空或使用了错误的数据库名(默认是dvwa)。 - 安全等级设置:登录DVWA后(默认账号
admin/password),务必在左侧“DVWA Security”选项卡中将安全级别设置为Low、Medium、High或Impossible。这个设置会全局影响所有漏洞模块的服务器端代码,是我们进行分级挑战的基础。练习时,请从Low开始,逐步提升。 - XSS(Stored)模块入口:在左侧导航栏找到“XSS(Stored)”。这个模块模拟了一个简单的留言板。页面通常包含两个部分:一个表单(让你输入“Name”和“Message”)和一个下方显示所有历史留言的区域。你的攻击目标,就是通过这个表单,注入恶意脚本,并让它持久化存储,在他人查看留言板时自动执行。
注意:强烈建议在虚拟机或隔离的网络环境中搭建和练习DVWA。虽然它是故意设计为有漏洞的,但错误的配置或随意的网络暴露,仍可能带来不必要的风险。
2.2 存储型XSS攻击原理深度解析
要发动有效的攻击,必须彻底理解攻击的原理。存储型XSS的攻击链可以清晰地分为四个阶段:
- 输入注入:攻击者在一个允许用户提交内容并存储的功能点(如留言、评论、个人简介、商品评价)中,提交一段包含恶意JavaScript代码的输入。这段输入不仅仅是简单的文本,而是被精心构造的Payload。
- 持久化存储:服务器端程序在没有进行充分过滤和验证的情况下,直接将用户输入(包含Payload)存入数据库。这是漏洞形成的根本原因。
- 恶意代码加载:当其他正常用户(受害者)访问包含这些存储数据的页面时(例如查看留言板),Web应用程序会从数据库中读取数据,并将其作为页面内容的一部分(通常是HTML的一部分)发送到受害者的浏览器。
- 客户端执行:受害者的浏览器接收到服务器响应,将其渲染为HTML。由于Payload被当作合法的页面内容,浏览器会忠实地执行其中的JavaScript代码。至此,攻击完成。
关键在于第3和第4步:代码的执行环境是受害者的浏览器,且其权限与目标网站当前会话的权限相同。这意味着,恶意脚本可以:
- 盗取Cookie:通过
document.cookie获取用户的会话标识,从而劫持用户账户。 - 模拟用户操作:发起网络请求(如转账、改密、发帖),即CSRF攻击。
- 钓鱼:在页面中伪造登录框,诱骗用户输入敏感信息。
- 破坏页面:篡改页面内容,进行挂马或跳转到恶意网站。
理解这个原理后,我们就能明白防御的核心思路:在数据存储前(输入验证/过滤)或数据输出前(输出编码)进行严格处理,确保用户输入的数据始终被当作“文本”来处理,而不是可以被浏览器解析执行的“代码”。
3. 分级攻击实战:从Low到High的攻防博弈
接下来,我们进入实战环节。我们将安全级别从Low逐步调到High,观察防御是如何层层加码的,而我们作为攻击者,又该如何见招拆招。
3.1 Low级别:毫无防护的“裸奔”状态
将DVWA安全级别设置为Low,进入XSS(Stored)模块。
攻击过程与Payload分析:在Name或Message输入框中,直接输入经典的XSS测试Payload:<script>alert('XSS')</script>。提交后,你会发现页面弹出了警告框,并且这条“留言”被成功保存。刷新页面或新开一个浏览器访问该页面,警告框依然会弹出。这说明我们的脚本被原封不动地存储并输出了。
背后的代码逻辑(源码分析):查看Low级别的服务端源码(通常位于vulnerabilities/xss_stored/source/low.php),关键部分如下:
<?php if( isset( $_POST[ 'btnSign' ] ) ) { // 获取输入 $message = trim( $_POST[ 'mtxMessage' ] ); $name = trim( $_POST[ 'txtName' ] ); // 直接存入数据库,没有任何过滤! $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>' ); } ?>以及输出部分的代码:
<?php // 从数据库查询留言 $query = "SELECT * FROM guestbook;"; $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>' ); while($row = mysqli_fetch_assoc($result)) { // 直接输出,没有任何编码! echo "<div class=\"message\">"; echo "<p><strong>{$row['name']}</strong>: {$row['comment']}</p>"; echo "</div>"; } ?>漏洞根因:代码对用户输入$message和$name没有进行任何过滤或编码,直接拼接SQL语句存入数据库,又从数据库直接取出拼接进HTML页面。我们的<script>标签被浏览器当成了合法的HTML标签解析并执行。
实操心得: 在Low级别,你可以尝试各种基础的XSS Payload,理解不同上下文:
- Name输入框:通常输出在
<strong>标签内,属于HTML标签内部(HTML上下文)。 - Message输入框:输出在
<p>标签内,也是HTML上下文。 你可以测试:<img src=x onerror=alert(1)>、<svg onload=alert(1)>等,它们都能成功。这里的关键是建立“输入点-输出点”的对应关系,并确认输出上下文。
3.2 Medium级别:初级的过滤与绕过
将安全级别调整为Medium,再次尝试之前的Payload:<script>alert('XSS')</script>。你会发现,提交后页面没有弹窗,Payload似乎被“吃掉”了。
防御机制分析:查看Medium级别的源码(medium.php),关键过滤代码如下:
<?php if( isset( $_POST[ 'btnSign' ] ) ) { $message = trim( $_POST[ 'mtxMessage' ] ); $name = trim( $_POST[ 'txtName' ] ); // 对$message和$name进行了简单的字符串替换过滤 $message = strip_tags( addslashes( $message ) ); $name = str_replace( '<script>', '', $name ); $name = str_replace( '</script>', '', $name ); // 存入数据库 $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>' ); } ?>代码解读:
strip_tags():这是一个PHP函数,用于剥去字符串中的HTML和PHP标签。它被用在了$message上。这意味着,如果你在Message框里输入<script>alert(1)</script>,strip_tags()会直接移除<script>和</script>标签,只留下alert(1)文本,从而失效。str_replace(‘<script>’, ‘’, $name):这是一个非常初级、存在明显缺陷的黑名单过滤。它仅将字符串“
