跳转至

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);
    }

建议

为了解决该漏洞,建议不要使用自定义协议来重定向身份验证令牌。

开发人员应考虑以下选项之一:

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