Thursday, January 8, 2015

Android app with full control over your Google account

Some time ago after I had defended my diploma thesis on OAuth security my groupmate asked me: "Hey, have you looked into Android OAuth?", and I felt slightly lost since I realized there is yet another OAuth implementation, and I didn't know how it works.

Lately I found some time to resolve this problem. The task seemed challenging at the beginning since Android OAuth is a part of Google Play, which is closed source: this was the first time I had to reverse-engineer to see how the open standard works (namely OAuth). Instead of explaining the whole design myself in this write up, I recommend to read sbktech's blog where he has recently published his full, descriptive, and easy to read explanation of Android OAuth internals. I would just add a few notes about my own findings to the existing sbktech's post:

TL;DR: I was able to find two vulnerabilities in Google Play system apk which allowed me to bypass the Android application permission model: an installed app asking no permissions could get full access to the device owner's Google account (it is sufficient for a new app install or Chrome sync access).

As a first step to understand the weak parts of the OAuth logic I binded to the com.google.android.gms/.auth.GetToken service manually and made, perhaps, a classic mistake with "NetworkOnMainThreadException", which thankfully brought me the "getToken() -> ... -> network request" callstack in a logcat to explore:

W/GLSUser (  602): GoogleAccountDataService.getToken()
I/GoogleHttpClient(  602): Falling back to old SSLCertificateSocketFactory
I/GoogleHttpClient(  602): Using GMS GoogleHttpClient
W/GLSActivity(  602): [GetToken] - getToken exception!
W/GLSActivity(  602): android.os.NetworkOnMainThreadException
W/GLSActivity(  602): at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1145)
W/GLSActivity(  602): at libcore.io.BlockGuardOs.connect(BlockGuardOs.java:84)
W/GLSActivity(  602): at libcore.io.IoBridge.connectErrno(IoBridge.java:144)
W/GLSActivity(  602): at libcore.io.IoBridge.connect(IoBridge.java:112)
W/GLSActivity(  602): at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:192)
W/GLSActivity(  602): at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:459)
W/GLSActivity(  602): at java.net.Socket.connect(Socket.java:843)
W/GLSActivity(  602): at com.android.okhttp.internal.Platform.connectSocket(Platform.java:131)
W/GLSActivity(  602): at com.android.okhttp.Connection.connect(Connection.java:101)
W/GLSActivity(  602): at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:294)
W/GLSActivity(  602): at com.android.okhttp.internal.http.HttpEngine.sendSocketRequest(HttpEngine.java:255)
W/GLSActivity(  602): at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:206)
W/GLSActivity(  602): at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:345)
W/GLSActivity(  602): at com.android.okhttp.internal.http.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:89)
W/GLSActivity(  602): at com.android.okhttp.internal.http.HttpURLConnectionImpl.getOutputStream(HttpURLConnectionImpl.java:197)
W/GLSActivity(  602): at com.android.okhttp.internal.http.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java:254)
W/GLSActivity(  602): at gaz.a(SourceFile:823)
W/GLSActivity(  602): at gaz.c(SourceFile:692)
W/GLSActivity(  602): at gaz.execute(SourceFile:601)
W/GLSActivity(  602): at xt.execute(SourceFile:365)
W/GLSActivity(  602): at xt.execute(SourceFile:447)
W/GLSActivity(  602): at avc.a(SourceFile:258)
W/GLSActivity(  602): at avd.a(SourceFile:575)
W/GLSActivity(  602): at avd.a(SourceFile:649)
W/GLSActivity(  602): at avd.a(SourceFile:812)
W/GLSActivity(  602): at avi.a(SourceFile:282)
W/GLSActivity(  602): at avh.a(SourceFile:163)
W/GLSActivity(  602): at axm.a(SourceFile:133)
W/GLSActivity(  602): at axf.a(SourceFile:337)
W/GLSActivity(  602): at axf.a(SourceFile:132)
W/GLSActivity(  602): at arx.a(SourceFile:92)
W/GLSActivity(  602): at arh.a(SourceFile:107)
W/GLSActivity(  602): at wj.onTransact(SourceFile:63)
W/GLSActivity(  602): at android.os.Binder.execTransact(Binder.java:404)
W/GLSActivity(  602): at dalvik.system.NativeStart.run(Native Method)
W/System.err( 1093): android.os.NetworkOnMainThreadException
W/System.err( 1093): at android.os.Parcel.readException(Parcel.java:1475)
W/System.err( 1093): at android.os.Parcel.readException(Parcel.java:1419)
W/System.err( 1093): at com.google.android.gms.auth.sample.helloauth.GetNameInForeground$myConnection.onServiceConnected(GetNameInForeground.java:100)
W/System.err( 1093): at android.app.LoadedApk$ServiceDispatcher.doConnected(LoadedApk.java:1110)
W/System.err( 1093): at android.app.LoadedApk$ServiceDispatcher$RunConnection.run(LoadedApk.java:1127)
W/System.err( 1093): at android.os.Handler.handleCallback(Handler.java:733)
W/System.err( 1093): at android.os.Handler.dispatchMessage(Handler.java:95)
W/System.err( 1093): at android.os.Looper.loop(Looper.java:136)
W/System.err( 1093): at android.app.ActivityThread.main(ActivityThread.java:5017)
D/ConnectivityService(  389): handleInetConditionHoldEnd: net=0, condition=0, published condition=0
W/System.err( 1093): at java.lang.reflect.Method.invokeNative(Native Method)
W/System.err( 1093): at java.lang.reflect.Method.invoke(Method.java:515)
W/System.err( 1093): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
W/System.err( 1093): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
W/System.err( 1093): at dalvik.system.NativeStart.main(Native Method)


