☰
DVWA CSRF模块深度解析:从Low到Impossible的攻防实战与防御体系
2026/10/19 2:51:46 网站建设 项目流程

1. 为什么CSRF是Web安全里最容易被低估的漏洞

DVWA里的CSRF模块,很多人第一次做的时候会觉得“就这?”,改个密码而已,看起来平平无奇。但恰恰是这个“平平无奇”的漏洞,在真实业务场景里造成的破坏力远超很多人的想象。我见过不少开发同学,XSS和SQL注入都能说得头头是道,一到CSRF就含糊了,觉得“攻击者又不知道用户的密码,能干什么?”这种认知偏差本身就是最大的安全隐患。

CSRF全称Cross-Site Request Forgery,跨站点请求伪造。它的核心逻辑其实特别简单:攻击者诱导已经登录了目标站点的用户,在不知情的情况下,向目标站点发起一个恶意请求。关键在于,浏览器会自动携带该站点下的Cookie,服务端看到Cookie有效,就认为是用户本人的操作,直接执行。整个过程攻击者不需要拿到用户的密码,也不需要入侵服务器,只需要让用户的浏览器“自愿”地发一个请求就够了。

这个漏洞在DVWA里被设计成Low、Medium、High、Impossible四个难度等级,每个等级对应不同的防御策略。我个人的经验是,DVWA的CSRF模块是理解“防御纵深”这个概念最好的教材之一。因为你会清楚地看到,只加一层防护和加多层防护之间的差距到底有多大,以及为什么有些看起来“应该有用”的手段实际上根本靠不住。

这篇文章我会从DVWA的CSRF模块出发,把四个难度的源码逻辑、攻击手法、防御原理全部拆开讲清楚。不管你是刚接触Web安全的新手,还是想系统梳理CSRF防御体系的开发人员,都能从中拿到可以直接用的东西。我会重点讲清楚每个防御手段背后的“为什么”,因为只有理解了原理,你才能在实际项目中做出正确的安全决策,而不是照搬照抄。

2. DVWA CSRF模块的整体设计与源码逻辑拆解

2.1 DVWA的CSRF模块到底在模拟什么场景

DVWA的CSRF模块模拟的是一个非常典型的业务场景:用户登录后可以修改自己的密码。页面提供两个输入框,分别是新密码和确认新密码,提交后服务端直接修改当前登录用户的密码。这个场景之所以经典,是因为“修改密码”这个操作在真实系统里几乎无处不在,而且一旦被CSRF攻击成功,后果非常严重——攻击者可以把你的密码改成他自己知道的密码,然后直接接管你的账号。

整个模块的页面结构很简单,一个表单,两个密码输入框,一个提交按钮。但就是这么一个简单的表单,在四个难度等级下展现了完全不同的安全水平。我建议在做这个模块的时候,不要只盯着“怎么攻击成功”,而是要把每个等级的源码都读一遍,理解每一行代码存在的意义。

2.2 四个难度的源码差异与设计意图

Low级别的源码是最裸奔的。它接收$_GET['password_new']和$_GET['password_confirmation']两个参数,直接比较是否相等,相等就执行UPDATE语句修改密码。没有Token,没有旧密码验证,没有任何额外的校验。而且注意,它用的是GET请求。这意味着攻击者只需要构造一个URL,里面带上新密码参数,诱导用户点击,密码就被改了。

Medium级别加了一层判断:检查$_SERVER['HTTP_REFERER']是否包含当前主机名。这个思路是“请求必须来自本站页面”,听起来有点道理,但实际上Referer头是客户端可控的,攻击者可以通过多种方式绕过。DVWA在这里的防御思路代表了早期Web开发中一种很常见的“看起来安全”的做法。

High级别引入了CSRF Token。服务端生成一个随机Token存入Session,表单提交时必须带上这个Token,服务端比对Session中的Token和提交的Token是否一致。这是目前业界公认的标准防御手段之一。但DVWA的High级别实现有一个细节值得注意——它没有对Token做时效性管理,也没有绑定用户的其他特征。

