Blog section
CTF

UMassCTF 2024 Writeup Roundup

A UMassCTF 2024 writeup roundup preserving my public notes across multiple challenge categories.

UMassCTFCTFWebForensicsReverse Engineering

This post preserves my original public notes for UMassCTF 2024 Writeup Roundup. I kept the challenge-by-challenge structure close to the source Markdown and only cleaned up formatting, image paths, and repeated footer text so it reads cleanly on the website.

UMassCTF 2024

challenge screenshot

About the Event

UMass CTF is back and better than ever this year! Get ready to dive into a thrilling array of challenges that will test your skills and push your limits. Participants can look forward to tackling intricate puzzles in Reverse Engineering, unlocking the mysteries of Cryptography, uncovering clues in Forensics, navigating the complex world of Binary Exploitation, and outsmarting defenses in Web Exploitation. Plus, we've got a host of miscellaneous challenges that are sure to surprise and engage. Don't miss out on the action-packed excitement at UMass CTF!

challenge screenshot

Conclusion

Pwned flags: 7/32

Total Score: 1340 points

Rank: 91th


100 Degrees

Description

Mr. Krabs has been tinkering with the restaurant thermometer to see what makes his staff the most productive. He's been tracking the data in his journal, but some "Lagrange" guy just called saying Mr. Krabs already has all the info he needs. Can you help Mr. Krabs predict how his staff will fare?

Files:

// journal.txt
p = 137

DAY(0) = 81
DAY(1) = 67
DAY(2) = 110
DAY(3) = 116
DAY(4) = 49
DAY(5) = 111
DAY(6) = 74
DAY(7) = 53
DAY(8) = 93
DAY(9) = 83
DAY(10) = 55
DAY(11) = 122
DAY(12) = 67
DAY(13) = 47
DAY(14) = 85
DAY(15) = 91
DAY(16) = 88
DAY(17) = 84
DAY(18) = 63
DAY(19) = 96
DAY(20) = 59
DAY(21) = 87
DAY(22) = 46
DAY(23) = 99
DAY(24) = 93
DAY(25) = 126
DAY(26) = 62
DAY(27) = 65
DAY(28) = 76
DAY(29) = 55
DAY(30) = 48
DAY(31) = 116
DAY(32) = 79
DAY(33) = 106
DAY(34) = 45
DAY(35) = 54
DAY(36) = 102
DAY(37) = 100
DAY(38) = 65
DAY(39) = 93
DAY(40) = 122
DAY(41) = 84
DAY(42) = 118
DAY(43) = 64
DAY(44) = 103
DAY(45) = 76
DAY(46) = 65
DAY(47) = 109
DAY(48) = 90
DAY(49) = 99
DAY(50) = 69
DAY(51) = 50
DAY(52) = 64
DAY(53) = 61
DAY(54) = 115
DAY(55) = 111
DAY(56) = 64
DAY(57) = 80
DAY(58) = 60
DAY(59) = 68
DAY(60) = 105
DAY(61) = 113
DAY(62) = 84
DAY(63) = 119
DAY(64) = 55
DAY(65) = 77
DAY(66) = 124
DAY(67) = 55
DAY(68) = 115
DAY(69) = 21
DAY(70) = 112
DAY(71) = 41
DAY(72) = 88
DAY(73) = 136
DAY(74) = 66
DAY(75) = 43
DAY(76) = 48
DAY(77) = 55
DAY(78) = 60
DAY(79) = 41
DAY(80) = 43
DAY(81) = 103
DAY(82) = 118
DAY(83) = 19
DAY(84) = 99
DAY(85) = 34
DAY(86) = 118
DAY(87) = 73
DAY(88) = 97
DAY(89) = 74
DAY(90) = 7
DAY(91) = 78
DAY(92) = 60
DAY(93) = 48
DAY(94) = 123
DAY(95) = 125
DAY(96) = 119
DAY(97) = 0
DAY(98) = 36
DAY(99) = 123
DAY(100) = 22

----------------------------------------

DAY(101) = ???
DAY(102) = ???
DAY(103) = ???
DAY(104) = ???
DAY(105) = ???
DAY(106) = ???
DAY(107) = ???
DAY(108) = ???
DAY(109) = ???
DAY(110) = ???
DAY(111) = ???
DAY(112) = ???
DAY(113) = ???
DAY(114) = ???
DAY(115) = ???
DAY(116) = ???
DAY(117) = ???
DAY(118) = ???
DAY(119) = ???
DAY(120) = ???
DAY(121) = ???
DAY(122) = ???
DAY(123) = ???
DAY(124) = ???
DAY(125) = ???
DAY(126) = ???
DAY(127) = ???
DAY(128) = ???
DAY(129) = ???
DAY(130) = ???
DAY(131) = ???
DAY(132) = ???

Solution Strategy

import numpy as np

# Function to perform modular Lagrange interpolation
def modular_lagrange_interpolation(x, y, x_new, p):
    sum = 0
    n = len(x)
    for i in range(n):
        prod = y[i]
        for j in range(n):
            if i != j:
                xi = int(x[i])
                xj = int(x[j])
                prod = (prod * (x_new - xj) * pow(xi - xj, -1, p)) % p
        sum = (sum + prod) % p
    return sum

# Function to convert values to ASCII characters if within printable range
def values_to_printable_ascii(values):
    return ''.join(chr(v) if 32 <= v <= 126 else '' for v in values)

# Given data points
x = np.arange(101)
y = np.array([
    81, 67, 110, 116, 49, 111, 74, 53, 93, 83, 55, 122, 67, 47, 85, 91, 88, 84, 63, 96, 59, 87, 46, 99, 93,
    126, 62, 65, 76, 55, 48, 116, 79, 106, 45, 54, 102, 100, 65, 93, 122, 84, 118, 64, 103, 76, 65, 109, 90,
    99, 69, 50, 64, 61, 115, 111, 64, 80, 60, 68, 105, 113, 84, 119, 55, 77, 124, 55, 115, 21, 112, 41, 88,
    136, 66, 43, 48, 55, 60, 41, 43, 103, 118, 19, 99, 34, 118, 73, 97, 74, 7, 78, 60, 48, 123, 125, 119, 0,
    36, 123, 22
], dtype=np.int64)
p = 137

# Predict days 101 to 132
days_to_predict = np.arange(101, 133)
predictions = [modular_lagrange_interpolation(x, y, day, p) for day in days_to_predict]

# Convert predictions to ASCII where possible
ascii_output_predicted = values_to_printable_ascii(predictions)

# Print the readable ASCII output
print("Readable ASCII Output:", ascii_output_predicted)

# Optionally, print all predictions for reference
print("Predictions:", predictions)
  • Imports and Dependencies: Utilizes the numpy library for numerical operations.
  • Modular Lagrange Interpolation Function:
    • Takes known data points (x, y), a new day x_new, and modulus p.
    • Computes the interpolated value at x_new using Lagrange's formula, performing calculations in modulo p to ensure values stay within bounds.
  • ASCII Conversion Function:
    • Converts a list of integer values into a string, using ASCII encoding only for values that fall within the printable range (32 to 126).
  • Data Setup:
    • x: An array representing the days from 0 to 100.
    • y: An array holding corresponding values for these days, which represent some sort of measurements or outputs.
    • p: The modulus used in the interpolation, set to 137.
  • Prediction Execution:
    • Predicts the values for days 101 to 132 by applying the interpolation function to each new day in this range.
  • Conversion to ASCII:
    • Transforms the predicted values into ASCII characters when possible, aiming to form a readable string.
  • Output:
    • Prints the readable ASCII output and the list of all predictions, providing both the potentially meaningful string and raw data for reference.

challenge screenshot


Holesome Birthday Party

Description

You just got invited to Spongebob's birthday! But he's decided to test your friendship with a series of challenges before granting you with the ticket of entrance. Can you prove that you're truly his friend and earn your entrance to this holesome birthday party? http://holesomebirthdayparty.ctf.umasscybersec.org

challenge screenshot

Solution Strategy

The website stated, 'You must first prove that your browser is from "Bikini Bottom!"' Let's change the User-Agent in the HTTP header to 'Bikini Bottom'

User-Agent: Bikini Bottom

challenge screenshot

The website stated, 'Sorry, but you're too early for the Spongebob Squarepant's birthday party!' Let's change the Date in the HTTP header to Spongebob Squarepant's birthday.

challenge screenshot

Date: Sat, 14 Jul 2024 23:25:28 GMT

challenge screenshot

The website stated, 'I’ve been trying to learn French… can you speak French?' Let's change the Accept-Language in the HTTP header to French 🇫🇷.

Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5

challenge screenshot

It seemed SpongeBob wanted a chocolate chip cookie, so I set the cookie in the HTTP header.

Cookie: flavor=chocolate_chip

challenge screenshot

