CSRF

跨站请求伪造(Cross-Site Request Forgery,CSRF),攻击者通过伪装来自某个网站受信任用户,对该网站发送恶意请求。

XSS 与 CSRF 的区别:

  • XSS:
    攻击者发现XSS漏洞 —> 构造代码 —> 发送给受害人 —> 受害人打开 —> 攻击者获取受害人的 cookie —> 完成攻击
  • CSRF:
    攻击者发现CSRF漏洞 —> 构造代码 —> 发送给受害人 —> 受害人打开 —> 受害人执行代码 —> 完成攻击

可以发现,与 XSS 相比,CSRF 在受害人执行代码后攻击就已完成。

举个例子

GET请求

假设某银行网站 xbank 的转账是采用 GET 方式进行操作的,如:

1
http://www.xbank.com/transfer.php?toUserId=88&Money=1000

给 ID 为 88 的账户转账 1000 元。

攻击者构造了一个恶意页面,如:

1
<img src="http://www.xbank.com/transfer.php?toUserId=88&Money=1000">

受害者打开了这个界面,浏览器访问图片的url,会携带 xbank 的 cookie,如果该受害者的浏览器中 xbank 的 Cookie 或 Session 还没有过期,xbank 就会认为是受害者主动发送的请求,那么就成功转账了。

POST请求

xbank 改为了用 POST 提交表单进行转账操作:

1
2
3
4
5
<form action="./transfer.php" method="POST">
<p>ToUserId <input type="text" name="ToUserId"></p>
<p>Money <input type="text" name="Money"></p>
<p><input type="submit" name="Submit"></p>
</form>

恶意攻击者根据转账表单进行伪造了一份一模一样的转账表单,并且嵌入到 iframe中:

index.html:

1
2
3
4
5
6
7
<h1>Waiting...</h1>
<script type="text/javascript">
function attack() {
window.frames[0].document.forms[0].submit();
}
</script>
<iframe style="display: none;" src="./csrf.html" onload="attack()"></iframe>

csrf.html:

1
2
3
4
5
<form action="http://www.xbank.com/transfer.php" method="POST">
<input type="hidden" name="toUserId" value="88">
<input type="hidden" name="Money" value="1000">
<input type="hidden" name="Submit" value="submit">
</form>

成功转账。

JSON 格式

这次 xbank 一个端口是要提交 JSON 格式的数据:

1
2
3
4
5
6
POST /check.php HTTP/1.1
Host: www.xbank.com
Content-Type: application/json; charset=utf-8
Content-Length: 45

{"Sno":"22900001","Sname":"zzk","Sgrade":"4"}

仍然可以构造表单进行攻击:

1
2
3
4
<form action="http://www.xbank.com/check.php" method="POST" enctype="text/plain">  
<input name='{"Sno":"22900001","Sname":"zzk","Sgrade":"4", "padding":"' value='padding"}'type='hidden'>
<input type=submit>
</form>

这里数据编码为 text/plain,不对 JSON 中的特殊字符编码。注意 form 标签的 enctype 只能设为:application/x-www-form-urlencoded、multipart/form-data、text/plain 三种。因此如果服务器如果检查 Content-Type 必须为 application/json,那就只能用XMLHttpRequest 了:

1
2
3
4
5
6
7
8
<script>
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://www.xbank.com/check.php", true);
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
xhr.withCredentials = true;
var s = {"Sno": "22900001", "Sname":"zzk", "Sgrade":"4"};
xhr.send(JSON.stringify(s));
</script>

注意这里将 withCredentials 设为 true,这样才会一并发送 cookie。但使用 XMLHttpRequest 又会牵扯到一个问题,那就是 CORS。增添 Content-Type 头部后,这是一个非简单请求,浏览器会发送一个 OPTIONS 的预检,服务端很有可能不会响应这个请求,浏览器也就不会发送 POST 请求了。即使是一个简单请求,如果服务器检查 Orgin 头部,那就凉了。

防御

  1. 使用验证码。只要涉及到数据交互就先进行验证码验证,可以完全解决 CSRF。但是用户体验极差,慎重考虑。
  2. 验证 HTTP Referer 字段。但不是很安全,可以绕过。
  3. 为每个表单添加令牌 Token 并验证。强烈推荐。
  4. 对于特殊的 Content-Type 进行校验,并检查 Orign 头部。

令牌(Token)

服务端为每一个表单生成一个随机字符串,并在服务端验证这个 Token,如果请求中没有 Token 或者 Token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。由于这个 Token 是随机不可预测,因此恶意攻击者就不能够伪造这个表单进行 CSRF 攻击了。

目前有两种方式来存储令牌:SESSION 和 COOKIE。

利用 SESSION

  1. 后端生成随机字符串 Token,储存在 Session 中。
  2. 每当有表单时,从 Session 中取出 Token,放入表单中,并隐藏。用户提交表单一并将 Token 提交。
  3. 服务端先验证 $_POST['token'] === $_SESSION['token'],再执行其他逻辑。

一个简单的利用 SESSION 防止 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
36
37
<?php
function getTokenValue() {
return md5(uniqid(rand(), true).time());
}
function getToken($tokenName) {
session_start();
if (!isset($_SESSION[$tokenName])) {
$_SESSION[$tokenName] = getTokenValue();
}
}
getToken('login_token');
?>

<form action="<?php echo basename(__FILE__);?>" method="POST">
<p>username <input type="text" name="username"></p>
<p>password <input type="password" name="password"></p>
<p><input type="hidden" name="login_token" value="<?php echo $_SESSION['login_token'];?>"></p>
<p><input type="submit" name="submit"></p>
</form>

<?php
if (isset($_POST['submit'])) {
$check = ($_POST['login_token'] === $_SESSION['login_token'])? true: false;
unset($_SESSION['login_token']);
if ($check) {
if ($_POST['username'] === 'admin' && $_POST['password'] === 'admin') {
echo "<script>confirm('Welcome, admin!', window.location.href='login.php')</script>";
}
else {
echo "<script>confirm('Username or password is wrong!', window.location.href='index.php')</script>";
}
}
else {
echo "<script>confirm('Token not right!', window.location.href='index.php')</script>";
}
}
?>

利用 Session 防御 CSRF,很难找出其破绽。但 Session 有两个致命弱点:

  1. 所有用户,不论是否会提交表单,都将生成一个 Session,这将是很大的资源浪费,对服务器的要求很高。
  2. 除了 php 的很多开发语言中,Session 是可选项,很多网站根本没有 Server Session。开发框架不能强迫开发者使用 Session,所以在设计防御机制的时候也不会使用 Session。

所以,像 Django 之类的 python 框架,会选择基于 Cookie 的 CSRF 防御方式。

与 Session 唯一的不同,只是将 Token 放入 Cookie中,然后每次验证后将之销毁。网上有文章说要生成 Token 和 Token 的散列,服务端再验证,这是完全没必要的。因为仔细思考一下,就会发现,攻击者无法轻易修改用户在目标网页上的 Cookie。

但如果可以写入 Cookie,也会使这种防御手法失效:

  1. 某些低级网页可以直接写入 Cookie
  2. 利用XSS漏洞写入 Cookie
  3. 利用CRLF漏洞注入 Cookie
  4. 利用畸形字符使后端解析 Cookie 出错,注入 Cookie

XSS + CSRF

但如果网页还存在 XSS 漏洞,那么 Token 有可能会被窃取,甚至 Cookie 都会被盗,那么还用什么 CSRF。

可以看 RCTF-2015 的 xss 这道题:
http://www.hackdig.com/11/hack-28667.htm

参考