DVWA-CSRF(跨站请求伪造)

csrf(Cross-site request forgery)跨站请求伪造:攻击者诱导用户访问第三方网站,在第三方网站中携带恶意代码,向被攻击者发送请求
原理可以这样来说

用户在访问了一个后台管理网站后,例如用户更改密码,但是更改密码需要登录认证的,用户在登录后,浏览器会保存认证一段时间,叫做cookie,保证下次访问不在弹出登录框
这个时候攻击者恶意伪造一个网站,这个网站的功能是更改用户的密码,但是攻击者没有用户的cookie,是无法更改密码的,攻击者通过诱导用户点击恶意网页,由于用户浏览器带有后台管理网站的cookie,恶意网页这是请求更改密码就会执行成功,导致用户什么都不知道,密码就被修改了

LOW

审计源码

<?php
// 判断有没有接收到Change
if( isset( $_GET[ 'Change' ] ) ) {
    // 获取输入的两次密码
    $pass_new  = $_GET[ 'password_new' ];
    $pass_conf = $_GET[ 'password_conf' ];

    // 两次输入密码是否相同
    if( $pass_new == $pass_conf ) {
        // 相同
        $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 );

        // 更新数据库密码
        $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>' );

        // 密码更改成功
        echo "<pre>Password Changed.</pre>";
    }
    else {
        // 两次密码输入不相同,密码更改失败
        echo "<pre>Passwords did not match.</pre>";
    }

    ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?> 

通过代码可以看出,Low没有做任何过滤,并且是一个更改密码操作
首先更改一次密码,获取更改密码的url

http://172.16.1.103/dvwa/vulnerabilities/csrf/?password_new=Admin123&password_conf=Admin123&Change=Change#
这里可以看到将密码更改为了Admin123
再次打开一个firefox,将链接粘贴的url中
http://172.16.1.103/dvwa/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change#

退出DVWA登录,使用123456登录靶场


可以看到,密码已经被更改成功

构造恶意网页

kali另一台机器上写入恶意网页

<img src="http://172.16.1.103/dvwa/vulnerabilities/csrf/?password_new=Admin123&password_conf=Admin123&Change=Change#" style="display:none"/>
<h1>Test HTML</h1>


使用登录DVWA靶场的firefox访问恶意网页

在访问后,回到DVWA退出登录,查看密码是否更改成功

可以看到,只有使用Admin123才可以登录成功

Medium

审计源码

<?php

if( isset( $_GET[ 'Change' ] ) ) {
    // 检查 http_referer 是否是 访问服务器的访问名称
    // stripos() 查找字符串首次出现的位置
    // $_SERVER变量 获取 http_referer 的值,server_name获取服务端的名称例如:www.baidu.com
    if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
        // 获取两次输入的密码
        $pass_new  = $_GET[ 'password_new' ];
        $pass_conf = $_GET[ 'password_conf' ];

        // 判断两次密码是否相同
        if( $pass_new == $pass_conf ) {
            // 相同
            $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 );

            // 更改密码
            $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>' );

            // 更改密码成功
            echo "<pre>Password Changed.</pre>";
        }
        else {
            // 更改密码失败
            echo "<pre>Passwords did not match.</pre>";
        }
    }
    else {
        // rerfer值不正确
        echo "<pre>That request didn't look correct.</pre>";
    }

    ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?> 

通过观察,使用
stripos()查找一个字符串时第一次出现的位置,
HTTP_REFERER是获取HTTP请求头中的REferer值,用来告诉服务端来自哪里的请求
SERVER_NAME访问目标的主机名
总体来说就是检查header中的Referer中是否含有服务端主机名
修改密码抓包查看数据包结构

可以看到,Referer中默认就是来自http://172.16.1.103/dvwa/vulnerabilities/csrf/
所以密码可以修改成功,这里密码修改,只要是Referer中含有服务器主机名就可以成功
我们直接修改为Referer:172.16.1.103


可以看到密码是可以修改成功的
但是,真正的CSRF是,诱导点击,访问恶意网页进行修改,我们这里只是通过burpsuite更改密码成功

恶意文件修改密码

这里我已失败告终了,学术不精,到底该如何在点击访问时更改Referer的值呢?

High

审计源码

<?php

// 定义改变默认为 false
$change = false;
// 定义请求类型默认为 html
$request_type = "html";
// 定义返回信息默认为 返回失败
$return_message = "Request Failed";

// 判断 change 是否为true
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) {
    // 检查 user_token
    checkToken( $token, $_SESSION[ 'session_token' ], 'index.php' );

    // 判断输入的两次密码是否相同
    if( $pass_new == $pass_conf ) {
        // 使用md5加密输入的新密码
        $pass_new = mysqli_real_escape_string ($GLOBALS["___mysqli_ston"], $pass_new);
        $pass_new = md5( $pass_new );

        // 更新用户的密码
        $insert = "UPDATE `users` SET password = '" . $pass_new . "' WHERE user = '" . dvwaCurrentUser() . "';";
        $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>";
    }
}

// 生成token
generateSessionToken();

?>

通过观察这里引入了一个user_token,防止点击修改密码,但是可以通过抓包的方式修改user_token
更改密码为Admin123抓包查看

这里的user_token在每一次更改密码服务器都会刷新返回一个;
通过同等级XSS (Reflected)漏洞获取user_token,然后通过获取的user_token更改密码,
<iframe src="../csrf" onload=alert(frames[0].document.getElementsByName('user_token')[0].value) />

这样确实可以获取user_token.
上述这个方法其实和在CSRF页面查看源代码获取user_token的方法类似

上述两个方法确实都获取到了user_token,但是并没有点击就可以修改用户密码,还是需要通过抓包更改密码;
所以如果要实现真正的CSRF,需要写一个javascript诱导用户点击,获取其user_token,然后通过获取的user_token更改密码
到底该怎么实现?由于我没有学过javascript,所以如何获取就不得而知,后续再来补坑吧。

Impossible

审计源码


<?php

if( isset( $_GET[ 'Change' ] ) ) {
    // 获取并检查 user_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' ];

    // 去除获取当前密码中的 /
    $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)) ? "" : ""));
    // 使用 md5 加密获取的当前密码
    $pass_curr = md5( $pass_curr );

    // 检查当前密码是否正确
    $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();

    // 判断输入两次新密码是否相同 和 输入当前密码是否正确
    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)) ? "" : ""));
        // 使用 md5 加密新密码
        $pass_new = md5( $pass_new );

        // 更新数据库中的密码为新密码
        $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();

        // 返回更改密码成功
        echo "<pre>Password Changed.</pre>";
    }
    else {
        // 否则返回错误
        echo "<pre>Passwords did not match or current password incorrect.</pre>";
    }
}

// 生成 user_token 认证
generateSessionToken();

?>

可以看到这里既引入了user_token认证,也去除了密码中的/,最重要的是需要输入当前的密码,才可以更改密码。
唯一不好的就是更改密码为GET请求方式,换为POST更好,可能是作者想方便测试吧

posted @ 2022-05-13 15:16  Junglezt  阅读(140)  评论(0编辑  收藏  举报