CSRF跨站请求伪造原理详解与DVWA/Pikachu靶场实战防御指南
安全行业有句老话不怕黑客水平高就怕开发者流程松。CSRFCross-Site Request Forgery跨站请求伪造就是这么个东西——攻击者根本不需要攻破你的服务器只需要利用你浏览器里已经保存的登录凭证就能在你不自知的情况下以你的身份发出请求。我最早在DVWA靶场里把CSRF从Low打到High时最大的感触是这个漏洞不像SQL注入那样需要猜语法也不像XSS那样需要精心构造载荷它更像是一场信任的滥用——服务器太信任浏览器的Cookie而浏览器又不负责任地把Cookie放行给任何跨域请求。今天这篇是CSRF深度剖析的第一期重点讲清楚三件事CSRF到底是怎么运作的、它在真实场景中长什么样、以及如何在DVWA和Pikachu这两个主流靶场里完成从Low到High级别的完整复现。全文不会堆术语每一步都会拆到为什么就算你之前完全没接触过Web安全也能跟着实操跑通流程。这次先把原理和基础攻防跑通第二期再深入Token缺陷、JSON CSRF等进阶话题。1. CSRF攻击原理拆解跨站请求伪造到底是怎么发生的1.1 一个真实的CSRF攻击场景你什么都没做钱却没了先别看技术定义看我给你画的这个场景。假设你是一家银行的网上银行用户。某天上午你在电脑上登录了网银查完余额后没有退出登录又顺手点开了一个论坛帖子。帖子内容其实没什么特别但帖子里藏了一张图片图片的地址不是一张.jpg而是一个转账接口http://bank.example.com/transfer?toAccountattackeramount1000你的浏览器看到img标签会无条件地向这个URL发起GET请求。此时你的网银登录Cookie还在有效期内浏览器自动把这个Cookie附加在请求头上。网银服务器收到请求后按照正常的业务逻辑处理——用户已登录、参数齐全、余额充足于是转账成功。整个过程里你只是在论坛里打开了一个帖子没有输入密码没有点击确认转账但你的钱就是被转走了。这就是CSRF。不夸张地说CSRF是所有Web漏洞里性价比最高的一个攻击者几乎不需要了解目标系统的内部代码不需要寻找注入点只需要找到一个以GET或自动可提交方式执行敏感操作的接口然后诱导受害者打开一个恶意页面即可。1.2 同源策略明明存在为什么拦不住CSRF很多刚接触安全的同学会困惑浏览器不是有同源策略Same-Origin Policy吗bank.example.com和evil.com不是不同源吗为什么evil.com发起的请求bank.example.com还会给响应这里有个关键误区需要纠正同源策略限制的是读取而不是发送。浏览器确实不允许evil.com的JavaScript直接读取bank.example.com的响应内容比如用它返回的JSON数据、操作它返回的DOM。但浏览器并不阻止你的浏览器向bank.example.com发起一个请求——请求发出去了服务器照常处理Cookie照常携带。这个逻辑可以用生活场景类比你把自己的家门钥匙Cookie交给了物业浏览器物业规定外人不能进你家看你家装修不能读取跨域响应但物业并不会阻止任何人往你家的门缝里塞信发送请求。真正的拦路人应该是房东服务器可房东只看钥匙不看人结果自然被人钻了空子。换句话说CSRF成立的三个前提缺一不可受害者已登录目标站点且浏览器中存在有效的会话Cookie或Basic Auth等自动携带的凭证该站点存在仅凭请求参数即可触发操作的敏感功能比如改密、转账、发帖浏览器默认允许跨域请求并自动携带目标站点的Cookie旧版Cookie策略下尤其如此1.3 CSRF与XSS的本质区别一个抢钥匙一个冒充本人很多人分不清CSRF和XSS这里我用一句话点透XSS是攻击者往你的页面里植入恶意脚本让脚本替你干活CSRF是攻击者在自己的页面里伪造请求让你的浏览器替你干活。XSS攻击的是客户端脚本环境它需要向目标页面注入一段可执行的JavaScript属于代码注入类漏洞。而CSRF攻击的是服务端的信任机制它不需要在目标站点注入任何内容只要受害者浏览器里有凭证攻击者随便找个地方就能构造攻击页面。举两个对比场景XSS你在论坛发了一篇帖子帖子内容里藏了script代码其他用户打开帖子时脚本自动执行窃取他们的Cookie。CSRF你在博客里放了一张图片图片指向论坛的发帖接口任何打开你博客的已登录论坛用户都会在毫不知情中帮你发一篇文章。从防御角度两者的思路也完全不同。XSS的防御重点是输入输出过滤而CSRF的防御重点是验证请求来源或增加不可预测的校验数据。2. 攻击场景与三种典型利用方式2.1 GET型CSRF只用一行代码就能打GET型CSRF是最简单的形态攻击者把敏感操作封装进一个GET请求然后利用浏览器特性诱导受害者触发。触发方式远不止img一种我实测中常用这几种!-- 方式一图片标签最隐蔽用户完全无感知 -- img srchttp://bank.example.com/transfer?toAccountaaaamount1000 styledisplay:none / !-- 方式二iframe内嵌常见于需要用户停留在当前页的场景 -- iframe srchttp://bank.example.com/transfer?toAccountaaaamount1000 styledisplay:none/iframe !-- 方式三超链接配合诱导文案 -- a hrefhttp://bank.example.com/transfer?toAccountaaaamount1000领取限时优惠券/a这里面最阴险的其实是img标签。很多思路谨慎的开发者会想POST接口不就行了——且慢先往下看。2.2 POST型CSRFAutoSubmit表单大法很多开发团队防CSRF的第一直觉是把GET改成POST。这个想法有一定道理因为POST请求确实无法通过img标签触发但这并不意味着POST就安全。攻击者只需用JavaScript动态生成一个表单并立即自动提交POST型CSRF就成立了html body script function createAndSubmit() { var form document.createElement(form); form.method POST; form.action http://bank.example.com/transfer; var input1 document.createElement(input); input1.name toAccount; input1.value attacker; var input2 document.createElement(input); input2.name amount; input2.value 1000; form.appendChild(input1); form.appendChild(input2); document.body.appendChild(form); form.submit(); // 页面会跳转但用户大多数情况下来不及反应 } window.onload createAndSubmit; /script /body /html把这段代码扔到攻击者的服务器上受害者只要访问了这个页面表单就会自动提交。为什么POST防不住因为HTML表单自带跨域提交能力这是浏览器从诞生之初就支持的特性。而CSRF的本质就是浏览器允许跨域发送请求、服务器不加甄别地处理请求。所以GET和POST在CSRF面前只是难度分水岭不是安全分水岭。2.3 绕过手段Referer校验是怎么被突破的说过GET和POST接下来聊聊对抗升级——当服务端开始校验Referer头即该请求是从哪个页面发送的时攻击者有哪些绕过思路。最常见的错误实现是只做包含匹配不做完整源匹配。比如开发者写了一段代码if (strpos($_SERVER[HTTP_REFERER], example.com) ! false) { // 校验通过 }这段代码的本意是只有来自example.com的请求才算合法但问题在于strpos做的是包含判断。攻击者可以直接把恶意页面挂在example.com.evil.com这个域名下由于Referer值是http://example.com.evil.com/xxx里面天然包含了example.com子串校验就会被直接绕过。我在DVWA靶场的Medium级别里复现的正是这个场景。DVWA Medium的防CSRF代码就是检查Referer里是否包含目标域名所以需要在攻击页面里构造一个与目标带相同域名的假地址或用Referer为空的场景绕过。具体来说有两种方式将恶意页面放在一个与目标站点域名相似但可控的域名上如victim.com.evil.com通过data:URI或某些沙箱环境发起请求让浏览器发送空Referer空Referer的情况一定要注意。很多站点的校验逻辑是if (isset($_SERVER[HTTP_REFERER]))直接放行空Referer——这等于给攻击者开了门。并且如果目标网站支持在地址栏填URL跳转攻击者甚至可以在同域名下构造路径既满足同域判断又能触发请求。3. 实战复现DVWA与Pikachu靶场下的CSRF攻击3.1 环境准备两套靶场的部署先把我常用的环境交代清楚。我这里是基于Docker搭建的PHP靶场环境DVWA和Pikachu都用默认配置部署在同一台虚拟机里。DVWA的部署很简单Docker一条命令docker run --rm -it -p 8080:80 vulnerables/web-dvwaPikachu也一样拉镜像后跑起来docker run --rm -it -p 8081:80 area39/pikachu如果你不想用Docker也可以直接在本地搭PHP环境把它们放到phpstudy或XAMPP的www目录下导入数据库后访问安装页面即可。注意DVWA首次打开会跳到安装页默认账号是admin/password必须先登录再把安全级别设置为Low。Pikachu不需要登录打开首页点进Cross-Site Request Forgery模块就能看到练习入口。3.2 DVWA Low级别的完整复现DVWA的CSRF模块提供了一个修改密码的功能提交信息如下GET /dvwa/vulnerabilities/csrf/?password_newadmin123password_confadmin123ChangeChange HTTP/1.1 Host: 127.0.0.1:8080 User-Agent: Mozilla/5.0 Accept: text/html Cookie: PHPSESSIDxxxxxxxxLow级别的代码没有任何防护直接对参数进行密码修改?php if (isset($_GET[password_new]) isset($_GET[password_conf])) { if ($_GET[password_new] $_GET[password_conf]) { $pass_new $_GET[password_new]; $pass_new mysqli_real_escape_string($GLOBALS[___mysqli_ston], $pass_new); $pass_new md5($pass_new); // 更新密码 } } ?看到了吧无Token、无Referer校验、无任何不可预测数据。攻击者只需要拿到目标站点的完整URL和Cookie字段就能构造攻击页面。我实测的完整攻击步骤如下第一步打开DVWA靶场的CSRF模块记录下修改密码接口的完整URLhttp://127.0.0.1:8080/dvwa/vulnerabilities/csrf/?password_newabc123password_confabc123ChangeChange第二步写一个攻击页面csrf-get.html放在攻击者自己的服务器上html headtitle免费抽奖/title/head body h1恭喜你获得一次抽奖机会请点击这里查看结果/h1 img srchttp://127.0.0.1:8080/dvwa/vulnerabilities/csrf/?password_newadmin123password_confadmin123ChangeChange width0 height0 / /body /html第三步让受害者已登录DVWA的账户访问这个页面。图片加载时会自动发出GET请求DVWA收到后直接改密成功。实测中受害者页面右下角会短暂出现一个断开的图片图标但绝大多数用户根本不会注意。这个Level的关键不是技术难度而是让读者理解改密这种敏感操作居然只用GET、还没有任何防护这一糟糕实现到底有多危险。3.3 Medium级别与High级别的绕过分析把DVWA安全级别切成Medium再登录DVWA的CSRF模块查看源码可以发现Medium级别加入了一段Referer检查?php if (stripos($_SERVER[HTTP_REFERER], $_SERVER[SERVER_NAME]) ! false) { // 通过检查才执行改密 } ?这段代码的问题我前面已经提过stripos是大小写不敏感的包含匹配只要Referer里包含服务器域名就算通过。对付它有两种思路。第一种是在攻击页面里预置一个同域路径跳转。DVWA靶场允许通过URL参数?page进行页面跳转不同版本的DVWA对这个功能的实现不同但很多真实应用都有类似的可控跳转点攻击者可以把攻击脚本挂在一个看似与目标同域的位置上最典型的就是http://127.0.0.1:8080/xxx这种路径前缀可以放在Referer里利用子串匹配绕过。我这里提供一个稳妥的通用思路找一个支持window.location修改的中间页或利用一个data:URI页面发起请求。当浏览器从data:协议发起导航时大多数浏览器的Referer策略会发送null而那张stripos代码在变量为空时会直接进入false分支改密即失败——但如果你发现服务端逻辑是if (empty($referer))就当校验通过了那天然就能绕过。再说High级别。DVWA High级别引入了Token机制每一次改密都附带一个随机的user_token值input typehidden nameuser_token value?php echo generateSessionToken(); ? /在这种情况下攻击者无法预知Token值无法直接构造有效请求。这里的打不了本身就有教学意义CSRF最有效的防线就是不可预测的校验参数。但High级别并非无懈可击——如果目标站同时存在XSS攻击者可以先用XSS读取页面中的Token再附带完成CSRF攻击。XSS加CSRF的组合拳就是后话了这也是我第二期想重点展开的内容。3.4 Pikachu靶场CSRF模块实战Pikachu靶场的CSRF模块和DVWA风格不太一样它模拟的是修改个人信息的场景。打开CSRF子模块填写姓名、邮箱后提交抓到请求长这样GET /pikachu/vul/csrf/csrfget.php?sexmanphonenum123456addsomeaddressemailtest%40test.comsubmitsubmit HTTP/1.1 Host: 127.0.0.1:8081 Cookie: PHPSESSIDyyyyyyyy同样没有Token没有Referer校验直接GET修改。Pikachu的训练重点在于**跨域携带Cookie的可见性**——攻击者在自己域名的页面上构造表单自动提交修改的却是Pikachu站点的信息整个过程一气呵成。我建议你把Pikachu和DVWA两个靶场都跑一遍原因有二DVWA帮你分级理解从无防护到Referer校验再到Token机制难度逐级递进适合建立防御心智Pikachu帮你贴近实战它模拟了个人信息编辑这类非金融级敏感操作更容易让读者意识到——CSRF的面并不只有转账、改密任何用户可操作的功能都可能被滥用4. 防御方案从开发端到用户端的加固实践4.1 CSRF Token机制目前最主流的防线如果要用一句话概括CSRF的本质那就是服务器无法判断当前请求是用户自愿发出的还是被第三方诱导发出的。所以防御的核心思路是在请求中增加一个只有目标站点能够提供的、且攻击者无法预知的字段。这个字段就是CSRF Token。它的实现流程是这样的用户访问带有敏感操作的页面时服务器生成一个随机的Token值存储在会话中$_SESSION[csrf_token]同时嵌入页面的隐藏表单字段用户提交表单时Token随表单一起提交到服务器服务器校验收到的Token与会话中存储的Token是否一致由于攻击者的恶意页面无法读取目标站点的响应所以无法提前获取Token值又因为攻击者无法在受害者Cookie中插入自定义Token所以等于天然地被隔离在有效请求之外。结合我踩过的坑给出两个Token实现要点Token必须绑定会话Session。有些粗糙的实现把Token做成固定的、或只放在表单里不存会话这等于虚设。正确做法是每次生成新Token或至少做一次会话级别的服务端校验。不要把所有Token校验都放在前端JavaScript里做。我在某个项目里见过用AJAX获取Token、然后前端统一加到请求头的方式如果XSS存在攻击者可以直接调用这个AJAX接口读取Token。另外对于REST API场景业界常采用**双重提交CookieDouble Submit Cookie**方案服务器在Cookie中设置一个随机的Token值同时要求前端每次请求附带相同Token的请求头或请求参数服务器校验收到的两个值是否一致。因为攻击者无法在受害者的Cookie域中写入自定义值所以形成了另一层防线同时也解决了原生Token方案在无状态服务上的会话绑定难题。4.2 SameSite Cookie与验证码辅助除了Token另一个值得重视的现代方案是SameSite Cookie属性。这个属性告诉浏览器跨站请求时不要携带这个Cookie。SameSiteLax模式下只有顶级导航如点击链接才携带Cookie而img、iframe、AJAX等跨站子资源请求都不携带SameSiteStrict则更严格只有同站请求才带。Chrome从80版本开始默认将Lax作为Cookie的默认SameSite行为这确实让CSRF的杀伤范围缩小了不少——被动的img型攻击基本被打掉。但要注意Lax模式下用户点击链接后跳转发起的GET请求仍然会携带Cookie所以GET型但绑定用户主动点击的场景仍可能被利用。对于某些敏感GET动作只靠SameSite Lax还不够稳妥。验证码则适用于高价值操作。为什么转账、改密这类操作值得加验证码因为浏览器在加载图片时不会弹出验证码但用户在提交表单时确实会看到验证码输入框——攻击者无法在跨站环境中模拟这个过程。验证码的出现天然切断了自动提交的链路。当然验证码会牺牲用户体验不能每个操作都加通常只给修改密码、绑定手机号、大额转账这些最核心的动作。4.3 开发基线与测试建议从开发规范的角度我给出一个可以直接抄的检查清单防护手段强度适用场景注意事项敏感操作仅接受POST低基础门槛表单跨域提交仍在不够Referer/Origin校验中防误点不能只做包含匹配要校验完整源Random Token高表单提交类功能必须绑定会话且防XSS双重提交Cookie高无状态API别用于Cookie被XSS读取的场景SameSiteLax中高跨站Cookie限制用户点击导航时仍带Cookie验证码/RSA签名最高高价值操作牺牲体验只用于关键动作测试建议上Burp Suite的CSRF PoC generator是个省力工具。在Burp里拦截一个敏感请求右键选择Engagement tools - Generate CSRF PoC它会自动生成一个跨域提交表单的HTML文件你直接复制保存为HTML用浏览器打开即可验证当前请求是否存在CSRF漏洞。我每次做代码评审都会把这个流程跑一遍通常能在5分钟内判断一个接口的安全性。4.4 常见问题与排查技巧我在带学员和做企业评估时经常遇到一些和CSRF相关的典型问题整理出来供参考问题1我们已经加了Token为什么还被绕过先检查Token是否绑定会话。如果Token是存在Cookie里但服务端没有严格的会话级校验或者Token值可以从某个公开接口获取都会导致绕过。另外检查代码里是否有if (isset($_POST[token]))这类逻辑——如果攻击者不提交Token字段而你又不校验是否为空就会直通。问题2登录接口要不要加CSRF防护要加而且必须是高强度的。登录接口虽然本身不需要登录态但攻击者可以利用CSRF对用户实施登录劫持——强制受害者登录到攻击者的账号后续用户的一切操作都会记录在攻击者名下造成隐私数据污染。还有一种登出CSRF也容易被忽略强制用户退出登录本身也是一种骚扰型攻击。问题3用移动App调用API是不是就不用管CSRF了不能一刀切。如果App通过原生客户端发起请求不依赖浏览器Cookie则CSRF风险较低但如果App内嵌了WebView、或同时运行在Web端那么Web端的Cookie依然存在CSRF仍然可能成立。最好的做法是Web端和App端统一采用自定义请求头Token的双重校验。问题4如何快速扫描存量接口是否包含CSRF漏洞手动测试效率很低。我通常用Burp Suite的Active Scan配合CSRF检测插件或者用Arachni、ZAP等自动化扫描器走一遍基础扫描。但自动化工具的检测结果只能作为参考CSRF的确认终究需要在Burp里手工生成PoC验证。写在最后一点实操体会整个DVWA和Pikachu的CSRF流程跑下来我最大的体会是——防御CSRF的经验要在攻击者视角里养成而不是在文档里记住。每写一个敏感接口我都会先自己问一遍如果我是攻击者这个请求我能不能用一个img标签或一段自动提交表单打出来如果能打出就说明还没防到位。我建议你跑完靶场后找一个自己维护的真实项目哪怕是个练习Demo也好试着用Burp的PoC生成器测一遍再把Token机制加进去对比一下改动前后拦截效果。这一步实操带来的体感比读十篇原理文章都有效。第一期先把原理、场景、基础实战和常规防御讲透。CSRF里还有很多高级对抗的边角——比如Token在Referer里泄漏、JSON CSRF的Content-Type校验绕过、以及XSS加CSRF的组合拳这些都会放到第二期继续拆。希望对你有用也欢迎实际操作中遇到问题来交流。