Skip to content

actuator/chatpdf.pro

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 

Repository files navigation

Dirty Stream Vulnerability in chatpdf.pro

App Name: PDF AI: Podcast, Notes, Slides Package Name: chatpdf.pro Version Tested: 4.22.0 Developer: iAI Lab Vulnerability Type: Dirty Stream - Arbitrary File Creation in Victim App's Private Storage via Trusted _display_name Metadata Play Store: https://play.google.com/store/apps/details?id=chatpdf.pro


Summary

chatpdf.pro accepts file-share intents (ACTION_SEND / ACTION_VIEW) carrying a content:// URI from any other app on the device. When the URI points at an attacker-controlled ContentProvider, the app reads the URI's _display_name metadata column and uses that string verbatim as the destination filename when copying the streamed bytes to disk. Because the value is attacker-controlled and never sanitized, supplying a _display_name containing ../ sequences gives the attacker arbitrary file creation inside the victim app's own private storage, both internal (/data/data/chatpdf.pro/) and external (/sdcard/Android/data/chatpdf.pro/).

The trust model is the bug: the app treats _display_name as a safe filename when the documentation explicitly states it is a display string and may contain anything the source provider chooses to return. A zero-permission attacker app delivers a single intent, and the victim app - running in its own process with its own UID - opens an output stream at the attacker-resolved path and writes the attacker-supplied bytes. All filesystem operations execute inside the victim's security context.

What the primitive yields:

  • Arbitrary file creation inside /data/data/chatpdf.pro/ - the victim app's UID-protected internal storage. Every subdirectory is reachable: files/, files/sp/ (MMKV preference stores), databases/, shared_prefs/, code_cache/, plus arbitrary new directories at any depth.
  • Arbitrary file creation inside /sdcard/Android/data/chatpdf.pro/ - the victim app's external app-private storage, including getExternalFilesDir(null) and getExternalCacheDir().
  • Intermediate directory creation at any depth - the sink calls mkdirs() on the resolved path, so the attacker can plant payloads inside fresh, attacker-named directory trees that did not previously exist.
  • Full attacker control over the file's contents via ContentResolver.openInputStream() against the attacker's ContentProvider.

All of this is reachable from any of seven exported <activity-alias> entries in the manifest that accept mimeType="*/*", requiring zero permissions on the attacker app.


Severity

CVSS 3.1 base score: 7.3 (High) - CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N

Metric Value Justification
Attack Vector Local Exploitation requires a malicious application installed on the same device. The vulnerable component is reachable via Android's local Binder IPC bus, not over the network. No shell access, physical access, or authenticated session is required - only co-residence on the device.
Attack Complexity Low No race condition, no environmental dependencies, no timing window. A single ACTION_SEND intent with the path-traversal _display_name succeeds on first try. Confirmed by runtime PoC.
Privileges Required None The attacking app declares zero Android permissions. The seven exported <activity-alias> entries targeting SplashActivity accept mimeType="*/*" with no permission attribute.
User Interaction None The PoC dispatches via setClassName() directly to one of the exported aliases. No share sheet, no user prompt; the intent flows through BaseSplashActivity.onCreate to the vulnerable sink autonomously.
Scope Changed The attacker (unprivileged third-party app, UID X) causes file writes inside the victim app's UID-Y-protected directory /data/data/chatpdf.pro/, which Android's app-sandbox model normally isolates from every other UID on the device. The vulnerable component (the intent-handling and file-copy code in chatpdf.pro) lives in one security authority; the impact (modification of UID-Y-owned private storage by an unprivileged third party) crosses into another.
Confidentiality None Write-only primitive. The sink writes attacker-supplied bytes to disk; it does not return any of the victim app's existing content to the attacker. No information disclosure has been demonstrated.
Integrity High Confirmed by runtime PoC: attacker has full control over filename, directory structure, and byte content of files written to /data/data/chatpdf.pro/{files,files/sp,databases,shared_prefs,…} and /sdcard/Android/data/chatpdf.pro/. Reachable targets include MMKV preference stores (billing_cache, paid_storage_sp, chatpdf.pro_preferences, inappconfig, the Firebase auth state in com.google.firebase.auth.api.Store.*), every SQLite database the app uses (chat-database, chatv2-database, history-database, reader-doc.db, the cached Firestore DB, gallery-database), and bundled cached PDFs the app's renderer consumes. Total compromise of the victim app's persistent state.
Availability None No availability impact has been demonstrated. The primitive creates files but overwrite of pre-existing files was not confirmed by the runtime PoC.

Technical Details

Attack Vector

  • Intent.ACTION_SEND (and ACTION_SEND_MULTIPLE, ACTION_VIEW)
  • Intent.EXTRA_STREAM and/or ClipData URI
  • Attacker-controlled content:// URI returning a path-traversal _display_name
  • Reachable from any of seven exported <activity-alias> entries declaring mimeType="*/*" (see Entry Points below)

Vulnerable Sink

Identified by static analysis of the decompiled APK. Class names are obfuscated using Thai Unicode characters; original semantics reconstructed below.

Class: Lo40; (located in classes.dex)

Method 1 - _display_name extractor:

public static Object getDisplayName(Context ctx, Uri uri) {
    Cursor c = ctx.getContentResolver().query(
        uri, new String[]{"_display_name"}, null, null, null);
    if (c != null && c.moveToFirst()) {
        int idx = c.getColumnIndex("_display_name");
        return Result.success(c.getString(idx));   // raw, untrusted
    }
    return Result.failure(new Exception("unknown"));
}

Method 2 - vulnerable copy:

public final Object copyUriToDir(FragmentActivity act, Uri src,
                                  File destDir, Continuation cont) {
    String displayName = (String) getDisplayName(act, src);
    if (displayName == null) return failure("get file name error");

    if (!destDir.exists()) destDir.mkdirs();
    File dest = new File(destDir, displayName);          // ← VULNERABLE
    InputStream in = act.getContentResolver().openInputStream(src);
    FileOutputStream out = new FileOutputStream(dest);
    copyStream(in, out);
    return Result.success(dest);
}

new File(parent, child) performs no path normalization on the child argument; .. resolution occurs at I/O time on the resolved string. There is no .. filter, no canonical-path check, no character whitelist. The query projection is hardcoded to _display_name, so _data is not a vector here.

The sink also calls destDir.mkdirs() before opening the output stream. Because mkdirs() is recursive and operates on the post-traversal resolved path, the primitive creates intermediate directories along arbitrary resolved paths. Attacker-named directory trees can be stamped into any reachable region - there is no "parent must exist" precondition.

Destination Directory

image

Every subdirectory under the package's internal data root is reachable by extending the attacker's _display_name payload:

  • files/ - app data including bundled cached PDFs (Welcome to PDF AI🤖.pdf, [Try to Read Novel] The little prince.pdf, [Try to Read Paper] Attention is all you need.pdf); Firebase Installation IDs; Crashlytics state; Firebase Sessions DataStore protobufs
  • files/sp/ - MMKV-backed preference stores: billing_cache, paid_storage_sp, chatpdf.pro_preferences, PrefCommon, inappconfig, splash_v_info, update, mmkv.default, plus per-SDK stores including com.google.firebase.auth.api.Store.* (Firebase auth state, 32 KB), com.google.firebase.messaging, com.google.firebase.crashlytics, com.applovin.sdk.*, com.vungle.sdk, admob (256 KB), WebViewChromiumPrefs
  • databases/ - every SQLite database the app uses: chat-database, chatv2-database (chat content), history-database, reader-doc.db, local-file-database, chat-template-database, gallery-database (516 KB), firestore.[DEFAULT].chatpdf-pro-9d3ad.(default) (cached Firestore, 160 KB), work_database, google_app_measurement_local.db
  • shared_prefs/ - empty in v4.22.0 (the app uses MMKV in files/sp/ instead) but writable as confirmed
  • code_cache/ - empty in v4.22.0 (no DEX/native artifacts dynamically loaded), but writable

2. Arbitrary file creation in the victim app's external private storage - /sdcard/Android/data/chatpdf.pro/

The classical Dirty Stream surface. The sink anchors at /sdcard/Android/data/chatpdf.pro/cache/tmp/ and writes propagate trivially throughout the package's external-private tree.

This region houses any cached or downloaded documents the app brings back from the network; overwriting one is a content-injection vector against the victim's document-handling code paths.

3. Intermediate directory creation at arbitrary depth

The sink calls destDir.mkdirs() on the resolved post-traversal path. Because mkdirs() is recursive, the attacker can stamp out fresh attacker-named directory trees at any depth as part of a single intent fire - there is no "parent must exist" precondition.

Confirmed: a payload of ../../../../../../../../data/data/chatpdf.pro/PROBE1_DEEP/a/b/c/d/e/f/g/h/i/j/POC.txt produced eleven new directory layers (PROBE1_DEEP/, a/, b/, … j/)

4. Full attacker control over file contents

copyUriToDir calls ContentResolver.openInputStream(src) on the attacker-supplied URI and copies the resulting stream byte-for-byte to the resolved destination. The attacker's ContentProvider can return any payload; in the PoC, EvilProvider.openFile() returns a ParcelFileDescriptor.createPipe() whose write-end is fed arbitrary bytes from a worker thread. There is no content-type check, no signature verification, no size limit at the sink.

Impact

The vulnerability gives a zero-permission attacker app arbitrary file creation inside the victim app's own private storage - both internal (/data/data/chatpdf.pro/, normally inaccessible to any other UID) and external app-private (/sdcard/Android/data/chatpdf.pro/) - with full control over filename, directory structure, and contents. The writes execute inside the victim app's security context as a consequence of the victim app trusting attacker-supplied _display_name metadata as a safe filename.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages