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.
// 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.packageorg.owasp.mastestappimportandroid.app.Activityimportandroid.app.Serviceimportandroid.content.Contextimportandroid.content.Intentimportandroid.os.Bundleimportandroid.os.IBinderimportandroid.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-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.classAuthService:Service(){companionobject{constvalKEY_PASSWORD="org.owasp.mastestapp.PASSWORD"}overridefunonBind(intent:Intent):IBinder? =nulloverridefunonStartCommand(intent:Intent?,flags:Int,startId:Int):Int{valnewPassword=intent?.getStringExtra(KEY_PASSWORD)if(newPassword!=null){applicationContext.getSharedPreferences(MastgTest.PREFS,Context.MODE_PRIVATE).edit().putString(MastgTest.KEY_PASSWORD_STORE,newPassword).apply()}returnSTART_NOT_STICKY}}}
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 91011121314
#!/bin/bash# List the services declared as exported in the AndroidManifest.python3-<<'PY'>output.txtimportrexml=open("AndroidManifest_reversed.xml").read()forminre.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)ifexportedandexported.group(1)=="true":print("Exported service:",name.group(1)ifnameelse"?","| permission:",permission.group(1)ifpermissionelse"none")PY
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: