JS RECON LABS - SOLUTIONS ========================= Lab 1 - String Array Lookups (/JSRECON/lab1.html, bundle: js/l1.app.js) The bundle stores every string in _0x41ab and rotates the array at load time. Console on the lab page: for (let i = 0; i < 11; i++) console.log(i, _0x2f(i)); Index 4 decodes to "legacy_profile.php" and is only used by a function that is never called. Request: /JSRECON/api/v1/legacy_profile.php?uid=1 Flag: flag{js_string_array_leaks_the_route} Lab 2 - Orphan Chunk (/JSRECON/lab2.html, bundle: js/l2.main.js) The chunk manifest maps two chunks: p -> chunk.4c1e9a.js (loaded on click), x -> chunk.9b70f3.js (never loaded). Fetch /JSRECON/js/chunk.9b70f3.js by hand. It holds four base64 fragments: atob("L0pTUkVDT04vYXBpLw==") + ... -> /JSRECON/api/v2/internal_backup.php?scope=all Or in the console: __req('x').then(c => console.log(c.route())); Request: /JSRECON/api/v2/internal_backup.php?scope=all (scope=all is required) Flag: flag{orphan_chunk_holds_the_admin_api} Lab 3 - XOR Table and Flat Control Flow (/JSRECON/lab3.html, bundle: js/l3.app.js) _s(i) = base64 decode of _t[i], XORed with the repeating key "r4t" (hidden as \x72\x34\x74). Console: for (let i = 0; i < 10; i++) console.log(i, _s(i)); _asm('0|1|2|3|4|5') Table: /JSRECON/ | api/ | v3/ | gateway.php | ?op= | svc.rotate.dump | health.php | ... Request: /JSRECON/api/v3/gateway.php?op=svc.rotate.dump (wrong opcode -> 403) Flag: flag{xor_table_plus_flat_switch_beats_you_not} Lab 4 - Signed Routes (/JSRECON/lab4.html, bundle: js/l4.app.js) On load the bundle fetches /JSRECON/js/l4.routes.map: reports -> L0pTUkVDT04vYXBpL3Y0L3JlcG9ydHMucGhw -> /JSRECON/api/v4/reports.php export -> L0pTUkVDT04vYXBpL3Y0L2V4cG9ydC5waHA= -> /JSRECON/api/v4/export.php (never called) Both routes require ?nonce=&sig=. The salt sits in _c[] as char codes XORed with 0x2b -> "r4t_sign_v4_5eCr3t". Signer (same in JS and PHP): h = 5381; for each char: h = ((h * 33) + code) >>> 0; sig = h.toString(16) Shortcut in the console: __signed(atob(__routes.export), 'hunter2') Worked example: nonce=hunter2 -> sig=120273d5 Request: /JSRECON/api/v4/export.php?nonce=hunter2&sig=120273d5 Flag: flag{client_side_signing_salt_is_not_a_secret} Lab 5 - Packed Second Stage (/JSRECON/lab5.html, bundles: js/l5.boot.js + two vendor chunks) l5.vendor.chunk.js sets __vseg.a and __vseg.c, l5.polyfill.chunk.js sets __vseg.b. l5.boot.js joins a+b+c, base64 decodes, RC4 decrypts with the key "r4t_v5_k3y" (built from four hex-escaped fragments) and runs the plaintext with new Function() - so the real code never hits disk. To read it: copy the boot IIFE into the console with new Function(_rc4(_p,_k))() swapped for console.log(_rc4(_p,_k)). The stage registers window.__console(), which sends: GET /JSRECON/api/v5/console.php?mode=dump X-Rat-Console: RAT-C0NS0LE-9f2b17 (header name is built from the char codes [88,45,82,97,116,45,67,111,110,115,111,108,101], token is joined from ['RAT','C0NS0LE','9f2b17']) curl: curl -H "X-Rat-Console: RAT-C0NS0LE-9f2b17" "http://labs.hackxpert.com/JSRECON/api/v5/console.php?mode=dump" Flag: flag{packed_stage2_header_token_pwned} TAKEAWAY Client-side code is not a trust boundary. Route names, opcodes, signing salts, header tokens and whole second-stage bundles all ship to the browser - obfuscation only changes how long the recon takes.