Impossible级别是终极防御形态。它同时做了三件事:验证CSRF Token、要求输入旧密码、并且对Token做了更严格的校验。这三层叠加之后,CSRF攻击基本不可能成功,因为攻击者既拿不到Token,也不知道用户的旧密码。

2.3 为什么DVWA要用“修改密码”作为CSRF的演示场景

我后来想明白了,DVWA选“修改密码”这个场景是有深意的。因为修改密码这个操作有一个特殊性:它是账号安全的核心防线。一旦密码被改,用户就失去了对账号的控制权。而且修改密码不像转账那样需要额外的验证步骤,很多系统里就是两个输入框一提交就完事了。

另一个原因是,修改密码这个操作在CSRF攻击中有一个天然优势——攻击者可以完全控制修改后的值。攻击者把密码改成自己知道的字符串,然后就可以直接用新密码登录了。这比那些“发起一个请求但结果不可控”的CSRF场景要直接得多。

从教学角度看,这个场景还有一个好处:它不涉及复杂的业务逻辑,学习者可以把全部注意力放在CSRF的攻防原理上,而不会被业务细节干扰。这也是DVWA作为教学靶场的核心设计理念——每个模块只聚焦一个安全问题,把问题剥离到最纯粹的状态。

3. 从Low到Impossible:四个等级的攻击与防御实操

3.1 Low级别:一个URL就能改掉密码

Low级别的攻击简单到令人发指。你只需要构造一个URL,格式如下:

http://靶场地址/vulnerabilities/csrf/?password_new=123456&password_confirmation=123456&Change=Change

把这个URL发给已经登录DVWA的用户,用户一点击,密码就被改成了123456。整个过程用户可能只看到页面闪了一下,甚至什么都没察觉到。

我实测的时候用的是一个更隐蔽的方式:在DVWA的留言板模块里嵌入一个自动加载的图片标签,src指向上面那个URL。用户浏览留言板的时候,浏览器会自动请求这个URL,密码就被改了。用户完全无感。

注意:Low级别用的是GET请求,这意味着攻击向量非常多——图片标签、链接、iframe、甚至CSS的background-url都可以触发。GET请求做敏感操作本身就是一种设计缺陷。

从防御角度看,Low级别的问题不只是缺少Token,还包括:敏感操作使用GET请求、没有旧密码验证、没有二次确认。这三个问题任何一个单独存在都会增加风险,三个同时存在就是灾难。

3.2 Medium级别:Referer校验为什么靠不住

Medium级别在Low的基础上加了一行代码:

