做Web安全测试绕不开DVWA靶场而DVWA里的CSRF模块几乎所有人第一次刷完都觉得就这。但我带过的人里十有八九只记住了点个链接改密码真要让他们说清楚Low和Medium差在哪、High为什么不能直接打、Impossible到底做了哪些事马上卡壳。这篇我打算用DVWA靶场把CSRF从原理到实操完整捋一遍从Docker搭环境开始到四个安全级别逐一通关顺带记录我在这个模块上踩过的坑和排查经验。不管你是刚开始接触Web安全的新手还是想系统化理解CSRF攻防的测试人员这篇都能直接照着操作。1. 先把DVWA靶场跑起来Docker方式十分钟搞定1.1 为什么我推荐用Docker跑DVWADVWA本身是一个PHPMySQL的Web应用对环境要求不复杂但麻烦就麻烦在兼容性。早些年我自己在Kali上手动搭过PHP版本和MySQL认证插件稍微不对付页面就开始报错什么mysqli::real_connect(): The server requested authentication method unknown to the client之类的排查起来非常浪费时间。Docker方案的好处在于官方维护的编排文件把Apache、PHP、MySQL的版本和配置全部固定好了你只需要把整个容器组合拉下来启动不需要在宿主机上装任何运行环境。整个DVWA依赖的数据库、目录权限、初始配置都在容器生命周期内自动完成。对我这种不想为靶场折腾环境的人来说这是最省事的路径。而且容器删了重建也就几秒钟反复做实验非常方便。1.2 实操利用Docker Compose搭建DVWADVWA官方仓库直接提供了docker-compose配置操作步骤只有三步。先在终端里执行git clone https://github.com/digininja/DVWA.git cd DVWA docker compose up -d如果你的机器还没装Docker Compose先装一下。Kali里一般都有docker.io和docker-compose没有的话用apt install docker.io docker-compose补上。第一次启动会拉取两个镜像一个负责ApachePHP跑DVWA代码一个负责MySQL存数据。拉取镜像的时间取决于网络情况国内环境可以给Docker配置一个镜像加速器不然可能要等很久。启动完成后在浏览器里访问http://127.0.0.1:8080/login.php如果能看到DVWA的登录页说明容器已经正常工作了。8080这个端口来自docker-compose.yml里配置的端口映射把容器内部的80端口映射到了宿主机的8080端口。如果你本机8080被占用了可以改映射比如8090:80。接下来是初始化数据库。第一次访问DVWA时系统会提示你数据库还没建立需要点击页面上的Create / Reset Database按钮。这个按钮会触发DVWA自带的初始化脚本自动建表、插入默认数据。点完之后回到登录页使用默认账号登录用户名admin 密码password登录后建议顺手把配置文件检查一下。DVWA的数据库连接配置在config/config.inc.php里官方仓库给的是config.inc.php.dist模板Docker容器里会自动处理但如果你后面要自定义数据库密码就会用到这个文件。手动搭建读者注意别漏了改名这一步。1.3 初始化环境与安全级别设置DVWA自带一个安全级别设置页面路径是DVWA Security里面可以选low、medium、high、impossible四个等级。不同等级对应完全不同的代码逻辑这正是我们练习CSRF的核心场景。我的建议是刚开始刷的时候按Low到Impossible的顺序逐个通关每个等级都看清楚代码是怎么写的再动手验证攻击思路。不要一上来就调到Impossible那样就失去了练习的意义。这里还要提一嘴Burp Suite。练习CSRF时浏览器开发者工具虽然够用但Burp能更方便地改请求、看响应、对比不同安全级别下的差异。如果你用Burp记得把浏览器代理指向127.0.0.1:8080注意别和DVWA的容器端口搞混了——Burp默认监听8080DVWA容器也可能映射8080两个偏偏撞在一起是常见事故。我习惯把DVWA映射到其他端口比如8081省得和Burp抢。2. CSRF漏洞底层逻辑为什么浏览器会帮攻击者代签2.1 用快递代签的类比讲清CSRFCSRF的全称是Cross-Site Request Forgery中文叫跨站请求伪造。要理解它我习惯用快递代签做类比。你下单买了个东西快递员把包裹送到门口只要签收单上写的是你的名字快递员默认这就是你本人签收的包裹一放就走了。攻击者干的事情就是趁你已经下过单、知道你那个快递员几点会来的情况下抢在快递员面前在签收单上写你的名字让快递员以为是你本人要求把包裹给他。映射到Web世界就是这样你已经登录了一个网站浏览器里存着有效的会话Cookie。攻击者构造一个请求诱导你的浏览器向那个网站发送。网站一看请求带着你的Cookie就认为这是你本人发起的操作照单全收。攻击者自始至终不知道你的Cookie是什么但他不在乎因为浏览器会自动带上。DVWA的CSRF模块就是把改密码这个典型场景做成了一道题。网站提供了一个修改密码的URL通过GET参数直接传新密码没有任何来源校验和身份复核。只要你登录着DVWA再访问了这个URL密码就被改了——这就是CSRF攻击的完整链路。2.2 形成CSRF的三个硬性条件不是说随便跨个站发个请求就算CSRF要成立至少得同时满足三个条件第一用户已经登录目标网站而且会话状态在浏览器中依然有效。这是前提中的前提你都没登录攻击者再怎么诱导也没用。第二目标网站存在能够改变状态的操作接口。比如修改密码、转账、发帖、改收货地址。如果只是个查询接口比如查个天气那就算跨站请求能发过去也不构成安全问题。CSRF关注的是状态变更类操作。第三用户浏览器在不知情的情况下会携带会话凭证发起请求。Cookie天然满足这个条件只要请求的域名符合Cookie的Domain属性浏览器就自动带上。这也是CSRF至今仍然存在的原因——它是HTTP协议和无状态Cookie体系的固有特性。这里很多人有个误解觉得同源策略能防住CSRF。我特意要说清楚浏览器的同源策略管的是能不能读取跨域响应它并不限制能不能发送跨域请求。img可以加载外站图片a可以跳转到外站form可以把数据提交到外站这些都是浏览器默许的行为。Cookie跟着请求去了外站但跨站页面里的JavaScript是无法读到Cookie值的——同源策略卡在了读这一层没卡在发这一层。2.3 和XSS、SSRF的区别很多人栽在这里我见过不少新手把CSRF、XSS、SSRF混在一起遇到请求伪造类漏洞就傻傻分不清。用一个信任维度的说法就能拆开CSRF信任的是浏览器携带的凭证攻击者利用的是用户已经登录的身份XSS信任的是注入的脚本内容攻击者利用的是服务端或客户端没有过滤的输入SSRF信任的是服务器自己发起的请求攻击者利用的是服务端可以访问内网资源的特性。三者的区别可以看这张对比表漏洞类型攻击者利用的信任方攻击目标典型载体CSRF用户浏览器自动携带的凭证用户身份下的状态变更操作HTML页面、图片标签、跨站表单XSS服务端输出的用户可控内容在用户浏览器中执行脚本恶意脚本、事件属性、URL参数SSRF服务端发起的网络请求内网资源、云元数据接口URL参数、导入功能、Webhook配置CSRF和XSS经常被放在一起聊还有个原因是它们可以配合使用。CSRF最大的短板是攻击者无法看到受害者的响应内容遇到Token防护就没辙了。但如果有XSS漏洞攻击者注入的脚本就能在受害者浏览器里直接读取Token、发起请求相当于给CSRF装上了眼睛。DVWA的High级别就是演示这个配合的经典场景。3. DVWA四个安全级别的CSRF攻击实操记录3.1 Low级别裸奔的密码修改接口先把DVWA安全级别设为Low然后打开CSRF模块页面。这个页面上有个改密表单填下新密码和确认密码点Change就能修改。但关键点不在于表单本身而在于提交请求的构造方式。Low级别的服务端代码是直接信任所有请求的核心逻辑就是if( isset( $_GET[ Change ] ) ) { $pass_new $_GET[ password_new ]; $pass_conf $_GET[ password_conf ]; if( $pass_new $pass_conf ) { // 直接更新数据库里的密码 } }注意这个接口用的是GET请求。这意味着不需要表单提交浏览器地址栏里直接拼参数就能触发密码修改。把URL构造出来http://127.0.0.1:8081/vulnerabilities/csrf/?password_new123456password_conf123456ChangeChange先用admin账号登录DVWA然后在同一个浏览器的另一个标签页里访问这个URL密码直接就被改成123456了。你可以再用123456试试登录会非常直观地看到效果。这是最基础的方式但真正要模拟受害者被攻击的场景需要把这个URL藏在一个诱导页面里。比如把下面这段HTML保存成一个csrf_demo.html文件放到任意静态服务器上或者直接用File://协议打开!DOCTYPE html html body a hrefhttp://127.0.0.1:8081/vulnerabilities/csrf/?password_new123456password_conf123456ChangeChange点我看美女照/a /body /html受害者只要在已登录DVWA的浏览器里点击这个链接密码就被改了。如果攻击者不想依赖用户点击可以用图片标签让浏览器自动发起请求img srchttp://127.0.0.1:8081/vulnerabilities/csrf/?password_new123456password_conf123456ChangeChange width0 height0只要受害者打开了包含这个标签的页面浏览器就会自动加载图片向目标地址发出GET请求DVWA照单全收。这就是CSRF里常用的零点击攻击。Low级别的实操到这里就完事了。它想表达的核心是如果服务端不做来源校验任何能发请求的载体都能成为攻击工具。a、img、form、iframe、fetch随便选。3.2 Medium级别Referer校验的绕过思路把安全级别调到Medium再试之前的攻击方式会发现密码改不成功了。原因在于服务端加了一层Referer头校验。代码长这样if( stripos( $_SERVER[ HTTP_REFERER ] , $_SERVER[ HTTP_HOST ] ) ! false ) { // 校验通过允许改密 }这段代码的思路是检查HTTP请求头里的Referer字段看里面是否包含当前站点的Host名。如果包含说明请求来自本网站如果不包含就认为是跨站请求拒绝执行。这么说起来逻辑好像没问题但实现有个致命缺陷校验方式是包含匹配而不是域名精确匹配。只要Referer字符串里包含目标Host字样就能通过验证。绕过思路就很清晰了。假设DVWA跑在192.168.1.100:8081Host值是192.168.1.100:8081。攻击者可以把自己恶意页面的部署地址构造成http://192.168.1.100:8081.evil.com/csrf.html这样浏览器发起跨站请求时Referer就是http://192.168.1.100:8081.evil.com/csrf.html服务端用stripos查找字符串192.168.1.100:8081发现Referer里确实包含这段校验直接通过。这是一种典型的子域名/域名前缀绕过思路。如果是实战环境攻击者需要能控制一个域名下的页面地址把目标Host拼进域名或路径里就能绕。比如http://csrftest.com/192.168.1.100:8081/evil.htmlReferer里同样会包含目标Host字符串也能通过校验。在DVWA本地练手时还有一个更直接的办法用Burp Suite拦截改密请求把请求头的Referer字段手动改成包含127.0.0.1:8081的值比如Referer: http://127.0.0.1:8081/whatever服务端只要检测到Referer包含Host这次请求就能通过。这个方式虽然实战中用不上因为攻击者没法控制受害者浏览器的请求头但用来理解校验原理非常管用。有一个细节必须提醒Medium级别的校验用的是stripos如果Referer为空字符串stripos会返回false校验失败。所以网上有些文章说把Referer删掉就能绕过在DVWA的这个场景里是不成立的。空Referer只对检查严格、非空即拒的站点有意义对包含式匹配的站点反而死得很难看。3.3 High级别Token防护下的利用方式把安全级别调到High再尝试之前的绕过方式会发现即使Referer完全正确请求依然失败。因为High级别引入了Anti-CSRF Token机制。打开CSRF页面时页面上隐藏字段user_token的值和当前用户的Session进行了绑定。每次提交改密请求服务端都会校验提交的Token是否和Session里的一致session_start(); if( isset( $_GET[ Change ] ) ) { $token $_SESSION[ token ]; if( $_GET[ user_token ] ! $token ) { exit( CSRF token mismatch. ); } }Token是个随机字符串存储在Session里页面上的表单每次刷新都会变化攻击者无法预先知道当前Session里的Token值。换句话说你构造一个静态的攻击URL里面带一个固定Token是永远对不上号的。那High级别就无解了吗当然不是。DVWA设置这个等级的目的是让你理解Token防护的真正意义以及它为什么不是万能的。Token的获取难点在于攻击者读不到受害者的响应但如果有办法在受害者的浏览器里执行脚本Token就是透明的。最经典的利用组合是配合存储型XSS。DVWA里有个XSSStored模块允许留言本里的内容被存储并展示给所有访问者。如果能把提取Token并自动提交改密请求的脚本注入到留言本里那么管理员只要访问留言本脚本就会在它的浏览器里运行。payload大致长这样script var xhr new XMLHttpRequest(); xhr.open(GET, /vulnerabilities/csrf/, false); xhr.send(); var token xhr.responseText.match(/user_token\s*value(.*?)/)[1]; var attack new XMLHttpRequest(); attack.open(GET, /vulnerabilities/csrf/?password_new123456password_conf123456user_token token ChangeChange, true); attack.send(); /script流程是这样的脚本先同源请求CSRF页面拿到响应HTML用正则提取出当前有效的Token再带着这个Token提交改密请求。因为脚本是在受害者浏览器里执行的Cookie和Token都自动携带服务端完全分辨不出来这到底是不是用户本人在操作。实操时我会把DVWA安全级别切到Low先在XSS存储模块里提交payload然后把安全级别调回High用管理员账号访问留言本页面密码立刻变掉。这演示了一个非常重要的概念任何CSRF漏洞的利用都可能因为XSS的加持从不可能变成可能。反过来也说明只靠Token并不是绝对安全的把输入过滤做好同样重要。3.4 Impossible级别正确防护长什么样Impossible级别展示的是DVWA作者认为足够安全的写法。打开代码可以看到防护从三个维度叠加第一校验Token而且Token是绑定Session且每次提交后更新第二校验HTTP Referer对来源进行限制第三修改密码必须输入当前密码且新密码不能与当前密码相同。部分版本还要求密码符合强度规则。if( $_GET[ user_token ] ! $_SESSION[ token ] ) { exit( CSRF token mismatch. ); } if( $pass_new $pass_current ) { // 新密码不能等于当前密码 } // 更新密码即使在存在XSS的情况下攻击者也最多能拿到Token但拿不到用户的当前密码。这是对关键操作做二次确认的典型示范也是日常开发中面对高风险操作应该采取的默认姿态。四个级别的防护措施差异我用表总结一下安全级别是否校验Token是否校验Referer是否需要当前密码是否能直接利用Low否否否是Medium否是包含匹配否可绕过High是绑定Session否否需配合XSSImpossible是是是极难4. 实战中的常见问题与排查技巧实录4.1 一份可以直接套用的CSRF检查清单刷完DVWA你会对CSRF有一个立体认识。但拿到真实项目里测CSRF光记着DVWA里的操作是不够的。我整理了一份我平时做安全测试时用的检查清单照着查基本能锁定大部分CSRF问题。第一看关键操作使用的HTTP方法。凡是修改密码、绑定手机、修改邮箱这类状态变更操作如果用GET请求大概率存在CSRF风险。因为GET请求可以被img标签自动触发攻击成本极低。正确做法是使用POST但注意POST本身不防CSRF只是增加了利用门槛。第二看请求里有没有Token以及这个Token是不是有效防护。很多站点喜欢往表单里塞一个隐藏Token但Token是固定值或者从Cookie里直接读取实际没有和Session绑定这种Token就只是样子货抓包拿到后照样能伪造。第三看服务端有没有校验来源。检查代码里是否对Referer或Origin头做限制以及限制做得是否严格。如果只是包含匹配很容易被域名前缀绕过绕过。第四看Cookie的SameSite属性。如果关键业务的会话Cookie设置了SameSiteLax或Strict浏览器在跨站请求时就不会带上CookieCSRF会从根上失效。但要注意SameSite只对跨站请求有效子域名和同站请求不在限制范围内所以不能把它当唯一防线。第五看高风险操作有没有二次认证。修改密码、绑定新手机号这类操作是否要求输入当前密码或验证码。如果有即使CSRF请求能到达服务端也会卡在身份复核这一步。4.2 常见误区与排查思路我在实战中见过不少同学拿着DVWA的经验去测真实系统结果误判频出。我总结几个高频误区你看完能少走弯路。误区一认为同源策略能防CSRF。这个前面说过同源策略限制的是JavaScript跨域读取响应并不阻止跨域发送请求。Cookie、表单、图片请求都能跨站发出。所以不能因为我们是同源的安全就忽略CSRF校验。误区二认为Token存在就安全。很多站点的Token是写在Cookie里的或者生成逻辑可预测比如时间戳加固定盐。这类Token要么直接被盗用要么可以被枚举。真正有效的Token必须满足三个条件随机性足够强、和当前用户Session绑定、每次使用后更换。误区三认为校验了Referer就万无一失。Referer头本身可以由服务端配置、浏览器插件、HTML标签上的referrerpolicy属性影响甚至有些场景下浏览器根本不发送Referer。安全开发里Referer校验只能作为辅助手段不能作为主力。误区四觉得HTTPS能防止CSRF。HTTPS解决的是传输过程中的窃听和篡改问题不解决身份冒用问题。攻击者诱导受害者浏览器发起的请求同样会走HTTPS服务端收到的还是一个带Cookie的合法请求照样被信任。排查问题上我的思路是这样的先用Burp拦截一个正常的关键操作请求看清楚请求参数里有哪几个是服务端校验的然后拿着这个请求直接改Session、删Cookie、修改Referer、去掉Token分发明发观察服务端反应每改一个变量发一次记录是否还能成功。通过这种正交测试可以快速定位服务端到底校验了哪些因素。如果想用脚本自动化验证CSRF是否存在可以写一个简单的Python脚本模拟不带Token提交请求的行为import requests session requests.Session() login_data { username: admin, password: password, Login: Login } session.post(http://127.0.0.1:8081/login.php, datalogin_data) r session.get( http://127.0.0.1:8081/vulnerabilities/csrf/, params{ password_new: test1234, password_conf: test1234, Change: Change } ) print(Status:, r.status_code) print(Access:, r.text[:300])如果这个不带Token的请求返回了成功页面说明请求没有经过Token校验CSRF风险已经坐实。一个合格的测试人员不应该只停留在能发请求层面而是要清晰描述出缺失了哪一道防护。4.3 在DVWA实操中我踩过的三个坑最后分享几个我实际刷DVWA时遇到的问题希望能帮你避开同样的弯路。第一个坑Medium级别里把Referer头删成空以为能绕过。我最初看某些资料说删除Referer可以绕过CSRF校验就在Medium级别里尝试结果请求全被拒绝。后来看了源码才发现DVWA的Medium校验用的是stripos(Referer, Host)Referer为空的时候stripos返回false一样会被拦截。空Referer只在站点对Referer做严格白名单、非空才校验的场景下有用。对DVWA这个靶场正确绕法是构造Referer包含目标Host的地址而不是删掉它。第二个坑在Low级别测试改密Burp里请求返回200但用新密码登录时发现密码根本没变。找出原因后哭笑不得我提交参数名写错了。DVWA的改密参数是password_new和password_conf我误写成了new_password和confirm_password服务端取不到参数自然不会有任何动作。这个问题在真实系统测试中也经常遇到建议每次测试前先用正常方式提交一次请求在Burp里看清参数名再动手造攻击载荷。第三个坑High级别里我尝试构造带Token的URL但反复失败。原因是Token每次请求都会变化而且和Session绑定你手动从页面响应里提取一个Token构造好URL再提交可能中间刷新了一下页面Token就失效了。要稳定攻击必须写脚本在同一个请求会话里先读取Token再立即提交中间不能有任何间隔操作。这个坑让我学会了CSRF Token防护下利用脚本必须保持请求链路的完整性。个人体会刷完整个CSRF模块我个人最大的感受是CSRF的核心问题不是怎么把请求发出去而是服务端有没有能力判断这个请求是不是用户本人的真实意图。DVWA从Low到Impossible的四个等级恰好把这道题从完全不管讲到了多因素复核每一步都有非常强的现实映射。建议你在刷完DVWA之后随便找个自己写的小项目试着按Impossible的标准把改密码功能重写一遍。你会发现真正难的其实不是写Token校验代码而是搞清楚每一层防护到底在防什么攻击者。有了这层理解以后再看到任何请求伪造类漏洞你都不会再心虚。