# NCA CTF Writeup

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727067021002/c5c92d51-44e9-42aa-b71f-5542629e1bda.png align="center")

Opening the link, we were greeted with a login button that lead to login panel which looked like

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727067803670/57194989-2d3e-48b2-b7b7-afcde9eb0ff9.png align="center")

As the name of the challenge suggested, the first thing I checked was for `robots.txt`

The following entries were present in it.

```plaintext
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

```plaintext
┌──(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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727067908237/30fd8a53-e24f-4285-b513-7eed4d62ef22.png align="center")

Logging in with credentials `fl4g:asudfgr3456_g1v3_m3_th3_fl4g_x435naslkjf`, we successfully get the flag.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727067974926/0d01fe00-78f3-41c1-9006-e1991f4315bf.png align="center")

### Epic Glitch

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727068031556/35b55853-f69f-4bb8-a8bf-6a0bcc00408e.png align="center")

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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727068154233/41cd3d5a-dc73-4634-b839-0575b83f0fcd.png align="center")

The contents of the file `app.py` were

```python
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.

```python
 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.

```python
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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727080100194/e2cd045a-e023-4db4-8606-36d5eb7ac9d6.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727080164784/23d4a080-7061-4616-aa01-6fc3058a8792.png align="center")

`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.

```python
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](https://gchq.github.io/CyberChef/#recipe=From_Base64\('A-Za-z0-9%2B/%3D',true,false\)XOR\(%7B'option':'UTF8','string':'%7B%22username%22:%20%22nepal%22,%20%22uid%22:%201%7D'%7D,'Standard',false\)&input=QzBjSEFCVVhEUlVFRTBkS1JWQWRGUlVDR0V0YVJWSVFHeGRTWDBORkZBPT0) 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](https://gchq.github.io/CyberChef/#recipe=XOR\(%7B'option':'UTF8','string':'perspective'%7D,'Standard',false\)To_Base64\('A-Za-z0-9%2B/%3D'\)&input=eyJ1c2VybmFtZSI6ICJuZXBhbCIsICJ1aWQiOiAwfQ&oeol=VT).

Once we have forged the token, we can replace it in our `auth_token` to recieve the flag

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727080277655/dd58622b-1312-4369-97f0-c22e70176a3d.png align="center")

### My Name is Marie Biscuit V2

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727080348337/dfddca01-4338-427b-8870-4bfea803291e.png align="center")

The link contained a simple login panel. Using the credentials provided in the description, I was able to login and got this screen.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727227686852/0f8ea305-3455-4eac-8447-56164393d816.png align="center")

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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727227769630/7cebc2d6-0650-43b9-917f-3cf94fe1dcde.png align="center")

I double checked using the `jwt.io` to ensure it was a `JWT` token.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727227824101/60bb296f-0033-47db-a61e-90eee06486dd.png align="center")

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.

```plaintext
┌──(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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727228125139/fa9200a2-df50-4fe8-9a71-1793b6d54b4c.png align="center")

Using the newly forged token on the cookie, I was able to get the flag.

```plaintext
┌──(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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727228290697/71e541cc-3c15-401b-b42b-1ff8d447d69f.png align="center")

Opening the link, we see the following prompt that asks to specify a username in base64 format.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727228393329/18121b3c-736d-4c41-afc7-d75cd47f0b4e.png align="center")

Clicking on the example, a GET request is made to

[`http://147.79.67.234:3223/?username=aGFyaWJhaGFkdXI=`](http://147.79.67.234:3223/?username=aGFyaWJhaGFkdXI=)

The username consisted the string `haribahadur` in base64 format and the output of page looked like.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727229470912/0253e81a-bbff-474a-9d6c-d6eabd3882f6.png align="center")

I quickly base64 encoded `hello` and tested it.

[`http://147.79.67.234:3223/?username=aGVsbG8=`](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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727228709300/4131967d-f815-4d84-8a63-6087af8681ea.png align="center")

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='`](http://147.79.67.234:3223/?username=YWRtaW4%3d--)

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.
