đź§ JavaScript - Deobfuscation
What is it?
- Concept: Client-side JS is sometimes protected with a commercial/open-source obfuscator (most commonly
obfuscator.io) to hinder casual reading — a shuffled string-lookup array, a custom decode function, control-flow flattening, and self-defending anti-tamper checks. Occasionally there’s an extra layer on top: the real application logic is encrypted (e.g. AES-GCM viawindow.crypto.subtle) and only decrypted +eval’d inside an actual browser session, so reading the file statically reveals nothing at all — not even obfuscated-but-readable code. - Impact: Recovering hidden client-side logic — endpoint names, validation rules, feature flags, or an entirely different network call that isn’t visible anywhere else in the visible page/network traffic. The mere presence of unusually heavy protection on what should be a plain, low-value bundle (e.g. a marketing site) is itself a signal something’s deliberately hidden.
How it works
- Recognize
obfuscator.io’s signature: an IIFE that reorders a large string array, calls like_0x1234('0x56')scattered everywhere referencing a custom decoder, and self-defending code that regex-checksFunction.prototype.toString()output to detect tampering/reformatting. - Work out whether it’s “just obfuscated” (safe to statically decode the string array and read the result directly) or “obfuscated and encrypted” — look near the end of the file for
crypto.subtle,importKey,decrypt, and a trailingeval. That combination means the real logic is a ciphertext blob, decrypted only at runtime. - For the encrypted case: don’t hand-transcribe the giant array from a screenshot/paste — a single mis-copied byte silently breaks the decode with no error, and there’s no way to tell decoded-garbage from a transcription mistake. Instead, run the actual file on disk inside an isolated JS runtime (Node’s
vmmodule, or a headless browser) with the dangerous primitives (crypto.subtle.decrypt,eval) swapped for instrumented stand-ins that capture their inputs/outputs without ever executing anything. - Recover the key/IV/ciphertext from the captured
importKey/decryptcalls, decrypt with a trusted crypto implementation (e.g. Node’s owncryptomodule) — independent of the obfuscated code’s own logic — and read the plaintext straight out of the capturedevalargument.
Exploitation
Prerequisites
- The actual JS file on disk (not a screenshot/paste — see step 3 above)
- A JS runtime capable of sandboxing execution (Node.js is enough; no headless browser required)
Attack Vectors
Recognizing the pattern.
// telltale obfuscator.io shape
var _0x2bc2 = function () { var _0x2b5fab = ['WO7dTmo...' /* thousands of entries */]; ...};
function a0_0x41a8(_0xf45fab, _0x39137a) { /* custom RC4-ish decoder */ }Safely decrypting a WebCrypto-guarded payload without ever running it. This is the valuable trick — de-aliased, the guarded logic typically looks like:
window.crypto.subtle.importKey('raw', keyBytes, {name: 'AES-GCM'}, false, ['decrypt'])
.then(key => window.crypto.subtle.decrypt({name: 'AES-GCM', iv: ivBytes}, key, ciphertextBytes))
.then(plaintext => (0, eval)(new TextDecoder().decode(plaintext)));Run the real file inside a Node vm sandbox with the dangerous primitives replaced:
const vm = require('vm');
const crypto = require('crypto');
const fs = require('fs');
const sandbox = {
eval: (code) => { sandbox.__captured_eval__ = code; }, // capture, don't run
window: { crypto: { subtle: {
importKey: (_fmt, keyData) => Promise.resolve({ __raw: Buffer.from(keyData) }),
decrypt: ({ iv }, key, data) => {
const ivBuf = Buffer.from(iv), dataBuf = Buffer.from(data);
// WebCrypto AES-GCM appends the 16-byte auth tag to the ciphertext — split it off
const tag = dataBuf.subarray(-16), ct = dataBuf.subarray(0, -16);
const decipher = crypto.createDecipheriv('aes-256-gcm', key.__raw, ivBuf);
decipher.setAuthTag(tag);
return Promise.resolve(Buffer.concat([decipher.update(ct), decipher.final()]));
},
}}},
document: { readyState: 'complete', addEventListener: () => {} },
TextDecoder, atob: (b64) => Buffer.from(b64, 'base64').toString('binary'),
};
vm.createContext(sandbox);
vm.runInContext(fs.readFileSync('app.js', 'utf8'), sandbox);
console.log(sandbox.__captured_eval__); // the real, decrypted source — never executedKey details that make this work:
- Indirect eval
(0, eval)(x)resolves the bareevalidentifier through normal scope lookup. Pre-settingsandbox.evalto a plain function before the script runs shadows the real intrinsic — since it’s now just an ordinary function reference rather than a direct call to the specific globalevalbinding, none of eval’s special “run in caller’s scope” semantics apply. It’s a fully inert, safe function call. - Decrypt with a trusted, independent implementation (Node’s own
crypto) rather than trusting the obfuscated code’s own crypto calls to behave — this means the plaintext is recovered without ever having to fully read or trust the obfuscated logic itself.
Notes
- A single mis-transcribed byte in a giant obfuscated string array breaks an RC4-style decode silently — garbage output, no error thrown. This is why hand-transcription from a chat/screenshot is unreliable; always work from the real file saved to disk.
- If a script this heavily protected is unusual for its surrounding context (e.g. a plain marketing site shipping AES-encrypted, self-decrypting JS), that mismatch is itself worth prioritizing over other recon — it’s a strong signal something is deliberately hidden there. (from HTB Machine - Cohort)
- The same “instrument a sandboxed copy instead of trusting/running the real thing” pattern generalizes well beyond JS obfuscation — the same reasoning applies to any self-decrypting/self-modifying payload.
Mitigation
- Obfuscation/encryption of client-side JS is not a security boundary. Anything that has to execute in the browser is inherently readable by the browser’s own JS engine at runtime — treat obfuscation as an obstacle to casual inspection only, never as an actual access control.
- If genuinely sensitive logic must ship to the client, the fix isn’t stronger obfuscation — it’s moving that logic server-side where it’s never exposed at all.
Related Usage
| File | Created |
|---|---|
| HTB Machine - Cohort | Monday, August 10th 2026, 12:30:58 pm |
References: