CSRF跨站请求伪造

0x00 CSRF跨站请求伪造

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装来自受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行和难以防范,所以被认为比XSS更具危险性。

0x01 原理

用户登陆网站A,并且生成本地cookie,在没有退出的情况下点击了攻击者构造的危险链接进入了网站B,而网站B是个危险请求,用户以自己的身份发送了危险请求。

0x02 CSRF攻击实例

CSRF 攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在并未授权的情况下执行在权限保护之下的操作。比如说,受害者 Bob 在银行有一笔存款,通过对银行的网站发送请求 http://bank.example/withdraw?account=bob&amount=1000000&for=bob2 可以使 Bob 把 1000000 的存款转到 bob2 的账号下。通常情况下,该请求发送到网站后,服务器会先验证该请求是否来自一个合法的 session,并且该 session 的用户 Bob 已经成功登陆。黑客 Mallory 自己在该银行也有账户,他知道上文中的 URL 可以把钱进行转帐操作。Mallory 可以自己发送一个请求给银行:http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory 。但是这个请求来自 Mallory 而非 Bob,他不能通过安全认证,因此该请求不会起作用。这时,Mallory 想到使用 CSRF 的攻击方式,他先自己做一个网站,在网站中放入如下代码:

1
src="http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory"

并且通过广告等诱使 Bob 来访问他的网站。当 Bob 访问该网站时,上述 url 就会从 Bob 的浏览器发向银行,而这个请求会附带 Bob 浏览器中的 cookie 一起发向银行服务器。大多数情况下,该请求会失败,因为他要求 Bob 的认证信息。但是,如果 Bob 当时恰巧刚访问他的银行后不久,他的浏览器与银行网站之间的 session 尚未过期,浏览器的 cookie 之中含有 Bob 的认证信息。这时,悲剧发生了,这个 url 请求就会得到响应,钱将从 Bob 的账号转移到 Mallory 的账号,而 Bob 当时毫不知情。等以后 Bob 发现账户钱少了,即使他去银行查询日志,他也只能发现确实有一个来自于他本人的合法请求转移了资金,没有任何被攻击的痕迹。而 Mallory 则可以拿到钱后逍遥法外。

0x03 CSRF环境搭建

首先我们写一个登陆之后能修改密码的系统,账号密码等信息存放在数据库中,数据库中假设有如下几个用户的信息:

然后在wamp环境中www/csrf路径中创建:

  • login.html
  • login.php
  • welcome.php
  • exit.php

各部分代码如下

登陆界面login.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>login</title>
</head>
<body>
<form action="login.php" method="post">
username:<input type="text" name="username" /><br />
password:<input type="password" name="password" /><br />
<input type="submit" value="登录" />
</form>
</body>
</html>

登陆界面代码只有简单的一个表单提交账号密码给后台。

登陆后台login.php:

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
49
50
51
52
53
54
55
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>login.....</title>
</head>
<body>
<?php
session_start();
$username=$_REQUEST["username"];
$password=$_REQUEST["password"];
$con=mysql_connect("localhost","root","123456");
if(!$con){
die("database connect error!");
}
mysql_select_db("test",$con);
$dbusername=null;
$dbpassword=null;
$result=mysql_query("select * from user where username='".$username."';");
while($row=mysql_fetch_array($result)){
$dbusername=$row["username"];
$dbpassword=$row["password"];
}
if(is_null($dbusername)){
?>
<script>
alert("username is not exist!");
window.location.href="login.html";
</script>
<?php
}
else{
if($dbpassword!=$password){
?>
<script>
alert("password error!");
window.location.href="login.html";
</script>
<?php
}
else{
$_SESSION["username"]=$username;
$_SESSION["code"]=mt_rand(0,100000);
?>
<script>
window.location.href="welcome.php";
</script>
<?php
}
}
mysql_close($con);
?>
</body>
</html>

后台拿到收到的账号密码,然后连接数据库登陆,与数据库中账号密码匹配成功则生成session并跳转到welcome.php,匹配失败就返回login.html。

登陆成功界面welcome.php

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
49
50
51
52
<html>
<head>
<meta charset="UTF-8">
<title>welcome</title>
</head>
<body>
<?php
session_start ();
if (isset ( $_SESSION ["code"] )) {//判断code存不存在,如果不存在,说明异常登录
?>
welcome <?php
echo "${_SESSION["username"]}";//显示登录用户名
?><br>
your ip:<?php
echo "${_SERVER['REMOTE_ADDR']}";//显示ip
?>
<br>
<a href="exit.php">exit</a>
<?php
} else {//code不存在,调用exit.php 退出登录
?>
<script type="text/javascript">
alert("exit");
window.location.href="exit.php";
</script>
<?php
}
?>
<br>
<!-- <input type="button" onclick="window.location.href='password.html'" value="change password"> -->
<form action="" method="get">
newpassword: <input type="text" name="password">
<input type="submit">
</form>
<hr>
<?php
$con=mysqli_connect("localhost","root","123456","test");
if(!$con){
die("database connect error!");
}
if($_GET['password']){
$n_password = $_GET['password'];
$username="${_SESSION["username"]}";
#print "update user set password= '".$n_password."' where username= '".$username."';";
mysqli_query($con,"update user set password= '".$n_password."' where username= '".$username."';");
}
?>
</body>
</html>

登陆成功界面显示用户名和ip地址,然后有一个修改密码的输入框,如果我们在这里提交新密码,就会连接mysql数据库修改密码,点击exit可以退出登陆。

退出处理exit.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>log out</title>
</head>
<body>
<?php
session_start();
session_destroy();
?>
<script>
window.location.href="login.html";
</script>
</body>
</html>

exit.php将session销毁,然后返回login.html。

上述四个文件编写完毕,基本的环境就完成了,下来我们看如何进行csrf攻击。

0x04 csrf实现

首先登陆界面如下:

lily用自己账号登陆了这个界面:

然后修改了密码,发现密码修改时的url请求为:

然后lily想,如果构造一个url请求发给别人,别人正好处于没有退出登陆的情况下点击了这个url,会不会被修改密码。

ps:此时lily密码为233333,jack密码为qwe123。

所以lily就构造url:http://127.0.0.1/csrf/welcome.php?password=123456 给已经登陆的jack
,然后jack点击了lily发来的网址,jack的密码变成了123456:

这个时候lily想,直接发网址太容易暴露了,我构造一个钓鱼网页,来欺骗jack点,所以写了一个hack.html(这个可以写在任何网络中,诱导jack点击):

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>hack</title>
</head>
<body>
<a href="http://127.0.0.1/csrf/welcome.php?password=123456">233333</a>
</body>
</html>

这个时候,只要处于未退出状态的人点击了这个链接,密码就会被改成123456

至此,一次完美的csrf攻击就完成了。

0x05 应对

  • 添加验证码
    如果提交请求的界面需要有验证码,即使存在csrf漏洞,攻击者也无法预测下一个验证码是什么,就成功的避免了csrf攻击。

  • 验证referer
    HTTP Referer是header的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器是从哪个页面链接过来的,而且它无法被javascript所更改,所以服务器可以通过Referer来判断是用户的正常操作还是被csrf攻击后的操作。

  • token
    CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

  • 设置跨域权限
    设置跨域权限使用户的某些操作不接受其他域的提交,就可以避免csrf。

0x06 总结

CSRF漏洞虽然原理不难理解,攻击也不难实现,但是搭漏洞环境时一句一句的写代码出了很多错误花了很多时间,不过总的来说受益良多,提升了自己的编程能力。