I restored the logic of those three-letter classes from arh to gaz (that's the Google Play part) and felt an extreme sympathy to the avd class because of the two following reasons:

1. URL parameter injection


The below function of the avd class parsed the getToken Bundle extras argument and inserted all _opt_XXX parameters from it inside the HTTP request as XXX, obviously allowing to set has_permission=1 without any user consent:

  public final List a(String paramString1, String paramString2, int paramInt, String paramString3, boolean paramBoolean1, Bundle paramBundle, boolean paramBoolean2, String paramString4, boolean paramBoolean3, boolean paramBoolean4, CaptchaSolution paramCaptchaSolution, PACLConfig paramPACLConfig, FACLConfig paramFACLConfig, String paramString5)
    if (str8.startsWith("_opt_"))
    {
      localaux1.a(str8.replaceFirst("_opt_", ""), paramBundle.getString(str8));

      ...


2. Magic scopes "SID" and "LSID"


The GooglePlay also gladly granted me a couple of undocumented scopes, actually giving me back those SID and LSID session cookies in clear:

  public final TokenResponse a(TokenResponse paramTokenResponse, Map paramMap, int paramInt, String paramString1, boolean paramBoolean1, boolean paramBoolean2, String paramString2, PACLConfig paramPACLConfig, FACLConfig paramFACLConfig)
    {
    ...
      if (("SID".equals(paramString1)) || ("LSID".equals(paramString1)))
      {
         str1 = (String)paramMap.get(paramString1);

         ...

Additionally, I made a few more steps on my way to the PoC:

  • I impersonated the gms app by setting _opt_app=com.google.android.gms
  • I bypassed the signature verification by copy-pasting the signatures and setting them through _opt_client_sig=<sig> (sorry, no crypto flaws here)
  • I collected signatures for all versions of gms (two in total: 58e1c4133f7441ec3d2c270270a14802da47ba0e and 38918a453d07199354f8b19af05ec6562ced5788), so that my code worked on all Android 4/5 phones
  • I was able to leak the device owner's email through the AccountManager.newChooseAccountIntent for using it in GoogleAuthUtil.getToken (this intent silently returns the user's email if you signed into the only one Google account)

As a result, considering an installed app requiring no permissions, (1) allowed me to just leak all possible oauth2 scopes, while with (2) I was able to take over Google account.

PoC: https://gist.github.com/isciurus/df4d7edd9c3efb4a0753

Timeline:
December 2, 2014 — Reported the vulnerability to the Android security, @natashenka confirmed the repro works
January 6, 2015 — Response form Android security saying that the fix was pushed in mid-December, I checked that the repro stopped working on all my phones
January 9, 2015 — Public disclosure

Thanks to @evdokimovds from DSecRG for helping with unpacking tools and to @jduck from droidsec for verifying the code on multiple Android phones.