Skip to content

MASTG-DEMO-0129: Sensitive Action Exposed Through an Unprotected Service

Download MASTG-DEMO-0129 APK Open MASTG-DEMO-0129 Folder Build MASTG-DEMO-0129 APK

Sample

The sample implements a small password vault. Tapping Start opens VaultActivity, which displays the password currently stored in the app (originalPass123 on first run). The app also declares AuthService, a started service that reads a new password from the intent extras passed to onStartCommand and writes it to shared preferences. AuthService is declared as exported in the AndroidManifest.xml with no android:permission, so external callers can start it directly with an explicit intent and reset the password. Tapping Refresh in VaultActivity then shows the new value.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// SUMMARY: This sample implements a small password vault. Tapping Start opens VaultActivity,
// which shows the password currently stored in the app. The app also declares AuthService, an
// exported started service that changes the stored password from an intent extra in onStartCommand
// without verifying the caller. Any app can start it and reset the password; tapping Refresh in
// VaultActivity then shows the new value. Inspired by the "Sieve" AuthService.

package org.owasp.mastestapp

import android.app.Activity
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView

class MastgTest(private val context: Context) {

    companion object {
        const val PREFS = "secure_prefs"
        const val KEY_PASSWORD_STORE = "vault_password"
        const val DEFAULT_PASSWORD = "originalPass123"
    }

    fun mastgTest(): String {
        // Seed the stored password the first time the demo runs.
        val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
        if (!prefs.contains(KEY_PASSWORD_STORE)) {
            prefs.edit().putString(KEY_PASSWORD_STORE, DEFAULT_PASSWORD).apply()
        }

        // Open the legitimate vault screen, which displays the stored password.
        context.startActivity(
            Intent(context, VaultActivity::class.java).apply {
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            }
        )
        return "Opening the password vault…"
    }

    // Legitimate UI: shows the password currently stored in the vault.
    // Not exported. Tap Refresh after running the attack to see the value change.
    class VaultActivity : Activity() {

        private lateinit var status: TextView

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            actionBar?.hide()

            val layout = LinearLayout(this).apply {
                orientation = LinearLayout.VERTICAL
                setPadding(64, 120, 64, 64)
            }

            val title = TextView(this).apply {
                text = "MASTestApp – Password Vault"
                textSize = 22f
            }

            status = TextView(this).apply {
                textSize = 18f
                setPadding(0, 48, 0, 48)
            }

            val refresh = Button(this).apply {
                text = "Refresh"
                setOnClickListener { showPassword() }
            }

            layout.addView(title)
            layout.addView(status)
            layout.addView(refresh)
            setContentView(layout)

            showPassword()
        }

        override fun onResume() {
            super.onResume()
            showPassword()
        }

        private fun showPassword() {
            val pwd = getSharedPreferences(MastgTest.PREFS, Context.MODE_PRIVATE)
                .getString(MastgTest.KEY_PASSWORD_STORE, "")
            status.text = "Current vault password:\n\n$pwd"
        }
    }

    // FAIL: [MASTG-TEST-0365] AuthService is exported and does not need any special permissions.
    // External callers, like other apps including adb, can start it directly and reset the password.
    class AuthService : Service() {

        companion object {
            const val KEY_PASSWORD = "org.owasp.mastestapp.PASSWORD"
        }

        override fun onBind(intent: Intent): IBinder? = null

        override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
            val newPassword = intent?.getStringExtra(KEY_PASSWORD)
            if (newPassword != null) {
                applicationContext
                    .getSharedPreferences(MastgTest.PREFS, Context.MODE_PRIVATE)
                    .edit()
                    .putString(MastgTest.KEY_PASSWORD_STORE, newPassword)
                    .apply()
            }
            return START_NOT_STICKY
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?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.INTERNET" />

    <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:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MASTestApp"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:windowSoftInputMode="adjustResize"
            android:theme="@style/Theme.MASTestApp">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name="org.owasp.mastestapp.MastgTest$VaultActivity"
            android:exported="false"
            android:theme="@android:style/Theme.Material.Light" />
        <service
            android:name="org.owasp.mastestapp.MastgTest$AuthService"
            android:exported="true" />
    </application>

</manifest>

Steps

  1. Use Obtaining Information from the AndroidManifest to obtain the AndroidManifest.xml.
  2. Use Enumerating Services to list the exported services and their associated android:permission by running run.sh.

The run.sh script lists the exported services declared in the reverse-engineered manifest.

run.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/bin/bash
# List the services declared as exported in the AndroidManifest.
python3 - <<'PY' > output.txt
import re
xml = open("AndroidManifest_reversed.xml").read()
for m in re.finditer(r"<service\b.*?(?:/>|</service>)", xml, re.S):
    block = m.group(0)
    name = re.search(r'android:name="([^"]+)"', block)
    exported = re.search(r'android:exported="([^"]+)"', block)
    permission = re.search(r'android:permission="([^"]+)"', block)
    if exported and exported.group(1) == "true":
        print("Exported service:", name.group(1) if name else "?",
              "| permission:", permission.group(1) if permission else "none")
PY

Observation

The output reveals the exported services and their associated permissions:

output.txt
1
Exported service: org.owasp.mastestapp.MastgTest.AuthService | permission: none

Evaluation

The test case fails because AuthService exposes a security-relevant operation (changing the vault password) and is exported (android:exported="true") without any permission protection. Because AuthService is exported and unprotected, external callers that can address the component can start it directly and overwrite the password.

The service changes the stored password from an intent extra in onStartCommand without enforcing any caller permission:

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    val newPassword = intent?.getStringExtra(KEY_PASSWORD)
    if (newPassword != null) {
        applicationContext.getSharedPreferences(MastgTest.PREFS, Context.MODE_PRIVATE)
            .edit().putString(MastgTest.KEY_PASSWORD_STORE, newPassword).apply()
    }
    return START_NOT_STICKY
}

VaultActivity does not protect the underlying exported service. Access control must be enforced at the AuthService boundary.