There was another cookie with a Base64-encoded value: Login=eyJsb2dnZWRpbiI6IGZhbHNlfQ==

challenge screenshot

I then added another cookie with true encoded in Base64.

Cookie: flavor=chocolate_chip; Login=eyJsb2dnZWRpbiI6IHRydWV9Cg==

HAPPY BIRTHDAY Spongebob!

HAPPY BIRTHDAY Spongebob!


Krusty Katering

Description

Krusty Katering is hemorrhaging money, and Mr. Krabs has brought you in to fix it. You have 10 line cooks, and while they're okay at making Krabby patties, they can't agree on who cooks what and when. To make matters worse, Squidward (trying to keep his job) refuses to give you the list of orders, and will only tell you them one by one. Each time Squidward tells you a job, you get to add it to a cook's schedule for the day. Cooks cannot trade jobs, once it's on the schedule, it stays there. You want to ensure the last order finishes as soon as possible so that Mr. Krabs can close and count his profits. The competing Plankton's Provisions assigns their jobs randomly. So long as your crew is 20% more efficient than Team Chum Bucket every day this week, you're hired. Can you save Mr. Krabs' business?

nc krusty-katering.ctf.umasscybersec.org 1337

challenge screenshot

Solution Strategy

import socket

# Configuration for the server connection
HOST = 'krusty-katering.ctf.umasscybersec.org'
PORT = 1337

def connect_to_server(host, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, port))
    return sock

def parse_cooking_time(time_str):
    """Parse the cooking time from string to total seconds."""
    if 'm' in time_str:
        # Split the time at 'm'
        parts = time_str.split('m')
        minutes = int(parts[0])
        seconds = 0  # Assume 0 seconds if none are specified
        if parts[1]:  # Check if there are seconds after 'm'
            seconds = int(parts[1].replace('s', '').strip())
        total_seconds = minutes * 60 + seconds
    else:
        # Time given in seconds only
        total_seconds = int(time_str.replace('s', '').strip())
    return total_seconds

def main():
    sock = connect_to_server(HOST, PORT)
    
    try:
        cooks = [0] * 10
        
        while True:
            data = sock.recv(4096).decode('utf-8')
            if not data:
                break
            
            print(data)

            if "Estimated time to cook:" in data:
                # Extract the cooking time
                time_str = data.split('Estimated time to cook: ')[1].split('\n')[0].strip()
                cooking_time = parse_cooking_time(time_str)
                
                # Find the cook with the minimum total cooking time
                min_cook = cooks.index(min(cooks))
                
                # Assign this cooking time to that cook
                cooks[min_cook] += cooking_time
                
                # Send the cook number (+1 because index is 0-based and cook numbers are 1-based)
                response = str(min_cook + 1) + '\n'
                sock.sendall(response.encode('utf-8'))
    
    finally:
        sock.close()

if __name__ == '__main__':
    main()
  1. Import and Configuration:
    • Imports the socket library necessary for network communications.
    • Defines the server's host and port for the connection.
  2. Connecting to Server:
    • connect_to_server: A function that initializes a socket connection to the server using the specified host and port. Returns the socket object for communication.
  3. Parsing Cooking Times:
    • parse_cooking_time: Converts the time given as a string (in minutes and seconds or just seconds) into total seconds. It checks for 'm' to differentiate formats and handles cases where only minutes are provided without any seconds.
  4. Main Function Logic:
    • Establishes a connection to the server using the earlier defined function.
    • Initializes a list cooks with 10 elements, each set to zero, to keep track of the total cooking time assigned to each cook.
    • Enters a loop to continuously receive and process data from the server:
      • Data Reception: Retrieves data from the server. If no data is received, it breaks out of the loop.
      • Data Processing: If the data contains an "Estimated time to cook" line, it extracts this time and uses parse_cooking_time to convert it into seconds.
      • Job Assignment: Finds the cook with the least total assigned time (ensuring even workload distribution), assigns the new cooking time to this cook, and updates the cook's total time.
      • Response to Server: Sends back the number of the selected cook (adjusted for 1-based indexing) as a response to the server.
    • Clean-up: Ensures that the socket is properly closed after the loop finishes or in case of errors.
  5. Running the Script:
    • If the script is run directly (not imported), it executes the main() function.

challenge screenshot


Polyglot

Description

I've created a HTTP server that serves not just HTTP but also a few other protocols. Can you find the flag?

http://polygot.ctf.umasscybersec.org

challenge screenshot

Nmap

