Bootstrap

【DVWA】CSRF详解

【DVWA】CSRF详解



一、low级别


1、测试流程

image-20210303170012898

如图,输入一次新密码和一次确认新密码就可更改密码,并没有索要旧密码进行验证。

用burp抓包看一下:

image-20210303170320927

发现是get请求,而且并没有token,所以明显存在CSRF漏洞。

伪造一个请求链接:

http://222.24.28.118/dvwa/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change

将password_new和password_conf参数的值改为123456,然后将此链接发送给攻击目标,如果目标在此网站处于登录状态并且点击此链接,则会将该用户的密码重置为123456

模拟用户点击链接:

在虚拟机中登录DVWA,并将安全等级设置为low,然后访问:

http://222.24.28.118/dvwa/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change

image-20210303171451671

如图所示,密码已被更改。

值得注意的是,用户必须用登录此网站的浏览器访问该链接,攻击才会成功。因为后端会校验cookie值,如果换浏览器请求数据包将无法获取cookie,后端的cookie会校验失败,从而跳转到登录页面攻击就会失效。看来CSRF攻击的关键就是利用受害者的cookie向服务器发送伪造请求。

但是,这样的攻击方式是十分简陋的,用户可以直接看到链接是一个更改密码的链接从而心有防备。

现实中,攻击者往往会先搭建一个站点,然后上传一个html文档,该文档中含有恶意的链接。让后将这个html文档的地址发送给用户,用户一旦点击将会自动加载恶意链接完成攻击。

image-20210303181038355image-20210303181010231

2、源码分析

<?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)) ? "" : ""));  //防SQL注入
        $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);
}

?> 

可以看到,后端仅仅检查change参数是否设置,以及password_new和password_conf是否相等做了判定,而并没有做任何防CSRF的措施。



二、medium级别


1、测试流程

按照上面的方法我们做一下测试,发现报错了:

image-20210303173032730

我们直接看一下源码

2、源码分析

<?php

if( isset( $_GET[ 'Change' ] ) ) {
    // Checks to see where the request came from
    if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) { //检测Referer中是否有host
        // 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(strs, str)函数:返回字符str在字符串strs中的位置

HTTP_REFERER表示数据包中的referer字段(表示数据包的来源链接),SERVER_NAME表示数据包中的host(要访问的主机地址),所以后端会检测看referer中是否有host,如果有才会通过。

绕过思路:直接将low级别制作的html文档名称改为[host].html,这样referer中就会存在host,用户一旦点击就会绕过检测攻击成功!

image-20210303184312319

三、high级别


### 1、测试流程

访问恶意链接,报错CSRF token is incorrect.

看来是用了token防范

image-20210303184934522

2、源码分析

<?php

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

    // 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);
}

// Generate Anti-CSRF token
generateSessionToken();

?> 

high级别中加入了Anti-csrf token机制,由checkToken函数来实现,用户每次访问更改密码页面时,服务器会返回一个随机的token,之后每次向服务器发起请求之前需要获取服务器返回的user_token,利用user_token绕过验证。

可以使用burp的一个插件CSRF Token Tracker:

image-20210303215241378

设置host和name

image-20210303215406694

然后抓包重放就行


四、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();

?> 

impossible级别同样采用了Anti-CSRF token机制,有效的防御了CSRF攻击,而且采用PDO预处理和参数化技术预防了sql注入。


五、防范建议


  • 验证请求的Referer值,如果Referer是以自己的网站开头的域名,则说明该请求来自网站自己,是合法的,如果Referer是其他网站域名或空白,就有可能是CSRF攻击,那么服务器应拒绝该请求,但此方法存在被绕过的可能。
  • 设置token机制,使伪造请求变得困难。
;