if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) { // 执行修改密码 }

这行代码的意思是:检查请求的Referer头里是否包含当前服务器的名称。如果包含,就认为是合法请求。这个思路的出发点是“只有从本站页面发起的请求才会带正确的Referer”。

但这个防御有两个致命问题。第一,Referer头是浏览器自动带的,但攻击者可以控制自己的请求不发Referer,或者伪造Referer。第二,如果攻击者把恶意页面部署在同一个域名下(比如通过XSS或者文件上传),Referer校验就完全失效了。

绕过方法其实很简单。如果攻击者直接构造一个HTML表单,把referer属性设置成目标站点的地址,或者用<meta name="referrer" content="no-referrer">让浏览器不发Referer,Medium级别的防御就形同虚设。我在实操中用的是第一种方法,构造一个自动提交的表单,把Referer设置成目标站点的URL,一次就过了。

实操心得:Referer校验只能防住“顺手攻击”,防不住“有心攻击”。在真实项目中,Referer可以作为辅助判断条件,但绝对不能作为唯一的防御手段。

3.3 High级别:CSRF Token的正确打开方式

High级别引入了CSRF Token,这是目前最主流的防御方案。源码逻辑是:服务端生成一个随机Token存入Session,表单页面渲染时把Token嵌入隐藏字段,提交时服务端比对Session中的Token和表单提交的Token是否一致。

// 生成Token $token = md5( uniqid() ); $_SESSION[ 'csrf_token' ] = $token; // 验证Token if( $_SESSION[ 'csrf_token' ] == $_POST[ 'csrf_token' ] ) { // 执行修改密码 }

这个方案的核心安全假设是:攻击者无法获取到用户Session中存储的Token值。因为Token是随机生成的,而且只存在于服务端Session和当前页面的表单中,攻击者从外部无法读取。

但High级别的实现有一个细节需要注意:它用的是==比较,而不是===。在PHP中,==是松散比较,存在类型 juggling 的问题。不过在这个场景下,Token是MD5字符串,类型 juggling 的风险不大。真正需要注意的是Token的生成方式——md5(uniqid())的随机性其实不够强,uniqid()是基于时间戳的,理论上可以被预测。更安全的做法是用random_bytes()或openssl_random_pseudo_bytes()生成Token。

我在实操中验证过,High级别确实防住了常规的CSRF攻击。因为攻击者构造的恶意页面无法读取目标站点的Token,提交的请求缺少正确的Token值,服务端直接拒绝。但这里有一个前提:Token必须和用户Session绑定,而且每次请求后应该刷新Token。DVWA的High级别没有做Token刷新,这意味着Token在Session生命周期内是固定的,如果Token通过其他途径泄露(比如XSS),CSRF防御就会失效。

3.4 Impossible级别:三层防御的叠加效应

Impossible级别是DVWA给出的“标准答案”。它同时做了三件事:

第一,验证CSRF Token。和High级别一样,但Token的生成和校验更加严格。

第二,要求输入旧密码。用户必须提供当前密码才能修改新密码。这意味着攻击者即使绕过了Token,也不知道用户的旧密码。

第三,对Token做了更严格的校验逻辑,包括Token的时效性检查。

// 验证旧密码 $pass_curr = $_POST[ 'password_current' ]; // 查询数据库比对旧密码 // 验证CSRF Token checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

这三层防御的叠加效果是:攻击者需要同时满足三个条件才能攻击成功——拿到有效的CSRF Token、知道用户的旧密码、并且请求在Token有效期内。这三个条件在正常的CSRF攻击场景下几乎不可能同时满足。

提示:Impossible级别的设计思路值得每个开发人员学习。它不是简单地“加一个Token就完事”,而是从多个维度构建防御纵深。即使某一层被突破,其他层仍然能提供保护。

4. 实操过程中踩过的坑与常见问题排查

4.1 为什么我的CSRF攻击在Medium级别失败了

这是我在实操中遇到最多的一个问题。很多人在Medium级别构造攻击页面时,发现请求被拒绝了,然后就开始怀疑自己的攻击方式有问题。实际上,问题往往出在Referer头上。

DVWA的Medium级别检查的是HTTP_REFERER中是否包含SERVER_NAME。如果你直接在浏览器地址栏输入攻击URL,浏览器可能不会发送Referer头,或者发送的Referer不包含目标站点的域名。这时候你需要用一个中间页面来发起请求,并且确保这个中间页面的Referer被正确设置。

我的做法是写一个简单的HTML页面,里面放一个自动提交的表单,表单的action指向目标URL。然后在页面的<head>里加上<meta name="referrer" content="unsafe-url">,强制浏览器发送完整的Referer。但这样发送的Referer是攻击者页面的地址,不包含目标站点域名,还是会被拒绝。

正确的绕过方式是:把攻击页面部署在目标站点的同一个域名下。比如利用DVWA的文件上传漏洞上传一个HTML文件,然后访问这个文件来发起CSRF请求。这时候Referer就是目标站点的域名,校验通过。

4.2 Token校验失败的那些原因

在High级别实操时,Token校验失败通常有以下几个原因:

第一个原因是Session没有正确初始化。DVWA的Token是存在Session里的,如果你的请求没有携带正确的Session Cookie,服务端就找不到对应的Token,校验自然失败。

第二个原因是Token没有随表单一起提交。有些人在构造攻击请求时只提交了密码参数,忘了带上user_token参数。DVWA的High级别会检查$_POST['user_token']是否存在,不存在直接拒绝。

第三个原因是Token过期。虽然DVWA的High级别没有严格的Token过期机制,但如果你在页面停留时间过长,Session可能已经失效,Token也就无效了。

排查方法很简单:打开浏览器的开发者工具,看Network面板里表单提交的请求,确认user_token参数是否携带、值是否和页面隐藏字段中的一致。如果一致但还是失败,检查Session Cookie是否正常发送。

4.3 常见问题速查表

问题现象可能原因排查方法解决方案
Low级别攻击无效果URL参数名写错检查password_new和password_confirmation拼写对照源码确认参数名
Medium级别请求被拒Referer头缺失或不匹配用开发者工具查看请求头将攻击页面部署在同域名下
High级别Token校验失败Token未提交或Session失效检查请求中是否包含user_token参数确保Session有效并携带正确Token
Impossible级别无法攻击多层防御叠加确认是否同时满足Token和旧密码条件这是预期行为,说明防御有效
修改密码后无法登录密码被改成了其他值查看攻击请求中的密码参数用攻击时设置的密码登录

4.4 一个容易被忽略的细节:DVWA的Session机制

DVWA的Session管理有一个特点:它的Session Cookie默认没有设置HttpOnly和Secure属性。这意味着如果存在XSS漏洞,攻击者可以通过JavaScript读取Session ID,进而劫持用户会话。在CSRF的语境下,如果攻击者能通过XSS拿到Session,那CSRF Token也就形同虚设了,因为攻击者可以直接读取页面中的Token值。

这个细节提醒我们:CSRF防御不能孤立地看。如果站点存在XSS漏洞,CSRF Token的保护效果会大打折扣。因为XSS可以让攻击者在用户浏览器中执行任意JavaScript,包括读取页面中的Token并构造合法的请求。所以,CSRF防御的前提是站点没有严重的XSS漏洞。

5. 从DVWA延伸到真实项目的CSRF防御体系

5.1 真实项目中CSRF防御的常见误区

很多人学完DVWA的CSRF模块后,觉得“加个Token就完事了”。但在真实项目中,事情远没有这么简单。我见过不少项目虽然加了Token,但仍然存在CSRF漏洞,原因通常有以下几个:

第一个误区是Token没有和用户绑定。有些项目生成Token后存在全局变量或者缓存里,所有用户共用同一个Token。这种情况下,攻击者只需要自己登录一次拿到Token,就可以用来攻击其他用户。

第二个误区是Token通过URL传递。有些项目为了“方便”,把Token放在URL参数里。但URL会被记录在浏览器历史、服务器日志、Referer头中,泄露风险很高。Token应该通过POST请求体或自定义请求头传递。

第三个误区是只对部分接口做了Token校验。有些项目只在登录接口加了Token,但修改密码、转账、下单等敏感操作却没有加。攻击者可以从这些未保护的接口入手。

第四个误区是Token没有时效性。Token一旦生成就永久有效,即使泄露了也不会失效。正确的做法是Token应该设置合理的有效期,并且在敏感操作完成后刷新Token。

5.2 多层防御体系的设计思路

结合DVWA的四个难度等级,我总结了一套真实项目中可落地的CSRF防御体系,按优先级从高到低排列:

第一层,CSRF Token。这是最核心的防御手段。Token必须满足以下条件:随机生成、和用户Session绑定、有时效性、通过安全通道传递、服务端严格校验。

第二层,关键操作二次验证。对于修改密码、转账、删除账号等高风险操作,要求用户输入旧密码或进行二次身份验证。这样即使Token被绕过,攻击者也无法完成操作。

第三层,SameSite Cookie属性。设置Cookie的SameSite属性为Strict或Lax,可以限制跨站请求携带Cookie。Strict模式下,任何跨站请求都不会携带Cookie;Lax模式下,只有顶级导航的GET请求会携带Cookie。这个属性在现代浏览器中已经得到广泛支持,是防御CSRF的一道重要防线。

第四层,验证请求来源。检查Origin头和Referer头,确认请求来自合法的源。虽然这两个头可以被伪造,但作为辅助判断条件仍然有价值。

第五层,自定义请求头。对于AJAX请求,要求携带自定义请求头(如X-Requested-With),因为浏览器不会自动在跨站请求中添加自定义头。这个方案适用于前后端分离的架构。

实操心得:不要指望单一防御手段能解决所有问题。真实项目中应该根据业务风险等级,组合使用多种防御手段。高风险操作至少要有Token加二次验证两层保护。

5.3 Token生成与校验的代码实现参考

下面是一个相对完善的Token生成和校验的PHP实现示例,比DVWA的High级别更加严格:

// 生成Token function generateCSRFToken() { if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); $_SESSION['csrf_token_time'] = time(); } return $_SESSION['csrf_token']; } // 校验Token function validateCSRFToken($token) { if (empty($_SESSION['csrf_token']) || empty($token)) { return false; } // 检查Token是否过期(30分钟有效期) if (time() - $_SESSION['csrf_token_time'] > 1800) { unset($_SESSION['csrf_token']); return false; } // 使用hash_equals防止时序攻击 return hash_equals($_SESSION['csrf_token'], $token); } // 敏感操作完成后刷新Token function refreshCSRFToken() { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); $_SESSION['csrf_token_time'] = time(); }

这段代码相比DVWA的High级别做了几个改进:使用random_bytes()生成密码学安全的随机数、增加了Token过期时间、使用hash_equals()防止时序攻击、敏感操作后刷新Token。这些改进在真实项目中都是必要的。

5.4 前后端分离架构下的CSRF防御

现在越来越多的项目采用前后端分离架构,前端用React或Vue,后端提供RESTful API。这种架构下,CSRF防御有一些特殊考虑。

首先,如果API使用JWT做认证,并且JWT存储在localStorage中而不是Cookie中,那么CSRF攻击天然不成立,因为浏览器不会自动携带localStorage中的内容。但这种方式引入了XSS风险,因为localStorage可以被JavaScript读取。

其次,如果API使用Cookie做认证,那么CSRF防御仍然必要。推荐的做法是使用SameSite=Strict的Cookie,并且在每个请求中携带自定义头(如X-CSRF-Token)。后端校验这个自定义头是否存在且有效。

最后,对于跨域请求,CORS配置也会影响CSRF防御。正确配置CORS的Access-Control-Allow-Origin,不要使用通配符*,可以有效限制跨站请求。

6. 我个人的实操体会与进阶建议

DVWA的CSRF模块我前前后后刷了不下十遍,每次都有新的收获。最开始的时候我只关注“怎么攻击成功”,后来慢慢开始关注“为什么这样防御有效”和“这样防御还有什么不足”。这个转变过程让我对CSRF的理解从表面深入到了本质。

我个人最大的体会是:CSRF防御的核心不是“加什么”,而是“验证什么”。Token的本质是验证请求来自合法页面,旧密码的本质是验证操作者知道敏感信息,SameSite的本质是验证请求的同源性。理解了每个防御手段在验证什么,你就能根据业务场景灵活组合,而不是机械地照搬。

另外一个建议是,不要只刷DVWA。DVWA的CSRF模块虽然经典,但它的场景比较单一。你可以尝试在其他的靶场或者自己搭建的测试环境里,模拟更复杂的CSRF场景,比如JSON格式的请求、文件上传的CSRF、多步骤操作的CSRF等。这些场景在真实项目中更常见,也更有挑战性。

最后分享一个小技巧:在做CSRF实验的时候,养成用Burp Suite抓包的习惯。Burp Suite可以让你清楚地看到每个请求的完整内容,包括请求头、请求体、Cookie、Token等。这对于理解CSRF攻击的细节和排查问题非常有帮助。我刚开始学的时候就是靠Burp Suite一点点分析请求和响应的差异,才真正搞懂了Token校验的流程。

如果你已经刷完了DVWA的CSRF模块,下一步可以试试OWASP Juice Shop或者WebGoat里的CSRF挑战,那些靶场的场景更加贴近真实业务,难度也更高。等你把这些都刷完了,再回头看DVWA,会有一种“原来这么简单”的感觉。但那个“简单”是建立在你已经理解了底层原理的基础上的,而不是一开始就觉得简单。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询