nmap -A -Pn polygot.ctf.umasscybersec.org

challenge screenshot

Solution Strategy

FTP

ftp polygot.ctf.umasscybersec.org 80
#haylin:haylin

challenge screenshot

After exploring the FTP site, I found that the interesting items were in the .ssh directory, including id_ed25519 and id_ed25519.pub.

challenge screenshot

chmod 600 id_ed25519

Let's try to connect via SSH using all the ports that were shown in the nmap scan.

SSH

ssh -i id_ed25519 -p 80 haylin@polygot.ctf.umasscybersec.org

BINGO!

BINGO!


Spongebobs Homepage

Description

Welcome to this great website about myself! Hope you enjoy ;) DIRBUSTER or any similar tools are NOT allowed.

http://spongebob-blog.ctf.umasscybersec.org

challenge screenshot

After reading the source code in the inspector, I found an image URL with two parameters.

challenge screenshot

Solution Strategy

After attempting SQL injection and XSS, which did not work, I tried command injection and it worked!

challenge screenshot

The error message indicated an issue with resizing the image and displayed '/bin/sh: 1: ls!: not found'. Let's adjust the payload.

/assets/image?name=house&size=500x494;+ls;

challenge screenshot

/assets/image?name=house&size=500x494;+cat+flag.txt;

challenge screenshot


Stop the voices

Description

Patrick’s been trying to remember the flag, but his vision seems a little blurry and the voices just don't stop...

Files:

challenge screenshot

from PIL import Image
import numpy as np

img = Image.open('FLAG.png').convert('L')
arr = np.asanyarray(img)

def normalize(mat):
    return (mat - mat.min()) / (mat.max() - mat.min()) * 255

for i in range(400):
    noise = np.random.normal(arr, 200)
    noise = normalize(noise)
    noise = noise.astype(np.uint8)
    im = Image.fromarray(noise)
    im.save(f"./samples/{i}.png")

Solution Strategy

from PIL import Image
import numpy as np
import os
from scipy.ndimage import gaussian_filter

def normalize(mat):
    return (mat - np.min(mat)) / (np.max(mat) - np.min(mat)) * 255

def average_and_process_images(folder_path):
    images = []
    
    for filename in os.listdir(folder_path):
        if filename.endswith('.png'):
            img = Image.open(os.path.join(folder_path, filename)).convert('L')
            images.append(np.array(img))
    
    if not images:
        print("No images found in the directory.")
        return None
    
    # Compute the mean of stacked images
    images_stack = np.stack(images, axis=0)
    mean_image = np.mean(images_stack, axis=0)
    
    # Apply Gaussian blur to the mean image
    smoothed_image = gaussian_filter(mean_image, sigma=1)
    
    # Increase brightness
    brightened_image = smoothed_image + 100  # Adjust the brightness level as needed
    brightened_image = np.clip(brightened_image, 0, 255)
    
    # Normalize the pixel values to [0, 255]
    normalized_image = normalize(brightened_image)
    normalized_image = normalized_image.astype(np.uint8)
    
    return Image.fromarray(normalized_image)

folder_path = './samples'
result_image = average_and_process_images(folder_path)
if result_image:
    result_image.save('Processed_FLAG.png')
    result_image.show()
    print("Image processed and saved as Processed_FLAG.png.")
  • Imports Necessary Libraries:
    • PIL.Image for image handling.
    • numpy for numerical operations on image arrays.
    • os for interacting with the file system.
    • gaussian_filter from scipy.ndimage for applying a Gaussian blur.
  • Defines normalize Function:
    • Normalizes a matrix (image) so its values are scaled between 0 and 255.
  • Defines average_and_process_images Function:
    • Loads Images: Iterates over files in a specified directory, loading any .png files as grayscale and converting them to NumPy arrays.
    • Checks for Images: If no images are found, it exits the function.
    • Averages Images: Stacks all the image arrays and calculates their pixel-wise mean, effectively reducing noise.
    • Applies Gaussian Blur: Smooths the averaged image using a Gaussian filter to reduce high-frequency noise.
    • Increases Brightness: Adds a constant value to all pixels to make the image brighter.
    • Clips Values: Ensures all pixel values are within the 0 to 255 range.
    • Normalizes the Image: Scales the pixel values to the full 0 to 255 range.
    • Converts to Image: Transforms the resulting array back into an image format.
  • Usage Code Block:
    • Sets the path where the noisy images are located.
    • Calls average_and_process_images with the specified path.
    • Saves the processed image if it was successfully created and displays it.
    • Prints out a message indicating the image has been processed and saved.
  • Processing Goal:
    • The script aims to combine multiple noisy images to approximate the original image by averaging them, applying blur to smooth out noise, adjusting brightness, and normalizing the final image.

challenge screenshot


Third Times the Charm

Description

This didn't work the first two times.

nc third-times-the-charm.ctf.umasscybersec.org 1337

challenge screenshot

Here's the source code for analysis

from Crypto.Util.number import getPrime

with open("flag.txt",'rb') as f:
    FLAG = f.read().decode()
    f.close()

def encrypt(plaintext, mod):
    plaintext_int = int.from_bytes(plaintext.encode(), 'big')
    return pow(plaintext_int, 3, mod)

while True:
    p = [getPrime(128) for _ in range(6)]
    if len(p) == len(set(p)):
        break

N1, N2, N3 = p[0] * p[1], p[2] * p[3], p[4] * p[5]
m1, m2, m3 = encrypt(FLAG, N1), encrypt(FLAG, N2), encrypt(FLAG, N3)

pairs = [(m1, N1), (m2, N2), (m3, N3)]
for i, pair in enumerate(pairs):
    print(f'm{i+1}: {pair[0]}\nN{i+1}: {pair[1]}\n')

Solution Strategy

def integer_cube_root(n):
    """ Return the integer cube root of n. """
    low, high = 0, n
    while low < high:
        mid = (low + high) // 2
        mid_cubed = mid ** 3
        if mid_cubed < n:
            low = mid + 1
        elif mid_cubed > n:
            high = mid
        else:
            return mid
    return low - 1 if (low - 1) ** 3 < n else low

def chinese_remainder_theorem(m1, N1, m2, N2, m3, N3):
    """ Solve the CRT for the given moduli and remainders. """
    # Compute the product of all the moduli
    N = N1 * N2 * N3
    
    # Compute the components for the CRT
    n1 = N // N1
    u1 = pow(n1, -1, N1)
    
    n2 = N // N2
    u2 = pow(n2, -1, N2)
    
    n3 = N // N3
    u3 = pow(n3, -1, N3)
    
    # Compute the combined result
    result = (m1 * n1 * u1 + m2 * n2 * u2 + m3 * n3 * u3) % N
    return result

m1 = 10973588338044630026393776466139629101535402297508552243095579715039519952960
N1 = 53300390655295507354295743299412341314859034210096993557820930892655160412911

m2 = 1047430100971457898055283625088929857742466910424038183292599585641630682928
N2 = 61042483956111277042126022211434452990034446614174198674122081070526057933847

m3 = 12886184147040798375394937665317145472702035394891013450696931746721499875559
N3 = 61324162672011523765365296520737106635216317316869399161544260726690977646883

# Apply CRT to find c
c = chinese_remainder_theorem(m1, N1, m2, N2, m3, N3)

# Extract the cube root of c
plaintext_int = integer_cube_root(c)

# Convert the integer back to bytes and decode to string
plaintext_bytes = plaintext_int.to_bytes((plaintext_int.bit_length() + 7) // 8, 'big')
flag = plaintext_bytes.decode('utf-8')

print("Recovered Flag:", flag)
  • Function: integer_cube_root(n)
    • Purpose: Computes the integer cube root of a given number n.
    • Method: Uses binary search to find the integer cube root efficiently. It checks for the exact cube root or determines the nearest integer less than the actual cube root.
  • Function: chinese_remainder_theorem(m1, N1, m2, N2, m3, N3)
    • Purpose: Solves the system of simultaneous congruences (each equation form is $$𝑚=𝑐3mod  𝑁m=c3modN$$) using the Chinese Remainder Theorem (CRT).
    • Method: Computes a combined result using the remainders (m1, m2, m3) and moduli (N1, N2, N3), by applying the properties of CRT. It calculates modular inverses and uses these to compute a single congruence that represents the original message cubed under a modulus that is the product of all the individual moduli.
  • Main Script:
    • Inputs: Values of ciphertexts (m1, m2, m3) and their corresponding moduli (N1, N2, N3).
    • Process:
      • Applies the CRT to determine a combined ciphertext under a combined modulus.
      • Extracts the cube root of this combined result to recover the original integer message.
    • Output Conversion: Converts the integer message back to bytes and decodes it to retrieve the plaintext, which in this case is the flag.
    • Output: Prints the recovered flag.

challenge screenshot