đź§  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 via window.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

  1. 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-checks Function.prototype.toString() output to detect tampering/reformatting.
  2. 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 trailing eval. That combination means the real logic is a ciphertext blob, decrypted only at runtime.
  3. 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 vm module, 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.
  4. Recover the key/IV/ciphertext from the captured importKey/decrypt calls, decrypt with a trusted crypto implementation (e.g. Node’s own crypto module) — independent of the obfuscated code’s own logic — and read the plaintext straight out of the captured eval argument.

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 executed

Key details that make this work:

  • Indirect eval (0, eval)(x) resolves the bare eval identifier through normal scope lookup. Pre-setting sandbox.eval to 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 global eval binding, 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.
FileCreated
HTB Machine - CohortMonday, August 10th 2026, 12:30:58 pm

References: