OAuth Account Takeover by hijacking custom schemes
通过劫持自定义协议接管 OAuth 账户
描述
该漏洞源于应用程序在 OAuth 身份验证期间在 redirect_uri 参数中使用自定义协议。
在典型的 OAuth 场景中,应保证 redirect_uri 属于向身份提供者(Google、Facebook、Github等)请求数据的客户端应用程序(由 client_id 标识)。使用自定义协议会破坏该前提,因为它可以被用户设备上的应用程序声明。
一个攻击场景示例是,恶意应用声明了某些 OAuth 客户端应用程序使用的自定义协议,并触发了对目标应用程序的 OAuth 身份验证流程,一旦用户成功登录并同意,他们将被重定向到带有 OAuth 流程生成的身份验证令牌的恶意应用,从而允许恶意应用接管其账户。
攻击者可以通过利用某些技术(如快速身份验证流程)或使用旨在跳过同意提示的 OAuth 参数(如果用户之前已同意)来绕过用户交互。
Kotlin
<activity android:exported="true" android:name="net.openid.appauth.RedirectUriReceiverActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:host="oauthredirect" android:scheme="mycustomscheme"/>
</intent-filter>
</activity>
Log.i(TAG, "Creating auth request for login hint: $loginHint")
val authRequestBuilder: AuthorizationRequest.Builder = Builder(
mAuthStateManager.getCurrent().getAuthorizationServiceConfiguration(),
mClientId.get(),
ResponseTypeValues.CODE,
"mycustomscheme://oauthredirect" // 具有自定义协议的重定向 URI
)
.setScope(mConfiguration.getScope())
if (!TextUtils.isEmpty(loginHint)) {
authRequestBuilder.setLoginHint(loginHint)
}
mAuthRequest.set(authRequestBuilder.build())
iOS
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
...
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>mycustomscheme</string>
</array>
</dict>
</array>
...
</dict>
</plist>
func doAuthWithAutoCodeExchange(configuration: OIDServiceConfiguration, clientID: String, clientSecret: String?) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
self.logMessage("Error accessing AppDelegate")
return
}
// 构建身份验证请求
let request = OIDAuthorizationRequest(configuration: configuration,
clientId: clientID,
clientSecret: clientSecret,
scopes: [OIDScopeOpenID, OIDScopeProfile],
redirectURL: "mycustomscheme://oauthredirect",
responseType: OIDResponseTypeCode,
additionalParameters: nil)
// 执行身份验证请求
logMessage("Initiating authorization request with scope: \(request.scope ?? "DEFAULT_SCOPE")")
appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in
if let authState = authState {
self.setAuthState(authState)
self.logMessage("Got authorization tokens. Access token: \(authState.lastTokenResponse?.accessToken ?? "DEFAULT_TOKEN")")
} else {
self.logMessage("Authorization error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
self.setAuthState(nil)
}
}
}
多平台
// android/build.gradle
android {
// ...
defaultConfig {
// ...
// 添加以下行
manifestPlaceholders = [auth0Domain: "oauthredirect", auth0Scheme: "mycustomscheme"]
}
// ...
}
final authorizationEndpoint =
Uri.parse('http://example.com/oauth2/authorization');
final tokenEndpoint = Uri.parse('http://example.com/oauth2/token');
final identifier = 'my client identifier';
final secret = 'my client secret';
// 具有自定义协议的重定向 URI
final redirectUrl = Uri.parse('mycustomscheme://oauthredirect');
final credentialsFile = File('~/.myapp/credentials.json');
Future<oauth2.Client> createClient() async {
var exists = await credentialsFile.exists();
if (exists) {
var credentials =
oauth2.Credentials.fromJson(await credentialsFile.readAsString());
return oauth2.Client(credentials, identifier: identifier, secret: secret);
}
var grant = oauth2.AuthorizationCodeGrant(
identifier, authorizationEndpoint, tokenEndpoint,
secret: secret);
var authorizationUrl = grant.getAuthorizationUrl(redirectUrl);
await redirect(authorizationUrl);
var responseUrl = await listen(redirectUrl);
return await grant.handleAuthorizationResponse(responseUrl.queryParameters);
}
建议
为了解决该漏洞,建议不要使用自定义协议来重定向身份验证令牌。
开发人员应考虑以下选项之一:
- 应用到应用集成,例如适用于 Android 的 Google Identity Services 和 Facebook Express Login
- Android's verifiable AppLinks
- iOS associated domains
Kotlin
您需要在后端托管格式如下的 /.well-known/assetlinks.json:
[
{
"relation": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "com.myapplication.android",
"sha256_cert_fingerprints": [
"APPLICATION_CERT_FINGERPRINT"
]
}
}
]
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- 如果用户点击使用 "http" 协议的共享链接,您的
应用程序应能够将该流量委托给 "https"。 -->
<data android:scheme="http" />
<data android:scheme="https" />
<!-- 包含一个或多个需要验证的域。 -->
<data android:host="auth.myapp.com" />
</intent-filter>
Log.i(TAG, "Creating auth request for login hint: $loginHint")
val authRequestBuilder: AuthorizationRequest.Builder = Builder(
mAuthStateManager.getCurrent().getAuthorizationServiceConfiguration(),
mClientId.get(),
ResponseTypeValues.CODE,
"https://auth.myapp.com/oauth/handler" // 具有 https 协议的重定向 URI
)
.setScope(mConfiguration.getScope())
if (!TextUtils.isEmpty(loginHint)) {
authRequestBuilder.setLoginHint(loginHint)
}
mAuthRequest.set(authRequestBuilder.build())
iOS
对于 iOS,您需要在后端托管格式如下的 /.well-known/apple-app-site-association:
{
"applinks": {
"details": [{
"appID": "ABCDE12345.com.myapplication.ios",
"paths": ["/oauth/redirect/*"]
}]
},
"appclips":{
"apps":[
"ABCDE12345.com.myapplication.ios"
]
},
"webcredentials":{
"apps":[
"ABCDE12345.com.myapplication.ios"
]
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
...
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:auth.myapp.com</string>
</array>
...
</dict>
</plist>
func doAuthWithAutoCodeExchange(configuration: OIDServiceConfiguration, clientID: String, clientSecret: String?) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
self.logMessage("Error accessing AppDelegate")
return
}
// 构建身份验证请求
let request = OIDAuthorizationRequest(configuration: configuration,
clientId: clientID,
clientSecret: clientSecret,
scopes: [OIDScopeOpenID, OIDScopeProfile],
redirectURL: "https://auth.myapp.com/oauth/handler",
responseType: OIDResponseTypeCode,
additionalParameters: nil)
// 执行身份验证请求
logMessage("Initiating authorization request with scope: \(request.scope ?? "DEFAULT_SCOPE")")
appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in
if let authState = authState {
self.setAuthState(authState)
self.logMessage("Got authorization tokens. Access token: \(authState.lastTokenResponse?.accessToken ?? "DEFAULT_TOKEN")")
} else {
self.logMessage("Authorization error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
self.setAuthState(nil)
}
}
}
多平台
// android/build.gradle
android {
// ...
defaultConfig {
// ...
// 添加以下行
manifestPlaceholders = [auth0Domain: "auth.myapp.com", auth0Scheme: "https"]
}
// ...
}
final authorizationEndpoint =
Uri.parse('http://example.com/oauth2/authorization');
final tokenEndpoint = Uri.parse('http://example.com/oauth2/token');
final identifier = 'my client identifier';
final secret = 'my client secret';
// 具有自定义协议的重定向 URI
final redirectUrl = Uri.parse('https://auth.myapp.com/oauth/handler');
final credentialsFile = File('~/.myapp/credentials.json');
Future<oauth2.Client> createClient() async {
var exists = await credentialsFile.exists();
if (exists) {
var credentials =
oauth2.Credentials.fromJson(await credentialsFile.readAsString());
return oauth2.Client(credentials, identifier: identifier, secret: secret);
}
var grant = oauth2.AuthorizationCodeGrant(
identifier, authorizationEndpoint, tokenEndpoint,
secret: secret);
var authorizationUrl = grant.getAuthorizationUrl(redirectUrl);
await redirect(authorizationUrl);
var responseUrl = await listen(redirectUrl);
return await grant.handleAuthorizationResponse(responseUrl.queryParameters);
}
链接
标准
- OWASP_MASVS_L1:
- MSTG_PLATFORM_3
- MSTG_PLATFORM_4
- MSTG_STORAGE_6
- OWASP_MASVS_L2:
- MSTG_PLATFORM_3
- MSTG_PLATFORM_4
- MSTG_NETWORK_5
- MSTG_STORAGE_6
- PCI_STANDARDS:
- REQ_2_2
- REQ_6_2
- REQ_6_3
- REQ_8_3
- REQ_11_3
- OWASP_MASVS_v2_1:
- MASVS_PLATFORM_1
- HIPAA_CONTROLS:
- SECURITY221
- SECURITY212
- SECURITY213
- SECURITY252
- SOC2_CONTROLS:
- CC_2_1
- CC_3_4
- CC_4_1
- CC_7_1
- CC_7_2
- CC_7_4
- CC_7_5