Writeup by shd33 for CORS Playground

web NodeJS

January 8, 2025

Analysis of the challenge

The website allows us to play with HTTP by setting custom headers, both on the requests (which we can already do) and on the server response (which is more unusual). The goal is to find the flag stored in the file flag.txt stored at the root of the file-system.

By looking into the code, we learn that if we manage to set req.session.user = "internal", we can call res.sendfile with a parameter we set in the query. The session variables are managed by the cookie-session module, which stores them client-side as a signed cookie. We can control this cookie, but to forge a signature, we need to know the cryptographic keys used. These are stored in a .env file at the root of the app. But static files are managed by Express and served from the public/ directory within the app. So it looks like we are stuck again as we need to access a protected file. But are we?

Nginx to the rescue

CORS response headers are directed towards the client and won’t be of interest to us. However, some response headers are directed instead to the reverse proxy server (a NGINX server in our case). One of these is the X-Accel-Redirect header, which allows the proxied server to indicate that the response should be redirected to a given path. This feature was introduced to allow for efficient streaming of protected files. This way, Node.js can handle authorizations and let nginx do the actual serving from some internal location.

In our case, the nginx server has only one location declared: /. This means that any request to a file with a URI starting with “/” will be forwarded to the proxied Node.js server. But if the path specified in the X-Accel-Redirect header doesn’t start with “/”, it will be understood as to be served relative to the nginx working directory, which in our case is the WORKDIR defined in the Dockerfile: /usr/app. This means we can access the secret .envfile!

Only one obstacle remains: the app checks for headers starting with “X-” and removes them. This is not a problem as nginx still understands the x-accel-redirect header in lowercase.

Requesting /cors?x-accel-redirect=.env yields the environment variables:

PORT=3000
KEY1=244f6308a26ad41dd8ebacf617282a7f3dc1cb6fec5fa7a03f1a907857295620
KEY2=c3f8b13c86454198e624813a8d480dd2a43ed5154e5b43f33eedfe962831bbf2

Baking cookies

We can now forge a session cookie to set our user to "internal". The easiest way is probably to simply run a minimal Express website and get the cookie:

import express from 'express';
import cookieSession from 'cookie-session';

const app = express();

app.use(cookieSession({
    name: "session",
    keys: ["244f6308a26ad41dd8ebacf617282a7f3dc1cb6fec5fa7a03f1a907857295620","c3f8b13c86454198e624813a8d480dd2a43ed5154e5b43f33eedfe962831bbf2"]
}));

app.get('/', (req, res) => {
  req.session.user = "internal";
});

app.listen(3000, () => {
  console.log('Express server initialized');
});

From the browser, we find that our cookies (data and signature) are:
session=eyJ1c2VyIjoiaW50ZXJuYWwifQ==; session.sig=RWQPhq-qWIqFf7mmmflim6aNM5s.
Let’s set them on the challenge website.

Get the flag

We can now request the file we want by setting “filename” as a GET parameter! …Or nearly. flag.txt is at the file-system root, but the app checks that !req.query.filename?.includes("/"). The solution is to make req.query.filename an array! This payload finally gives the flag: /cors?filename[]=/flag.txt.