一步一步学习DVWA--CSRF跨站请求伪造攻击(第五期)

小朋友们,今天我们开始学习CSRF跨站请求伪造攻击。

CSRFCross-site request forgery的首字母拼写,中文一般称为跨站请求伪造,是指利用受害者尚未失效的身份认证信息(cookie、会话等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作(如转账、改密等)。早在 2007 年就曾被列为互联网 20 大安全隐患之一,即便是大名鼎鼎的 Gmail, 2007 年底也因 CSRF 漏洞而被黑客攻击,对 Gmail 的用户造成了巨大的损失。

CSRFXSS的区别

上面的案例中,很多人会疑惑这跟XSS攻击有什么不同吗?关于这点,我们从攻击方式和应对手段上分别来说说。

CSRF XSS 可以理解为两个不同维度上的分类。XSS 是实现 CSRF 的其中一种方式。通常习惯把通过 XSS 来实现的 CSRF 称为 XSRF

CSRF 攻击原理举例

CSRF 攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在并未授权的情况下执行在权限保护之下的操作。比如说,受害者 张三 在银行有一笔存款,通过对银行的网站发送请求 http://bank.example/withdraw?account=张三&amount=1000000&for=张三2 可以使 张三 1000000 的存款转到 张三2 的账号下。通常情况下,该请求发送到网站后,服务器会先验证该请求是否来自一个合法的 session,并且该 session 的用户 张三 是否已经成功登录。黑客 李四 自己在该银行也有账户,他知道上文中的 URL 可以把钱进行转帐操作。李四 可以自己发送一个请求给银行:http://bank.example/withdraw?account=张三&amount=1000000&for=李四。但是这个请求来自 李四 而非 张三,他不能通过安全认证,因此该请求不会起作用。这时,李四 想到使用 CSRF 的攻击方式,他先自己做一个网站,在网站中放入如下代码: src=”http://bank.example/withdraw?account=张三&amount=1000000&for=李四,并且通过广告等诱使 张三 来访问他的网站。当 张三 访问该网站时,上述 url 就会从 张三 的浏览器发向银行,而这个请求会附带 张三 浏览器中的 cookie 一起发向银行服务器。大多数情况下,该请求会失败,因为他要求 张三 的认证信息。但是,如果 张三 当时恰巧刚访问他的银行后不久,他的浏览器与银行网站之间的 session 尚未过期,浏览器的 cookie 之中含有 张三 的认证信息。这时,悲剧发生了,这个 url 请求就会得到响应,钱将从 张三 的账号转移到 李四 的账号,而 张三 当时毫不知情。等以后 张三 发现账户钱少了,即使他去银行查询日志,他也只能发现确实有一个来自于他本人的合法请求转移了资金,没有任何被攻击的痕迹。而 李四 则可以拿到钱后逍遥法外。

 

 

为了加深理解,请看下面这张图(来自于网络)。

https://ask.qcloudimg.com/http-save/yehe-6005537/a0ujqpogey.jpeg?imageView2/2/w/1620

CSRFXSS最大的区别就在于,CSRF并没有盗取cookie而是直接利用。

2013年发布的OWASP Top 10中,CSRF排名第A8,但是没有进入2017年最新版本的OWASP Top 10中,原因是很多平台、开发框架等融入了CSRF防御方法,比较有效的防止了该安全漏洞,从统计来看,只有5%的应用程序受到了该漏洞威胁,比例减少,被挤出了Top 10

1、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); 


?> 

可以看到,服务器收到修改密码的请求后,会检查参数password_newpassword_conf是否相同,如果相同,就会修改密码,并没有任何的防CSRF机制。当然服务器对请求的发送者是做了身份验证的,是检查的cookie,只是这里的演示代码体现不出来。注意,不能跨浏览器演示这个攻击,有的浏览器已经设置了防止简单的CSRF攻击,也可能无法演示出来。

构造链接如下:

http://192.168.92.129/DVWA/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#

单击上面链接,密码就会被改成123456,但是这种攻击显得有些拙劣,链接一眼就能看出来是改密码的,而且受害者点了链接之后看到这个页面就会知道自己的密码被篡改了。

上面的攻击对于不太懂电脑的人来说,可能能够成功,但是对于稍微懂些电脑的人来说,这个攻击链接就非常明显。需要对链接做一些处理。

