注意:有关CSRF漏洞的基础知识与原理已在上篇文章指出,此处不再解释。
可参考该文章https://www.cnblogs.com/lky-sec/articles/21412117
本文仅用于网络安全学习与防御研究。所有测试均在本人本地部署、合法授权的 DVWA 靶场中进行,禁止将相关方法用于任何未授权系统。
一 测试范围与环境
本次测试目标为本地DVWA靶场中的CSRF模块。
| 测试环境 | 本地 DVWA 靶场 |
| 安全等级 | Medium |
| 测试系统 | Kali Linux |
| 测试工具 | Firefox、Burp Suite |
| 测试方式 | 黑盒测试、源码审计 |
| 授权情况 | 本人本地靶场,合法授权 |
二 页面与请求分析
1. 设置难度等级
进入DVWA Security,将安全等级设置为Medium,随后进入CSRF模块。

2. 观察模块正常功能
CSRF模块提供密码修改功能,需要分别输入新密码和确认密码。
在两个输入框中输入相同的新密码后点击Change,页面提示密码修改成功。

由此可以确定,该功能会根据用户提交的新密码参数修改当前登录账户的密码
3. 使用Burp Suite进行抓包分析
开启Burp Suite Proxy模块进行拦截,正常修改一次密码并观察HTTP请求。

可以观察到请求头中存在:
Referer: http://localhost:8080/vulnerabilities/csrf/?password_new=1&password_conf=1&Change=Change
Referer 用于表示当前请求由哪个页面跳转或发起。此处不仅包含目标主机 localhost:8080,还保留了上一次访问 CSRF 页面时 URL 中的密码修改参数。
三 手工黑盒测试
1. 使用Repeater模块
右键请求发送至Burp Suite Repeater。

2. 测试Low难度利用方式
将刚才的URL的密码1修改为2,复制此URL,在相同浏览器重新打开此URL。发现DVWA靶场被重新打开,但返回结果显示That request didn't look correct.


3. 尝试绕过请求来源限制
将刚才同时拦截的密码2的请求中,粘贴来自密码1请求的Referer,发现密码更改成功。


此后经过不断尝试,即便Referer中的新密码值与密码确认值不一致,也可修改密码成功。


继续尝试,发现只要Referer保留有http://localhost,即可修改成功。


四 黑盒测试结果分析
| 直接使用Low难度URL修改密码 | 修改失败,返回That request didn't look correct. | Medium等级增加了额外的请求来源校验,Low难度直接构造URL的方式失效 |
| 使用正常DVWA请求中的Referer | 密码修改成功 | Referer会影响服务器对请求合法性的判断 |
| Referer中的密码值与实际请求密码不同 | 仍可修改成功 | 服务器并未校验Referer中的密码参数,其具体参数内容不是判断依据 |
| Referer中的新密码与确认密码不一致 | 仍可修改成功 | Referer中的密码参数不会参与实际密码一致性校验 |
| 将Referer简化为http://localhost | 密码修改成功 | 判断Medium等级主要校验Referer中是否包含目标主机相关内容 |
五 代码审计
1. 查看Medium源码
点击view source查看源码。
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
}
else {
// Didn't come from a trusted source
echo "<pre>That request didn't look correct.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
2. 源码分析
Medium 等级首先通过:
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false )
检查请求头中的 Referer 是否包含当前服务器名称。
其中:
$_SERVER['HTTP_REFERER']表示请求来源地址。
而:$_SERVER['SERVER_NAME']表示当前服务器名称。在本地 DVWA 环境中为:localhost stripos() 用于进行不区分大小写的字符串查找,因此只要 Referer 中包含localhost,就会通过来源检查。这与前面黑盒测试中将Referer简化为:http://localhost仍然能够修改密码的现象一致。
通过来源检查后,程序获取新密码和确认密码:
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
并判断两次输入是否一致:
if( $pass_new == $pass_conf )
随后对密码进行数据库转义并使用 md5() 计算哈希值:
$pass_new = mysqli_real_escape_string(…);
$pass_new = md5( $pass_new );
最后更新当前用户密码:
$insert = "UPDATE `users` SET password = '$pass_new'
WHERE user = '" . dvwaCurrentUser() . "';";
Medium相比Low增加了Referer来源校验,但这种方式仅检查来源字符串中是否包含服务器名称,并没有使用随机的CSRF Token验证请求真实性,因此仍然存在绕过可能。
因此,该漏洞的主要原因是:程序仅依赖可绕过的Referer来源检查进行CSRF防护,没有使用不可预测的CSRF Token对敏感操作进行有效验证。
3. 修复方案
1)为密码修改等敏感请求加入随机且不可预测的CSRF Token。
2)在服务端严格验证CSRF Token,并与当前用户会话绑定。
3)不应将Referer作为唯一的CSRF防护手段。
4)密码修改等状态变更操作应使用POST请求,而不是通过GET参数完成。
5)对重要账户操作增加当前密码验证或二次身份认证。
6)为Session Cookie 配置合理的SameSite 属性,降低跨站请求携带 Cookie 的风险。
7)使用安全的密码哈希算法保存密码,避免继续使用 MD5。
8)对异常密码修改请求和来源异常行为进行日志记录与安全监控。
六 Low与Medium差异
| 请求方式 | GET | GET |
| 参数获取 | password_new、password_conf | password_new、password_conf |
| 密码一致性校验 | 有 | 有 |
| 请求来源校验 | 无 | 有 |
| 来源校验方式 | 无 | 检查HTTP_REFERER中是否包含SERVER_NAME |
| 校验函数 | 无 | stripos() |
| 是否使用CSRF Token | 否 | 否 |
| 是否依赖Referer | 否 | 是 |
| Low难度直接构造URL | 可直接利用 | 通常会被来源校验拦截 |
| Referer绕过 | 不需要 | 可通过构造包含服务器名称的Referer绕过 |
| 密码处理 | mysqli_real_escape_string() + md5() | mysqli_real_escape_string() + md5() |
| 数据库更新方式 | 直接执行UPDATE修改当前用户密码 | 通过来源校验后执行相同UPDATE操作 |
| CSRF漏洞 | 存在 | 仍然存在 |
| 主要防护措施 | 无明显CSRF防护 | 增加Referer来源检查 |
| 主要安全问题 | 未验证请求真实性 | 仅依赖可绕过的Referer检查,未使用CSRF Token |
| 根本修复方式 | 使用CSRF Token等可靠请求验证机制 | 使用CSRF Token等可靠请求验证机制,避免将Referer作为唯一防护手段 |




