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
- Confirm the blob is JSFuck: only the characters
[ ] ( ) ! +are used - Extract the obfuscated expression from the page's
onsubmithandler - Hook
Function.prototype.constructorin Node so every generated source string is dumped instead of executed - Evaluate the blob and collect the dumped layers - expect
return unescape/escapemachinery first - Decode the
%uXXXX-escaped layers until the final comparison appears as plain JavaScript - 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.
"return unescape"/"return escape"- the machinery fetches helpers. - 2.
%u-escaped strings assembled viaunescape(...)- the actual payload text. - 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
constructorhook 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
Post a Comment