从DVWA四级别看CSRF攻击原理与Token防御落地
在做Web安全测试的这几年里CSRF一直是我觉得最“借刀杀人”的漏洞类型。它不像SQL注入那样直接拖库也不像XSS那样在浏览器里跑一段脚本而是利用浏览器自动携带Cookie的机制在受害者毫不知情的情况下用受害者的身份去完成攻击者预设的操作。DVWADamn Vulnerable Web Application作为一套开源的漏洞演示靶场把CSRF漏洞从低到高划分了四个安全级别非常适合用来理解这类漏洞的底层机制和利用思路。这篇文章我会以DVWA的CSRF模块为主线逐步拆解攻击原理、四种安全级别下的源码逻辑、利用方式以及对应的工程化防御方案。适合刚接触Web安全、正准备做漏洞复现的同学也适合负责业务系统安全设计、想搞清楚“CSRF Token到底怎么落地”的开发人员。你在看完之后不仅能在这个靶场里完整复现一遍攻击链路还会明白为什么现在的Web框架普遍默认加上Token校验。1. 从一次“无感改密”说起CSRF的核心原理1.1 浏览器替你干了“脏活”CSRF的全称是Cross-Site Request Forgery中文叫跨站请求伪造。它的根子在于Web应用天然无法区分一个请求到底是不是用户本人的“真实意愿”。平时我们登录一个网站服务器会在浏览器里种下Session Cookie。只要浏览器还存在这个Cookie后续发出的Http请求就会自动把它带上。服务器看到有效Cookie就认为请求来自已登录用户本人。问题在于这个“自动带上”的动作是浏览器层面完成的。也就是说无论这个请求是用户自己点击按钮发出的还是加载了一个来自第三方站点的图片触发的Cookie都一样会被带上。我举个生活化的例子你进了一家超市刷脸办了会员卡相当于登录。收银台认卡不认人只要看到你的会员卡号就自动给你记账。这时候有个不怀好意的人拿着你的照片做了一张一模一样的卡趁你不注意塞给收银员说你刚才买了两箱红酒。收银员一看卡号对直接记账到你名下。CSRF就是这个拿着假卡冒充你的人而浏览器就是那个不看“人”只看“卡”的收银员。1.2 一次成功的CSRF攻击需要哪些前提CSRF攻击要生效不是简单地让受害者打开一个恶意页面就行需要同时满足几个条件受害者已经登录目标站点并且Session还处于有效状态。目标站点的关键操作比如修改密码、转账使用的是可预测的GET或POST请求参数完全公开可构造。请求发出后服务器只校验Cookie或Session没有校验Token、Referer等来源信息。浏览器允许第三方请求携带目标站点的Cookie这里就牵扯到Cookie的SameSite属性后面会细说。DVWA里Low级别的改密接口就完全符合以上条件一个简单的GET请求参数不加密、不加随机值、不校验来源。攻击者只需要构造一个页面让受害者访问一下密码就被悄悄改掉了。1.3 CSRF和XSS、SSRF到底有什么区别很多人会把CSRF和XSS、SSRF混在一起。我在带团队做安全评审的时候经常需要先帮开发同学理清这三者的边界。XSS跨站脚本是攻击者往正常页面里注入了一段恶意的JavaScript本质是“篡改页面内容”CSRF是攻击者不碰页面内容只是借受害者的身份向服务器发请求本质是“伪造用户操作”SSRF服务端请求伪造是利用服务器作为跳板去请求内网资源本质是“让服务器替攻击者发起请求”。一句话总结XSS攻击的是“浏览器-页面”这条链路CSRF攻击的是“浏览器-服务器”的身份信任链路SSRF攻击的是“服务器-内网”的访问控制链路。三者的防御思路也完全不同——XSS靠输出编码和过滤CSRF靠Token和SameSiteSSRF靠限制目标地址和协议。理解了这个底层区别再去DVWA里看CSRF模块你会发现很多代码逻辑一下子就有了画面感。2. 先把靶场搭起来DVWA环境准备与安全级别说明2.1 环境选型用现成集成包还是手动部署DVWA本身是PHP项目依赖MySQL/MariaDB。我最早是手动在本地搭LNMP后来换了方式直接用某开源集成环境五分钟就完事。两者各有取舍手动部署的优势是更贴近真实生产环境适合想顺带练一遍环境搭建的人劣势是PHP版本、MySQL驱动配置可能踩坑。集成包的优势是省心PHP版本、扩展、Web服务全部配好劣势是目录结构跟自己搭的不一样代码审计时找路径要适应一下。我个人建议初学者先用集成包跑通后续有精力再手动搭一遍。需要注意的是DVWA目前对PHP 8.x兼容性不算好很多老版本在PHP 8环境下会直接报错。建议使用PHP 7.4或8.0配合MySQL 5.7这个组合我实测下来最稳。2.2 DVWA安装与初始化集成包部署完之后把DVWA源码放到Web根目录然后进入config/目录把config.inc.php.dist复制一份改成config.inc.php。打开配置文件确认数据库的连接信息$_DVWA[ db_database ] dvwa; $_DVWA[ db_user ] root; $_DVWA[ db_password ] 你的数据库密码; $_DVWA[ db_port ] 3306;保存之后浏览器访问http://127.0.0.1/dvwa/首页有一个Create/Reset Database按钮点一下就会自动建库、建表、写入初始数据。初始化完成后再登录默认账号是admin密码是password。登录成功后进入DVWA Security页面把安全级别设置为Low点击提交。DVWA还支持通过URL直接指定级别http://127.0.0.1/dvwa/security.php?securitylow这个机制在切换测试场景时好用。2.3 快速摸清CSRF模块的业务场景DVWA的CSRF模块模拟的是一个典型的“修改密码”业务。页面会展示当前登录用户的密码哈希并要求输入新密码、确认密码再点提交。正常情况下用户需要登录才能修改。但漏洞点在于“修改”这个行为本身没有做足够的来源校验——在Low级别下任何知道接口格式的人都可以直接构造URL让受害者在不知情时完成密码修改。这个场景选得非常典型。因为修改密码的危害是灾难性的攻击者一旦把受害者密码改成自己知道的就等于完全接管了账号。拿到账号后可以查看隐私、执行更多操作甚至横向提权。这也是为什么现在各大平台修改密码都需要“先验证旧密码”或“短信验证码”——本质上就是在CSRF之上又加了一道业务侧的防线。3. 四个安全级别的源码分析与利用实录3.1 Low级别没有一丝防范的GET型CSRF先看Low级别的服务端核心源码?php if( isset( $_GET[ Change ] ) ) { $pass_new $_GET[ password_new ]; $pass_conf $_GET[ password_conf ]; if( $pass_new $pass_conf ) { $pass_new mysqli_real_escape_string( $conn, $pass_new ); $pass_new md5( $pass_new ); $sql UPDATE users SET password $pass_new WHERE user . dvwaCurrentUser() . ;; // 执行SQL } } ?注意几个信息点。第一整个接口是GET请求参数直接暴露在URL里没有任何随机Token第二服务端只校验了两个密码字段是否一致至于“是谁发的请求”完全不加甄别第三密码只做了MD5哈希没有加盐——这在真实业务中也是禁忌但作为靶场教学可以理解。基于这个接口攻击者可以构造出一个恶意页面。最简单的POC如下html body img srchttp://127.0.0.1/dvwa/vulnerabilities/csrf/?password_newadmin123password_confadmin123ChangeChange /body /html把这个页面放到攻击者自己的站点上诱导已登录DVWA的受害者访问。浏览器加载页面时会默默向127.0.0.1请求这张“图片”Cookie自动携带。服务端收到请求后发现Session有效参数齐全直接执行UPDATE语句。受害者再次登录时会发现密码已经变成admin123。这里有一个容易被忽略的细节img src触发的请求是一次子资源请求。如果目标站点的Cookie设置了SameSiteLax现代浏览器在跨站子资源请求场景下不会携带CookiePOC就会失效。但DVWA默认环境中Cookie是传统会话Cookie没有设置SameSite属性所以老版本浏览器和部分配置下都能复现成功。这也是我在近两年的测试中调整POC时最常碰到的变量。3.2 Medium级别Referer校验与域名欺骗绕过Medium级别的源码加了一个Referer校验?php if( stripos( $_SERVER[ HTTP_REFERER ] , $_SERVER[ SERVER_NAME ] ) ! false ) { // 执行业务逻辑修改密码 } ?这段代码的逻辑是检查HTTP请求头里的Referer字段是否包含服务器名比如127.0.0.1。如果包含认为是同源请求允许执行否则拒绝。从设计思路上讲这是在尝试做“来源校验”但实现方式存在明显漏洞。仔细看stripos函数它做的是“子串包含”判断而不是“域名精确相等”判断。这意味着只要Referer字符串里包含127.0.0.1这一段就能通过校验并不要求Referer真的是目标站点的页面。攻击者的绕过思路是找一个域名让它的前缀里带上目标服务器的名字。比如攻击者控制了evil.com这个域名并且通过通配DNS解析让127.0.0.1.evil.com也指向自己的服务器。那么攻击者把恶意页面放在http://127.0.0.1.evil.com/poc.html受害者在浏览这个页面时浏览器发出的自动请求头里Referer的值就是http://127.0.0.1.evil.com/poc.html这段字符串中包含了127.0.0.1于是stripos返回非false校验通过。代码被绕过了。另外一个实操中常见的变通方案是临时把POC页面丢到目标主机上。假如攻击者通过其他途径比如文件上传漏洞在某主机上放置了一个HTML文件访问路径是http://127.0.0.1/upload/poc.html那么从该页面发出的请求Referer天然就是http://127.0.0.1/upload/poc.html包含服务器名自然通过校验。这一类“Referer包含”的判断思路在真实系统中很常见也是我评审代码时看到就想提醒对方换掉的重点模式。3.3 High级别Token校验打破“无脑请求”High级别的源码引入了Anti-CSRF Token机制。页面在渲染表单时后端会生成一个随机的Token存储到Session里同时作为隐藏字段输出到HTML?php $token $_SESSION[ token ]; // 表单里输出 input typehidden nametoken value$token // 提交时校验 if( $_POST[ token ] ! $_SESSION[ token ] ) { // 校验失败拒绝执行 } ?攻击者的POC面临一个核心难题请求里必须携带一个有效的、和Session中一致的Token。而由于浏览器的同源策略Same-Origin Policy攻击者读取不到目标站点页面HTML里的Token值天然无法在跨域场景下预知Token。如果不能提前获取Token就算把密码改写的请求发送过去服务端也会因为Token不匹配而拒绝。所以High级别下单纯的CSRF攻击基本失效。想要绕过攻击者必须找到一种方式先拿到Token常见场景包括目标页面存在XSS漏洞可以借助XSS在目标站点的同源环境下读取Token再构造带Token的请求。目标接口存在CORS配置错误允许指定来源跨域读取响应。Token强度不足比如使用固定值、可预测的时间戳序列、或者复用同一Token导致攻击者可以猜测。这里也引出一个很关键的观点CSRF Token并不是绝对安全它的强度取决于随机数质量、是否与Session唯一绑定、是否一次性消费。真实的Web框架比如某PHP框架的CSRF组件默认会在每次请求后轮换Token同时结合Cookie指纹安全性会强很多。3.4 Impossible级别把业务校验变成“多重保险”最后看DVWA里定义的Impossible级别?php // 1. 先校验旧密码 if( $pass_old ! $current_password ) { // 错误旧密码不正确 } // 2. 再校验Token if( $_POST[ token ] ! $_SESSION[ token ] ) { // 错误Token不匹配 } // 3. 校验通过后更新密码并重新生成Token ?这个级别的设计思路值得每一位开发者学习它不依赖单一防线而是把“安全”拆成多层校验。第一层要求用户输入当前密码——这一信息只有用户自己知道攻击者无法在受害者的浏览器环境中替他盲猜第二层校验CSRF Token——阻止跨站伪造请求第三层更新密码成功后立即轮换会话Token缩小Token泄漏的窗口期。从这四档安全级别里可以提炼出一条清晰的安全设计演进路径从“完全不设防”到“只认来源不认真伪”再到“用随机数标识用户意愿”最后到“多个独立校验因子叠加”。这也是我在给团队做安全培训时会专门拿出来的案例——安全不是某一个“银弹”特性而是层层防御。4. 从靶场走到生产CSRF防御的工程化方案4.1 从“加一个Token”到“正确使用Token”很多团队也知道要加CSRF Token但在落地时容易犯两个错误。第一个错误Token被全局复用。如果同一个Token在整段会话内保持不变攻击者只要通过某一种方式拿到一次Token比如抓包、日志泄露就能在有效期内反复构造攻击请求。正确的做法是让CSRF Token绑定当前Session ID并在每次敏感操作成功后强制轮换。第二个错误把Token放在URL里。部分早期框架会把Token作为?csrf_tokenxxx追加在链接上这会导致Token出现在浏览器历史、服务器访问日志、代理日志中形成二次泄露风险。正确做法是放在请求头比如自定义头或POST表单体里避免在URL中传递。一个工程化的Token校验流程通常长这样用户登录成功后服务端生成随机Token存入Session。渲染表单时把Token同时写进隐藏字段和Response Header。提交请求时框架拦截器取出Session里的Token与请求携带的Token做hash_equals恒定时间比较。校验成功后立即刷新Session里的Token旧的立即失效。4.2 SameSite Cookie一道被低估的前置防线前文提到Cookie的SameSite属性直接影响CSRF攻击能否生效。这个属性在Chrome 80之后默认值变成了Lax从浏览器层面就给跨站请求上了一道保险。Lax模式下跨站场景中只有“顶层导航”类请求比如用户点击链接跳转、window.open打开新标签会携带Cookie而img、iframe、fetch这些子资源或异步请求不会携带。这就直接砍掉了大多数传统CSRF POC的生效路径。Strict模式更严格所有跨站场景都不带Cookie副作用是用户体验会略有下降——比如从第三方站点点链接跳回来登录态可能短暂丢失None模式配合Secure属性则完全放开跨站携带适用于确实需要跨站传Cookie的场景。在业务上建议优先用Lax高安全场景用Strict。这是成本最低、效果明显的一层防御也是我在所有新项目中会默认加上的配置。4.3 业务侧增加“用户意愿”校验CSRF的本质是“用户无意中发起了请求”所以业务侧如果能增加一个“用户确实想做这件事”的校验效果会非常好。典型的做法是修改密码必须输入旧密码手机换绑必须输入短信验证码大额转账必须二次确认并输入支付密码敏感操作通过WebAuthn或App扫码确认。这些措施在交互上增加了一步但相当于把“请求发出者”从“浏览器自动行为”变为“用户主动参与的行为”。这一点任何纯粹的技术方案都无法替代。我在审计现有系统时判断CSRF风险级别会先看两点第一请求是否依赖Cookie/Session做身份识别第二操作是否包含一个“用户主动输入的、动态的、不可预测的信息”。如果两点都不满足哪怕套了Token我也会标注为中高风险。4.4 安全测试时重点关注清单每次做CSRF专项测试我都会按这个清单过一遍检查项关注点敏感接口是否校验Token重点看新增、修改、删除、授权类接口Token是否与Session绑定两个用户会话的Token能否互换使用Token是否一次性消费同一个Token能否重复提交多次Token比较是否安全是否使用恒定时间比较函数Cookie SameSite属性是否显式配置策略是否与业务匹配是否存在JSONP/CORS接口可能被用于跨域读取Token或利用信任关系是否校验Referer/Origin若校验是否严格、能否被字符串包含绕过这个清单不复杂但每次都能帮我在集成测试阶段发现漏网之鱼。5. 实操中常见的“翻车”场景与排查心得5.1 问题一DVWA页面白屏或报错新手在搭建DVWA时最常见的报错有两个。第一个是数据库连不上通常是config.inc.php里的数据库密码没改成实际配置或者MySQL端口不对。第二个是PHP版本不兼容老版本DVWA遇到PHP 8.2会直接抛出一堆Warning甚至Fatal Error。解决思路优先选用集成的PHP 7.4运行环境如果坚持用更高版本需要开启兼容模式并安装对应扩展查看报错日志时重点关注config目录下文件是否加载成功。我通常的做法是先确认php -m能看到mysqli扩展再确认config.inc.php里的连接串字段没有拼写错误。5.2 问题二CSRF POC页面打不开如果你已经写好了img src型POC放到本地Web服务器里打开却发现目标密码没被修改先别急着怀疑漏洞不存在。绝大多数情况是这三类原因当前浏览器对跨站子资源请求不携带CookieSameSite或第三方Cookie阻止。受害者的Session已经过期或被应用刷新。DVWA配置了登录后跳转未登录状态下的请求被拦截到登录页。排查方法打开浏览器开发者工具切到Network面板观察POC页面触发的那条请求的状态码和请求头。如果Sec-Fetch-Site显示cross-site并且没有Cookie头基本就是SameSite的问题。这时候可以临时降低测试环境的浏览器Cookie策略或者改用window.open、a链接跳转等顶层导航方式触发请求。5.3 问题三Medium级别绕过方案失败很多人在Medium级别绕Referer校验时第一反应是把恶意页面放到本地别名路径下或者直接在浏览器地址栏输入POC链接。前者不一定生效因为双方是不同端口Referer字段可能不包含目标服务器名后者则会让请求走到全新的导航流程不一定满足模拟作案场景。从我的经验看最稳妥的做法是准备一个自定义域名比如evil.com利用通配子域名或路径前缀构造一个含有目标服务器名称的访问地址。在测试DVWA时也可以用另一种方式直接修改本地hosts文件把某个别名解析到攻击者页面所在的IP再让这个别名里带上目标机的域名片段。思路正确之后再去看Referer头是否符合预期即可。5.4 问题四手动测试和自动化扫描结果不一致扫描器报CSRF手动复现却不生效或者反过来扫描器没报手工构造却成功了。这类不一致在安全测试里太常见了。原因是扫描器通常只做“静态逻辑判断”和“简单回放”而手工测试可以组合多种条件比如域名欺骗、同站子域名、Cookie属性、业务前置条件。遇到这种情况我会以手工链路为准但会把扫描器报的每一项都记录下来筛选排查。CSRF这类漏洞测试的重心不应该是“能不能打”而是“业务设计上允不允许不打”。多从业务需求和边界场景去推演比闷头构造POC更能发现深层问题。6. 用DVWA验证之后顺手做一次“安全加固复盘”最后再说说我在实际使用DVWA做CSRF测试之后的一些体会希望对你有实际帮助。我第一次完整跑完这四档级别的CSRF利用最大的感受是一个漏洞的“危害程度”不完全取决于代码写得好不好也取决于攻击者能控制的场景有多少。Low级别里攻击者只需要一个img标签Medium级别里攻击者必须拥有一个可操控的域名或一个可写文件的位置High级别里攻击者必须找到一个能获取Token的同源漏洞。安全等级每提升一档攻击链的复杂度就会翻几倍。这也是我们在做防御时反复强调纵深和成本控制的原因——只要让攻击者觉得“不划算”漏洞的价值就降了一半。如果你刚入门建议在DVWA里把Low和Medium两个级别完完整整地走一遍务必自己手动构造POC、自己抓包看Referer和Cookie别直接抄现成脚本。这个过程能帮你把“浏览器自动携带Cookie”“同源策略”“Referer头”这几个基础概念彻底串起来。等你对攻击链路有了肌肉记忆再去看High级别的Token校验就会理解Web框架里那些默认配置到底在防谁。如果是在真实业务系统里做防御我个人的建议是优先确保每个敏感接口都有强随机且一次性消费的Token同时把SameSite设为Lax再叠加一层业务侧的“用户主动确认”校验。三层都做齐了CSRF基本可以从你的风险清单里划掉了。剩下的精力可以投入到XSS和访问控制这类更容易被利用的问题上。