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 PasswordResetReceiver, a broadcast receiver that changes the stored password from a newpass intent extra and logs the old password. PasswordResetReceiver is declared as exported in the AndroidManifest.xml with no android:permission, so external callers can send the broadcast and reset the password. Tapping Refresh in VaultActivity then shows the new value.
// 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// PasswordResetReceiver, an exported broadcast receiver that changes the stored password from// an unvalidated intent extra and logs the old password. Any app can send the broadcast to// reset the password; tapping Refresh in VaultActivity then shows the new value.// Inspired by the receiver in the "Android Insecure Bank" app.packageorg.owasp.mastestappimportandroid.app.Activityimportandroid.content.BroadcastReceiverimportandroid.content.Contextimportandroid.content.Intentimportandroid.os.Bundleimportandroid.util.Logimportandroid.widget.Buttonimportandroid.widget.LinearLayoutimportandroid.widget.TextViewclassMastgTest(privatevalcontext:Context){companionobject{constvalPREFS="secure_prefs"constvalKEY_PASSWORD_STORE="vault_password"constvalDEFAULT_PASSWORD="originalPass123"}funmastgTest():String{// Seed the stored password the first time the demo runs.valprefs=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.classVaultActivity:Activity(){privatelateinitvarstatus:TextViewoverridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)actionBar?.hide()vallayout=LinearLayout(this).apply{orientation=LinearLayout.VERTICALsetPadding(64,120,64,64)}valtitle=TextView(this).apply{text="MASTestApp – Password Vault"textSize=22f}status=TextView(this).apply{textSize=18fsetPadding(0,48,0,48)}valrefresh=Button(this).apply{text="Refresh"setOnClickListener{showPassword()}}layout.addView(title)layout.addView(status)layout.addView(refresh)setContentView(layout)showPassword()}overridefunonResume(){super.onResume()showPassword()}privatefunshowPassword(){valpwd=getSharedPreferences(MastgTest.PREFS,Context.MODE_PRIVATE).getString(MastgTest.KEY_PASSWORD_STORE,"")status.text="Current vault password:\n\n$pwd"}}// FAIL: [MASTG-TEST-0366] PasswordResetReceiver is exported and does not need any special permissions.// External callers, like other apps including adb, can send the broadcast and reset the password.classPasswordResetReceiver:BroadcastReceiver(){overridefunonReceive(context:Context,intent:Intent){valnewPassword=intent.getStringExtra("newpass")?:returnvalprefs=context.getSharedPreferences(MastgTest.PREFS,Context.MODE_PRIVATE)valoldPassword=prefs.getString(MastgTest.KEY_PASSWORD_STORE,"")Log.d("MASTG-DEMO","Password changed from $oldPassword to $newPassword")prefs.edit().putString(MastgTest.KEY_PASSWORD_STORE,newPassword).apply()}}}
Use Enumerating Broadcast Receivers to list the exported broadcast receivers and their associated android:permission by running run.sh.
The run.sh script lists the exported receivers declared in the reverse-engineered manifest.
run.sh
1 2 3 4 5 6 7 8 91011121314
#!/bin/bash# List the broadcast receivers declared as exported in the AndroidManifest.python3-<<'PY'>output.txtimportrexml=open("AndroidManifest_reversed.xml").read()forminre.finditer(r"<receiver\b.*?(?:/>|</receiver>)",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)ifexportedandexported.group(1)=="true":print("Exported receiver:",name.group(1)ifnameelse"?","| permission:",permission.group(1)ifpermissionelse"none")PY
The test case fails because PasswordResetReceiver performs a security-relevant action (storing a password and being able to update it) and is exported (android:exported="true") without any permission protection. Because PasswordResetReceiver is exported and unprotected, external callers can send a broadcast to it and overwrite the password.
The onReceive method changes the stored password from an intent extra and discloses the old password to the log:
overridefunonReceive(context:Context,intent:Intent){valnewPassword=intent.getStringExtra("newpass")?:returnvalprefs=context.getSharedPreferences(MastgTest.PREFS,Context.MODE_PRIVATE)valoldPassword=prefs.getString(MastgTest.KEY_PASSWORD_STORE,"")Log.d("MASTG-DEMO","Password changed from $oldPassword to $newPassword")prefs.edit().putString(MastgTest.KEY_PASSWORD_STORE,newPassword).apply()}
VaultActivity does not protect the underlying exported broadcast receiver. Access control must be enforced at the PasswordResetReceiver boundary.
The output also lists androidx.profileinstaller.ProfileInstallReceiver. This receiver is added by the AndroidX Profile Installer library and, although exported, is protected by android:permission="android.permission.DUMP", a signature/privileged permission that ordinary apps can't hold. It is development tooling and is not reported as vulnerable in this test case.