247CTF // The Six-Character Alphabet - Deobfuscating a JSF*c|< Login

247CTF // The Six-Character Alphabet - Deobfuscating a JSFuck Login

Category: reverse engineering, client-side trust

Stack: JavaScript, JSFuck encoding, Function constructor


STEPS
  1. Confirm the blob is JSFuck: only the characters [ ] ( ) ! + are used
  2. Extract the obfuscated expression from the page's onsubmit handler
  3. Hook Function.prototype.constructor in Node so every generated source string is dumped instead of executed
  4. Evaluate the blob and collect the dumped layers - expect return unescape/escape machinery first
  5. Decode the %uXXXX-escaped layers until the final comparison appears as plain JavaScript
  6. Read the hard-coded username/password pair from the if - the flag is derived from it

1. The target

A login form whose onsubmit is a single expression written in only

[]()!+. Submitting the right pair pops "Valid"; the flag materializes from the

credentials.

2. What JSFuck is (thirty seconds)

JSFuck is a JavaScript subset encoding the whole language from six characters via

type coercion:


[] + []      -> ""            (empty string)

![]          -> false         (coerce array to bool)

!![]         -> true

+[]          -> 0

[![]] + [][[]] -> "undefined"  (string concat)

From those atoms, string indexing ("false"[2] === 'l') builds every character;

character strings assemble keywords; keywords reach the Function constructor;

and Function("source")() executes arbitrary code. The wall of brackets is

assembly, not encryption.

Recognizing it is half the solve: any blob containing *only* [ ] ( ) ! + is

JSFuck, and JSFuck always bottoms out at Function or eval.

3. The deobfuscation rig

Never run unknown JSFuck in your browser or a bare node -e. Hook the execution

layer instead:


// intercept.js  -  run under: node intercept.js jsfuck.js

const fs = require('fs');

const payload = fs.readFileSync(process.argv[2], 'utf8');



const payloads = [];

const RealFunction = Function;

// shadow Function.prototype.constructor used by fn["constructor"](src)

Object.defineProperty(Function.prototype, 'constructor', {

  value: function (...args) {

    if (typeof args[args.length - 1] === 'string') payloads.push(args[args.length - 1]);

    return RealFunction.apply(this, args);

  },

  writable: true, configurable: true

});



try { eval(payload); } catch (e) { /* deobfuscation layers throw; harmless */ }



for (const p of payloads) console.log('LAYER:

' + p + '

---');

What falls out, layer by layer:

  1. 1. "return unescape" / "return escape" - the machinery fetches helpers.
  2. 2. %u-escaped strings assembled via unescape(...) - the actual payload text.
  3. 3. The final layer: plain JavaScript doing the comparison.

For the login form, the endgame is always the same shape:


if (this.username.value == 'the_flag_is'

 && this.password.value == '247CT.{...}') { alert('Valid...'); }

this is the form; username/password are named form elements. The

credentials - and the flag, which shares the password's format - are string

literals in the source you just dumped.

4. Why this works, philosophically

JSFuck raises the *reading* cost of client-side code; it does not lower the

*trust* cost. The comparison runs in the attacker's environment, so the attacker

controls the debugger, the DOM, the network, and - with one constructor hook -

the moment the payload reveals itself. Obfuscation of an authorization decision

that executes off-server is disclosure by construction.

The practical corollary for defenders: anything shipped to a browser - JSFuck or

not - must be assumed readable by its recipient. Client-side checks are UX, not

security.

5. Hunting notes for similar challenges

  • Charset fingerprinting. Only six characters -> JSFuck; only letters/dots/

pipes/braces -> look for template injection instead; letters+parens+quotes ->

plain eval'd JS.

  • The constructor hook generalizes. Any obfuscation that bottoms out at

fn["constructor"]("code")(), setTimeout("code"), or eval yields its

payload to the same interception. setTimeout/setInterval with string args

are worth hooking too.

  • Layered unescape. JSFuck-built strings are frequently %uXXXX-escaped;

each dumped layer may need unescape/decodeURIComponent before it's readable.

Decode everything you capture, then read it again - the flag is rarely in the

first readable layer.

Comments

Popular Posts