1. 从“能改密码”到“能改你密码”:CSRF漏洞的核心与DVWA靶场价值
CSRF(跨站请求伪造)漏洞,是Web安全里一个看似简单、实则极易被低估的威胁。它不直接窃取你的密码,而是利用你已经登录的“身份”,在你不知情的情况下,让浏览器代替你向网站发送一个恶意请求。比如,攻击者可以伪造一个“修改密码”或“转账”的请求,只要你登录了目标网站并访问了恶意页面,这个请求就会被自动执行。很多开发者会疑惑:“我的登录有Session,有Cookie,为什么还会中招?” 这正是CSRF的狡猾之处——它利用的是浏览器会自动携带Cookie发起请求的机制,绕过了对用户身份的“意图”验证。
DVWA(Damn Vulnerable Web Application)靶场,是学习和复现这类Web漏洞的绝佳环境。它把漏洞“封装”在一个可控的、安全的本地环境里,让你能亲手从攻击者视角构造攻击,再从开发者视角实施修复,形成完整的攻防认知闭环。对于CSRF,DVWA提供了从低到高不同安全等级的关卡,非常适合用来理解漏洞原理、掌握攻击手法、并实践最有效的防护措施。
这篇文章,我会带你手把手在DVWA里复现CSRF漏洞。我们不只讲“怎么攻击”,更重点拆解“为什么能攻击”以及“如何从根上修复”。我会把整个流程拆解成:环境准备、漏洞原理分析、低中高各级别攻击复现、以及对应的Token验证、Referer检查、SameSite属性等防护方案的代码级实现。即使你之前没搭过靶场,跟着步骤也能跑通。
2. 搭建你的“安全实验室”:DVWA环境准备与配置
在开始“攻防”之前,你得先有一个安全的实验场。DVWA的搭建过程本身,就是一次很好的Web应用部署练习。
2.1 基础环境选择与安装
DVWA是一个PHP/MySQL应用,所以你需要一个支持PHP和MySQL的Web服务器环境。对于新手,最省事的方法是使用集成环境包:
- Windows/Mac用户:推荐使用XAMPP或PHPStudy。它们一键安装Apache、PHP、MySQL,管理界面友好。
- Linux用户:可以通过包管理器(如
aptfor Ubuntu/Debian,yumfor CentOS)分别安装apache2(或nginx)、php、mysql-server和php-mysql扩展。
这里以XAMPP为例,演示通用步骤:
- 下载并安装XAMPP:从官网下载对应你操作系统的版本,按向导安装。建议安装路径不要有中文和空格。
- 启动服务:打开XAMPP控制面板,启动Apache和MySQL服务。看到端口旁亮起绿灯,表示服务已运行。
- 下载DVWA:从DVWA的GitHub仓库(搜索“DVWA GitHub”)下载ZIP包,解压后,将整个
dvwa文件夹复制到XAMPP的htdocs目录下(例如C:\xampp\htdocs\)。 - 访问DVWA:打开浏览器,访问
http://localhost/dvwa/。如果看到DVWA的安装引导页面,说明Web服务已就绪。
2.2 数据库配置与安全等级设置
访问http://localhost/dvwa/setup.php,这是DVWA的配置页面。你需要解决两个常见问题:
- 数据库连接错误:页面可能会提示“数据库连接失败”。这是因为DVWA默认的数据库配置(在
config/config.inc.php里)可能不对。你需要修改这个文件。找到$_DVWA[ 'db_server' ]、$_DVWA[ 'db_user' ]、$_DVWA[ 'db_password' ]等配置项。对于XAMPP,通常用户是root,密码为空('')。修改后保存,刷新setup.php页面。 - 创建数据库:在
setup.php页面底部,点击“Create / Reset Database”按钮。DVWA会自动创建所需的数据库和表。成功后,页面会跳转到登录页(http://localhost/dvwa/login.php)。
默认登录凭证是:用户名admin,密码password。登录后,在左侧菜单找到“DVWA Security”。这里你可以设置漏洞的安全等级:
- Low:毫无防护,用于理解漏洞最原始形态。
- Medium:有基础但可绕过的防护,适合学习绕过技巧。
- High:有较强的防护,通常需要结合其他漏洞或更精巧的利用。
- Impossible:理论上已修复的版本,用于学习最佳实践。
我建议你从 Low 等级开始,一步步提升,对比攻击手法的变化和防护的增强。每次修改安全等级后,部分挑战可能需要重新登录或重置数据库。
3. 解剖CSRF:在DVWA Low等级下发起一次“改密”攻击
现在,我们进入正题。在DVWA左侧菜单选择“CSRF”,并将安全等级设置为Low。你会看到一个简单的修改密码表单,需要输入新密码和确认密码。
3.1 理解攻击原理:请求是如何被伪造的?
我们先正常操作一次。输入新密码(比如newpassword123),点击“Change”。用浏览器的开发者工具(F12)查看Network(网络)标签页,找到这个请求。你会发现这是一个GET请求,URL类似于:http://localhost/dvwa/vulnerabilities/csrf/?password_new=newpassword123&password_conf=newpassword123&Change=Change
关键点来了:
- 请求类型是GET:这意味着所有参数都暴露在URL里。
- 没有身份验证令牌:请求体或URL中没有类似
token=xxxxxx的随机值。 - 依赖Session Cookie:浏览器发起这个请求时,会自动带上你登录DVWA的会话Cookie。
攻击者的思路就是:构造一个包含恶意参数的URL,然后诱骗已登录的用户去访问这个URL。因为浏览器会带上Cookie,服务器看到有效的Cookie就认为这是用户的合法操作,从而执行修改密码。
3.2 手把手构造攻击页面
攻击者不需要黑进服务器,他只需要让你访问一个他控制的网页。这个网页可以隐藏地发起那个修改密码的请求。
创建一个简单的HTML文件,命名为csrf_attack.html,内容如下:
<!DOCTYPE html> <html> <head> <title>看起来人畜无害的页面</title> </head> <body> <h1>快来点击这个有趣的链接!</h1> <!-- 方法1: 图片标签自动加载 --> <img src="http://localhost/dvwa/vulnerabilities/csrf/?password_new=hacked&password_conf=hacked&Change=Change" width="0" height="0" border="0" /> <p>(上面有一个看不见的图片,其实已经发出了改密请求)</p> <!-- 方法2: 诱导点击的链接 --> <a href="http://localhost/dvwa/vulnerabilities/csrf/?password_new=hacked&password_conf=hacked&Change=Change"> 点击抽奖! </a> <!-- 方法3: 页面加载后自动提交表单 (更隐蔽) --> <iframe name="hiddenFrame" style="display:none;"></iframe> <form id="maliciousForm" action="http://localhost/dvwa/vulnerabilities/csrf/" method="GET" target="hiddenFrame"> <input type="hidden" name="password_new" value="hacked"> <input type="hidden" name="password_conf" value="hacked"> <input type="hidden" name="Change" value="Change"> </form> <script> document.getElementById('maliciousForm').submit(); </script> </body> </html>解释一下这三种方式:
- 图片标签:
<img src="恶意URL">,浏览器加载图片时会自动发起GET请求。将宽高设为0,用户无感知。 - 诱导链接:将恶意URL做成一个超链接,诱骗用户点击。
- 隐藏表单自动提交:创建一个不可见的表单(或放在隐藏的iframe里),用JavaScript在页面加载后自动提交。这是最常用、最隐蔽的方式,对POST请求同样有效。
现在进行攻击复现:
- 确保你已登录DVWA(Low安全等级下的CSRF页面)。
- 在浏览器新标签页中打开你刚创建的
csrf_attack.html文件(可以直接拖入浏览器,或用file://路径访问)。 - 页面加载后,迅速回到DVWA的CSRF页面,尝试再次修改密码。你很可能会看到“Password Changed.”的提示,但用的却是攻击者设定的密码
hacked。或者,直接尝试用admin/hacked重新登录DVWA。
成功了!这就是一次最基础的CSRF攻击。它利用了:GET请求的可预测性+浏览器自动携带Cookie+服务端缺乏二次验证。
注意:在真实环境中,攻击者的页面会放在他的服务器上(例如
http://evil.com/attack.html),然后通过钓鱼邮件、论坛发帖等方式传播链接。DVWA在本地,所以我们用本地文件模拟。
4. 中级防护与绕过:当服务端开始检查Referer
在DVWA中将安全等级调到Medium,然后刷新CSRF页面。你会发现功能没变,但后端代码已经增加了防护。查看源码(DVWA通常提供“View Source”按钮),你会发现类似这样的逻辑(以PHP示例):
// 检查 HTTP Referer 头部是否来自本站点 if( stripos( $_SERVER[ 'HTTP_REFERER' ] , $_SERVER[ 'SERVER_NAME' ] ) === false ) { // 请求可能来自外部,拒绝执行 echo “<pre>Referer check failed. Request denied.</pre>”; exit; } // ... 执行修改密码操作 ...4.1 Referer检查的原理与局限
Referer(或正确的拼写Referrer)是HTTP请求头的一个字段,它告诉服务器这个请求是从哪个页面链接过来的。Medium等级的策略是:只处理来自同域名(localhost)下的请求。
这确实能防御直接从evil.com发起的攻击。但是,Referer检查有多个可被绕过的点:
- Referer可能为空或被篡改:一些浏览器插件、安全软件或用户配置会禁止发送Referer。此外,从本地文件(
file://)、HTTPS跳到HTTP等场景,Referer也可能为空或不被发送。如果服务器在Referer为空时默认放行,那就产生了漏洞。 - 利用子域名或近似域名:如果检查不严格(例如只用
stripos查找子字符串),攻击者可以注册一个像localhost.attacker.com或attackerlocalhost.com的域名来绕过。 - 利用服务端漏洞:如果网站本身存在某些开放重定向或XSS漏洞,攻击者可以构造一个“站内跳转”的链接,使Referer看起来是合法的。
4.2 在DVWA Medium等级下的绕过实践
DVWA Medium等级的Referer检查实现得相对宽松。我们可以尝试以下方法绕过:
方法A:利用空Referer(如果允许)有些实现会在Referer不存在时跳过检查。我们可以构造一个从data:URL 或javascript:URL 发起的请求,这些来源通常没有Referer。例如,将攻击页面中的表单提交动作,改为通过一个没有Referer的窗口触发(这需要更复杂的JS构造,在本地文件环境下可能受限)。
方法B:更常见的实践——寻找不检查Referer的“漏洞点”在真实渗透测试中,我们不会死磕一个点。如果修改密码有Referer检查,我们会去测试网站的其他功能点:修改邮箱、发表评论、点赞、转账等。不是所有接口都做了同等强度的防护。很可能“修改密码”做了检查,但“修改个人信息”的接口忘了做。攻击链就转移了。
在DVWA的Medium等级下,为了教学目的,它可能故意留了一个“后门”,比如对某些特定参数或请求方式检查不严。但更重要的是理解这种“防护不一致”的思维。在实际修复时,必须对所有敏感操作(状态变更)进行防护,而不是挑几个做。
我个人的经验是,单纯依赖Referer防护是不够的。它更像一道辅助防线,而不是主防线。因为它的可控性不在服务端手中,而在客户端和网络环境手中。
5. 构建真正有效的防线:Token验证与SameSite Cookie
要真正防御CSRF,必须在服务端引入一个攻击者无法预测、无法伪造的凭证。这就是Anti-CSRF Token(反CSRF令牌)的理念。同时,结合客户端的SameSite Cookie属性,可以构建纵深防御。
5.1 Token验证机制的实现(对应DVWA High/Impossible等级)
在DVWAHigh安全等级下,查看CSRF页面的源码,你会看到类似如下结构:
前端表单生成时(PHP示例):
<?php // 生成一个随机Token,并存入用户Session $token = bin2hex( random_bytes(32) ); $_SESSION['csrf_token'] = $token; ?> <form action="#" method="POST"> <input type="hidden" name="user_token" value="<?php echo $token; ?>"> <!-- 其他表单字段 --> <input type="password" name="password_new"> <input type="password" name="password_conf"> <input type="submit" name="Change" value="Change"> </form>后端处理请求时(PHP示例):
if( $_SERVER[ 'REQUEST_METHOD' ] == 'POST' ) { // 检查Token是否存在且匹配 $user_token = $_POST['user_token'] ?? ''; $session_token = $_SESSION['csrf_token'] ?? ''; if( !hash_equals( $session_token, $user_token ) ) { // Token验证失败,拒绝请求 echo “<pre>CSRF token validation failed. Request denied.</pre>”; exit; } // Token验证通过,执行敏感操作 // ... 修改密码 ... // 操作完成后,使当前Token失效(一次性使用) unset($_SESSION['csrf_token']); }这个机制为什么有效?
- 随机性与不可预测性:Token是服务器为每个会话、甚至每个表单随机生成的长字符串,攻击者无法提前获知。
- 绑定会话:Token存储在服务器的Session中,与当前登录用户绑定。攻击者即使拿到另一个用户的Token,也无法用于他自己的会话。
- 同源策略保护:由于浏览器的同源策略(Same-origin policy),攻击者页面(
evil.com)的JavaScript无法读取目标网站(localhost)页面中的Token值。因此他无法在伪造的请求中携带正确的Token。 - 一次性使用(可选但推荐):重要操作(如支付、改密)的Token应在验证后立即失效,防止重放攻击。
在DVWAImpossible等级,除了使用Token,还会要求用户输入当前密码进行二次验证。这是“已知秘密”验证,进一步提升了安全性,即使Token因某种原因泄露(如XSS漏洞),没有当前密码也无法完成操作。
5.2 SameSite Cookie属性的作用
这是从客户端(浏览器)层面加固的防线。你可以通过设置Cookie的SameSite属性来告诉浏览器:“这个Cookie只能在同站请求中发送”。
SameSite=Strict:最严格。Cookie仅在同站请求(即当前网站顶级域名相同)中发送。这意味着从evil.com发往localhost的请求,浏览器绝不会携带设置为Strict的Cookie。彻底杜绝CSRF,但可能影响用户体验(例如从邮件链接点回网站,登录状态会丢失)。SameSite=Lax(默认值):宽松模式。在跨站请求中,仅对安全(HTTPS)的顶级导航(如点击链接)发送Cookie,而对子资源请求(如图片、脚本、AJAX)不发送。这能阻止大多数CSRF攻击(因为CSRF通常通过表单POST或GET图片发起),同时保持了主要导航场景的用户体验。SameSite=None:必须与Secure属性(仅HTTPS)一同使用。Cookie在所有上下文中都会发送。这是旧版浏览器的行为,也是CSRF漏洞的根源之一。
如何设置?在服务端设置Cookie时(例如PHP的setcookie函数,或Web框架的中间件):
setcookie(‘session_id’, $sessionId, [ ‘expires’ => time() + 3600, ‘path’ => ‘/’, ‘domain’ => ‘localhost’, ‘secure’ => true, // 仅HTTPS ‘httponly’ => true, // 禁止JS访问 ‘samesite’ => ‘Lax’ // 或 ‘Strict’ ]);将SameSite设为Lax或Strict,是防御CSRF非常有效且低成本的一环,可以作为Token验证机制的有力补充。
6. 从攻到防:修复方案总结与实战检查清单
经过前面的攻击复现和原理分析,我们现在从防御者角度,总结一套完整的CSRF防护方案。
6.1 分层防护策略
最安全的做法是实施“纵深防御”,不依赖单一措施:
首选且必需:Anti-CSRF Token
- 为每个用户会话生成唯一、高强度的随机Token。
- 在渲染表单时,将Token放入隐藏域(
<input type=“hidden”>)。 - 在处理请求时,校验提交的Token是否与Session中存储的一致。
- 对重要操作,使用一次性Token,用后即废。
- 确保Token与用户会话绑定,避免全局通用Token。
重要补充:SameSite Cookie属性
- 为会话Cookie设置
SameSite=Lax(或Strict)。这是现代浏览器的推荐做法,能自动拦截大量跨站请求。 - 如果网站需要跨站使用Cookie(例如嵌入第三方),则必须使用
SameSite=None; Secure,并务必配合CSRF Token使用。
- 为会话Cookie设置
辅助验证:检查Referer/Origin头部
- 作为第二道或第三道防线,可以验证请求的
Origin或Referer头部是否来自可信域名。 Origin头部比Referer更可靠(它不会包含完整路径,且在某些场景下如302重定向仍会发送)。- 注意:需要处理这些头部为空的情况(制定安全的默认策略,如“空则拒绝”)。
- 作为第二道或第三道防线,可以验证请求的
业务层面:二次确认
- 对于极其敏感的操作(如转账、修改核心账户信息),要求用户进行二次验证,例如:
- 输入当前密码。
- 输入手机/邮箱验证码。
- 使用生物识别(指纹、面部)。
- 这不仅能防CSRF,也能防账户被盗后的恶意操作。
- 对于极其敏感的操作(如转账、修改核心账户信息),要求用户进行二次验证,例如:
6.2 开发者自查清单
在开发或代码审计时,可以对照以下清单检查CSRF防护是否到位:
| 检查项 | 是/否 | 说明与操作 |
|---|---|---|
| 1. 敏感操作识别 | 是否梳理了所有会导致状态变更的端点?(POST/PUT/DELETE/PATCH,以及部分GET) | |
| 2. Token生成与存储 | 是否使用密码学安全的随机数生成器生成Token?Token是否与用户会话绑定? | |
| 3. Token传递与校验 | 前端表单是否包含Token隐藏域?后端是否在业务逻辑前校验Token?校验失败是否立即终止并日志记录? | |
| 4. Token一次性使用 | 对于支付、改密等核心操作,Token是否在用后立即失效? | |
| 5. Cookie属性设置 | 会话Cookie是否设置了HttpOnly、Secure(HTTPS下) 和SameSite=Lax/Strict? | |
| 6. 验证请求方法 | 是否遵循RESTful规范,对状态变更操作使用POST等非GET方法?(这不能防CSRF,但是好习惯) | |
| 7. 框架内置防护 | 如果使用Spring Security、Django、Laravel等框架,是否启用了其内置的CSRF防护中间件? | |
| 8. 第三方依赖检查 | 引用的第三方库/组件是否可能引入CSRF风险?(如旧版jQuery插件) | |
| 9. 异常处理 | Token校验失败或Referer检查失败时,返回的HTTP状态码是否是403 Forbidden等明确拒绝状态? | |
| 10. 安全测试 | 是否在测试环节包含CSRF漏洞扫描或手工测试? |
6.3 遇到“Token验证失败”的排查思路
在实际开发或维护中,如果配置了Token却出现“Token验证失败”的错误,可以按以下顺序排查:
- 检查Session状态:用户会话是否有效?是否在生成Token和校验Token之间,Session丢失或过期了?(例如服务器重启、Session存储配置问题)。
- 检查Token传递:
- 前端表单的隐藏域名称(如
name=“user_token”)和后端获取参数的名称(如$_POST[‘user_token’])是否一致? - 如果是AJAX请求,Token是否被正确添加到请求头(如
X-CSRF-TOKEN)或请求体中?
- 前端表单的隐藏域名称(如
- 检查页面缓存:如果表单页面被浏览器或CDN缓存,可能导致多个用户收到同一个Token。确保生成Token的动态页面不被缓存。
- 检查多标签/多窗口操作:用户在A标签页打开表单,在B标签页登录/注销,再回到A标签页提交,可能导致Session或Token不一致。可以考虑为每个表单生成唯一Token,并管理一个Token池。
- 检查框架配置:如果使用框架,检查CSRF中间件是否已正确启用,排除路径(如API接口)设置是否正确。
CSRF的防护,核心思想是“验证请求的意图是否真正来自用户”。Token是服务器给用户的一个“一次性口令”,而SameSite Cookie是浏览器帮用户把守的“关卡”。两者结合,再辅以良好的开发习惯和安全意识,就能有效筑起防线。
通过DVWA从Low到Impossible等级的实战,你应该能清晰地感受到:安全是一个持续的过程,从毫无防护,到简单防护被绕过,再到构建起稳固的防御体系。理解攻击,是为了更好地防御。