使用短链接来隐藏URL,点击短链接会自动重定向到真实网站。可以使用百度短网址(https://dwz.cn/)进行处理,例如:

但是对于我们自己搭建的服务器,由于没有域名而没有办法生成短域名的。虽然使用短链接隐藏了原来真实的url,但是被攻击者最终还是会看到密码修改成功的页面,所以这种方式也很容易漏出破绽。

构造攻击页面

在发起攻击之前,这种方法实现要在公网上传一个攻击页面,诱骗被攻击者访问,真正能够在受害者不知情的情况下完成CSRF攻击。租不起公网服务器,我们就在本机做一个页面getf.html处理吧,下面代码:

Getf.html页面代码:

<img src="http://192.168.92.129/dvwa/vulnerabilities/csrf/?password_new=1234&password_conf=1234&Change=Change#" border="0" style="display:none;"/>

<h1>404<h1>

<h2>file not found.<h2>

把上面链接放在公网某个地址,当用户点击这个链接后,则出现下面页面,客户可能以为访问了一个失效页面。

退出DVWA ,重新登录,发现密码已经被修改。

2、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); 


?>

我们首先分析一下这个函数stripos$_SERVER'HTTP_REFERER' ]

查找百度

$_SERVER['HTTP_REFERER'] 完全来源于浏览器。并不是所有的用户代理(浏览器)都会设置这个变量,而且有的还可以手工修改 HTTP_REFERER。因此,$_SERVER['HTTP_REFERER'] 不总是真实正确的。

通常下面的一些方式,$_SERVER['HTTP_REFERER'] 会无效:

  1. 直接输入网址访问该网页。
  2. Javascript 打开的网址。
  3. Javascript 重定向(window.location)网址。
  4. 使用 meta refresh 重定向的网址。
  5. 使用 PHP header 重定向的网址。
  6. flash 中的链接。
  7. 浏览器未加设置或被用户修改。

所以一般来说,只有通过 <a></a> 超链接以及 POST 或 GET 表单访问的页面,$_SERVER['HTTP_REFERER'] 才有效。

由于 $_SERVER['HTTP_REFERER'] 对 POST 表单访问也是有效的,因此在表单数据处理页面一定程度上可以通过校验 $_SERVER['HTTP_REFERER'] 来防止表单数据的恶意提交。但该方法并不能保证表单数据的绝对正确,即对表单数据的真实性检测并不能完全依赖于 $_SERVER['HTTP_REFERER'] 。

可以看到,Medium级别的代码检查了保留变量 HTTP_REFERER(http包头的Referer参数的值,表示来源地址)中是否包含SERVER_NAME(http包头的Host参数,及要访问的主机名,这里是127.0.0.1),希望通过这种机制抵御CSRF攻击。(防止了从不知名页面的跳转)

这是我们通过网站修改密码的Fiddler抓包,如下:

 

如果使用getf.html申请修改,抓取的报文是:

 

对比两个抓包报文,最大的不同在于Referer字段,这也正是服务器验证的字段。再看过滤规则,是对http报头的Referer参数的值进行是否包含主机名进行校验,主机名是字段Host的值,即192.168.92.129,仔细看看我们使用链接抓取的报文Referer字段,把文件名称修改为上述主机名,能绕过规则检查。于是修改getf.html文件名称为192.168.92.129.html。访问实验,抓取报文看提交成功。密码已经被修改。

 

抓取返回报文:

 

 

 

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

?> 

分析:

High级别的代码加入了反CSRF的验证token机制,每次用户修改密码时,服务器会返回随机的token,向服务器再次发起请求时,需要提交token参数,而服务器收到token后,会首先检查token,只有token正确,才会继续处理客户端的请求。

首先我们先抓取一个正常修改密码的报文

 

可以到请求里面包括了token值,这里发出的token一般是在上次请求时返回的报文中,这次请求返回的报文中查找token,可以查到

 

把该token放在参数

http://192.168.92.129/DVWA/vulnerabilities/csrf/?password_new=1234567&password_conf=1234567&Change=Change&user_token=20b5ef00ffec3cfe83347a43d4c80c6b

使用上面请求,则密码进行正确修改。

4、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_currPDO::PARAM_STR ); 
    
$data->execute(); 

    
// Do both new passwords match and does the current password match the user? 
    
if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == ) ) { 
        
// 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_newPDO::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(); 

?> 

可以看到,Impossible级别的代码利用PDO技术防御SQL注入,至于防护CSRF,则要求用户输入原始密码(简单粗暴),前提是协议传输没有问题,攻击者在不知道原始密码的情况下,无论如何都无法进行CSRF攻击。

PHP 数据对象(PDO 扩展为PHP访问数据库定义了一个轻量级的一致接口。实现 PDO 接口的每个数据库驱动可以公开具体数据库的特性作为标准扩展功能。也就是采用了预处理的方式将运行语句与参数隔离:

 

(完)

--------------------------------------------------------------------------------------------------------------------------------------------------

关注安全  关注作者

 

发布了309 篇原创文章 · 获赞 31 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/manok/article/details/103552937