漏洞引发的原因是 Firebase(CVE-2024-45489)。
我们从 Arc 的主页开始,这也是我第一次听说它时打开的页面。我下载了安装包并开始分析,第一个发现是 Arc 需要用户登录才能使用。为什么他们需要账户?
介绍 Arc 的云功能
我启动了 mitmproxy
并注册了一个账户,发现他们使用 Firebase 进行身份验证,但没有其他请求。难道他们真的只用 Firebase 来做身份验证?
在探索了一会儿后,我发现 Arc 有一个名为Easels的功能,它类似于白板,可以与他人共享,并让他们在 Web 上查看。然而,当我点击“分享”按钮时,我的 mitmproxy
并没有捕获到任何请求——那么这里到底发生了什么?
攻击基于 Objective-C 的 Firebase 应用
根据我以往对 iOS 应用的渗透测试经验,我立刻有了一个猜测:这可能与 Firestore 相关。
Firestore 是一种 Database-as-a-Backend(数据库即后端)服务,允许开发者无需编写后端逻辑,而是通过数据库安全规则,直接让用户访问数据库。这种机制已经导致许多服务存在不安全或安全规则不足的问题。
此外,在 Firebase 的 Swift SDK 中,Firestore 往往不会遵循系统代理设置。因此,基于我的猜测,我编写了一个 Frida 脚本来捕获相关的 API 调用。
var documentWithPath =
ObjC.classes.FIRCollectionReference["- documentWithPath:"];
var queryWhereFieldIsEqualTo =
ObjC.classes.FIRQuery["- queryWhereField:isEqualTo:"];
var collectionWithPath = ObjC.classes.FIRFirestore["- collectionWithPath:"];
function getFullPath(obj) {
if (obj.path && typeof obj.path === "function") {
return obj.path().toString();
}
return obj.toString();
}
var queryStack = [];
function logQuery(query) {
var queryString = `firebase.${query.type}("${query.path}")`;
query.whereClauses.forEach((clause) => {
queryString += `.where("${clause.fieldName}", "==", "${clause.value}")`;
});
console.log(queryString);
}
Interceptor.attach(documentWithPath.implementation, {
onEnter: function (args) {
var parent = ObjC.Object(args[0]);
var docPath = ObjC.Object(args[2]).toString();
var fullPath = getFullPath(parent) + "/" + docPath;
var query = { type: "doc", path: fullPath, whereClauses: [] };
queryStack.push(query);
logQuery(query);
},
});
Interceptor.attach(collectionWithPath.implementation, {
onEnter: function (args) {
var collectionPath = ObjC.Object(args[2]).toString();
var query = { type: "collection", path: collectionPath, whereClauses: [] };
queryStack.push(query);
},
});
Interceptor.attach(queryWhereFieldIsEqualTo.implementation, {
onEnter: function (args) {
var fieldName = ObjC.Object(args[2]).toString();
var value = ObjC.Object(args[3]).toString();
if (queryStack.length > 0) {
var currentQuery = queryStack[queryStack.length - 1];
currentQuery.whereClauses.push({ fieldName: fieldName, value: value });
}
},
onLeave: function (retval) {},
});
var executionMethods = [
"- getDocuments",
"- addSnapshotListener:",
"- getDocument",
"- addDocumentSnapshotListener:",
"- getDocumentsWithCompletion:",
"- getDocumentWithCompletion:",
];
executionMethods.forEach(function (methodName) {
if (ObjC.classes.FIRQuery[methodName]) {
Interceptor.attach(ObjC.classes.FIRQuery[methodName].implementation, {
onEnter: function (args) {
if (queryStack.length > 0) {
var query = queryStack.pop();
logQuery(query);
}
},
});
}
});
function formatFirestoreData(data) {
if (data.isKindOfClass_(ObjC.classes.NSDictionary)) {
let result = {};
data.enumerateKeysAndObjectsUsingBlock_(
ObjC.implement(function (key, value) {
result[key.toString()] = value.toString();
})
);
return JSON.stringify(result);
}
return data.toString();
}
var documentMethods = [
{ name: "- updateData:completion:", type: "update" },
{ name: "- updateData:", type: "update" },
{ name: "- setData:completion:", type: "set" },
{ name: "- setData:", type: "set" },
];
documentMethods.forEach(function (method) {
if (ObjC.classes.FIRDocumentReference[method.name]) {
Interceptor.attach(
ObjC.classes.FIRDocumentReference[method.name].implementation,
{
onEnter: function (args) {
var docRef = ObjC.Object(args[0]);
var data = ObjC.Object(args[2]);
var fullPath = getFullPath(docRef);
var formattedData = formatFirestoreData(data);
console.log(
`firebase.doc("${fullPath}").${method.type}(${formattedData})`
);
},
}
);
} else {
console.log("Warning: " + method.name + " not found");
}
});
虽然是个临时脚本,但它有效。于是,我在启动时加载了该脚本并运行 Arc,结果如下:
firebase.doc("preferences/UvMIUnuxJ2h0E47fmZPpHLisHn12");
firebase.doc(
"preferences/UvMIUnuxJ2h0E47fmZPpHLisHn12/stringValues/autoArchiveTimeThreshold"
);
firebase.doc("preferences/UvMIUnuxJ2h0E47fmZPpHLisHn12");
firebase.doc(
"preferences/UvMIUnuxJ2h0E47fmZPpHLisHn12/stringValues/autoArchiveLittleArcTimeThreshold"
);
firebase.doc("preferences/UvMIUnuxJ2h0E47fmZPpHLisHn12");
firebase.doc(
"preferences/UvMIUnuxJ2h0E47fmZPpHLisHn12/stringValues/autoArchiveTimeThresholdsPerProfile"
);
firebase.doc("users/UvMIUnuxJ2h0E47fmZPpHLisHn12");
firebase
.collection("user_referrals")
.where("inviter_id", "==", "UvMIUnuxJ2h0E47fmZPpHLisHn12");
firebase
.collection("boosts")
.where("creatorID", "==", "UvMIUnuxJ2h0E47fmZPpHLisHn12");
看起来 Arc 在 Firestore 中存储了一些偏好设置,以及一个基本的用户对象、推荐信息和 Boosts。
Arc Boosts 到底是什么?
Arc Boosts 允许用户自定义网站,比如屏蔽元素、更改字体、颜色,甚至使用自定义 CSS 和 JS。
你明白这意味着什么了吗? 于是,我手动登录到我的账户,使用我的测试 Firebase 账户页面,并执行了完全相同的查询来获取我的 Boosts:
酷,让我在 google.com 上创建一个简单的 Boost。
嘿!我们的 Boost 出现了,让我们尝试更改一些参数。
我发现它是通过 creatorID
进行查询的,我们无法查询不同的 creatorID
,只能查询自己的。但如果我们把自己的 Boost 更新为另一个用户的 ID 会发生什么?
于是,我用我的另一个账户尝试了这个方法,然后当我在另一台电脑(受害者设备)上访问 Google.com 时,得到了如下结果:
什么鬼?这真的有效?
快速回顾
creatorID
字段获取需要使用的 Boosts。creatorID
字段为任何用户的 ID。获取其他用户的 ID
用户推荐
user_referrals
表中。这意味着你只需要向某人索取他们的 Arc 邀请码,他们很可能会提供给你。已发布的 Boosts
boostSnapshots
(已发布的 Boosts)包含了创建者的用户 ID。用户 Easels
整合攻击链
creatorID
字段,将其改为目标用户的 ID。在特权页面上实现 RCE(远程代码执行)
settings
,它会在 chrome://settings
页面上执行,从而进一步提升权限。隐私问题
firebase
.collection("boosts")
.where("creatorID", "==", "UvMIUnuxJ2h0E47fmZPpHLisHn12")
.where("hostPattern", "==", "www.google.com");
hostPattern
代表你访问的网站,这实际上违反了 Arc 的隐私政策[2],因为该政策明确声明 Arc 不会追踪用户访问的网站。References
[1]
公开网站: https://arc.net/boosts[2]
Arc 的隐私政策: https://arc.net/privacy声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。
原文始发于微信公众号(白帽子左一):在无需访问网站的情况下获取浏览器的访问权限
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论