MetaCTF September 2024 Flash CTF consists of 5 challenges. Only 3 of them are covered here.

Stack Smashers

This is the first challenge.

We are given the source code for a small C program that takes user input, writes it to a 16-byte buffer (buffer[16]), and then checks if memory.winner is set to true. If it is, the win() function is executed, which prints out the flag.

The program uses gets() to read the input. Since gets() is unsafe (it doesn’t perform bounds checking), and there is no input validation, an overflow is possible.

To exploit this, we need an input of 17 or more bytes to overflow into the memory of memory.winner. By writing anything to this location, the condition check returns true, and the flag is printed.

void win() {
    printf("Congratulations! You've successfully performed a buffer overflow! Here's your flag: FLAG_REDACTED_IN_SNIPPET");
    //The flag has been removed from this version of the code, in the real running code, it will be printed when this function is called
}

int main() {
    struct {
        char buffer[16];
        bool winner;
    } memory;
    memory.winner = false;
    gets(memory.buffer);
    if (memory.winner) {
        win();
    } else {
        printf("Try again!\n");
    }
}

challenge

Key For Me

The second challenge.

We’re thinking of creating an serial key activation system to distribute our CTFs, can you do a quick check to make sure we’re the only ones able to make keys for it? I’m just a little paranoid recently. Here’s the source code, if you do make a key, enter it at nc [REDACTED] 30008 to claim a flag!

#!/usr/local/bin/python

from flag import FLAG

def check_key(key):

    if len(key) != 8 or ord(key[0]) % 5 != 3 or ord(key[1]) % 4 != 2 or not key[2].isdigit() or not key[3].islower() or not ord(key[4]) <= ord(key[3]) + 5 or not key[5].isdigit() or int(key[5]) <= int(key[2]) + 2 or not key[6].islower() or ord(key[6]) >= ord(key[3]) - 3 or not key[7].isupper() or ord(key[7]) >= ord(key[4]) - 4:
        return False
    return True

def main():

    print("""
 MetaCTF Key Validation System
      _____
     /   _ \\__.--._._--.-_
    |   |_| ______________\\
     \\_____/
""")

    key = input("Enter your activation key: ")
    if check_key(key):
        print(f"Congratulations! You unlocked the door, here's your prize: {FLAG}")
        return 0
    else:
        print("Sadly your key didn't work this time, keep working on it!")
        return 1

if __name__ == "__main__":
    main()  

In this challenge, we need to generate a key to get the flag. The function of interest here is check_key(), which has a single conditional statement with multiple logical conditions. We can break it down to make it easier to understand:

    # Length must be 8
    if len(key) != 8:
        return False
        
    # First character - ASCII value must be modulo 5 equals 3. Eg. 8, 15, 28, 33...
    if ord(key[0]) % 5 != 3:
        return False
        
    # Second character - ASCII value must be modulo 4 equals 2. Eg. 10, 24, 26    
    if ord(key[1]) % 4 != 2:
        return False
        
    # Third character - Must be a digit
    if not key[2].isdigit():
        return False
        
    # Fourth character - Must be in lower case. Eg a-z
    if not key[3].islower():
        return False
        
    # Fifth character - ASCII value must be less or equal to the ASCII value of fourth character + 5
    if not ord(key[4]) <= ord(key[3]) + 5:
        return False
        
    # Sixth character - Must be a digit and its value must be greater than the value of third character + 2
    if not key[5].isdigit() or int(key[5]) <= int(key[2]) + 2:
        return False
        
    # Seventh character - Must be in lower case and its ASCII value must be less the value of fourth character - 3
    if not key[6].islower() or ord(key[6]) >= ord(key[3]) - 3:
        return False
        
    # Eighth character - Must be in upper case and its ASCII value must be less than the ASCII value of the fifth character - 4
    if not key[7].isupper() or ord(key[7]) >= ord(key[4]) - 4:
        return False
    return True

We can either write a key generator or try to solve it manually.

import random
import string

def check_key(key):

    if len(key) != 8 or ord(key[0]) % 5 != 3 or ord(key[1]) % 4 != 2 or not key[2].isdigit() or not key[3].islower() or not ord(key[4]) <= ord(key[3]) + 5 or not key[5].isdigit() or int(key[5]) <= int(key[2]) + 2 or not key[6].islower() or ord(key[6]) >= ord(key[3]) - 3 or not key[7].isupper() or ord(key[7]) >= ord(key[4]) - 4:
        return False
    return True

def gen_key():
    key = [0] * 8

    key[0] = chr(random.choice([x for x in range(33, 127) if x % 5 == 3]))

    key[1] = chr(random.choice([x for x in range(33, 127) if x % 4 == 2]))

    # Makes sure key[2] and key[5] work together.
    # key[5] is dependant and it needs to be greater than key[2]+2. This means the maximum value of key[2] can be 6. When key[2]+2=6+2=8, key[5] can only be 9
    key[2] = random.choice([x for x in range(0, 7)])

    # Makes sure key[3] and key[6] work together.
    # Must be at least 101(e)
    key[3] = random.choice(string.ascii_lowercase[5:])

    # Makes sure key[4] and key[7] work together.
    # 69(E). Must be at least 70(F)
    while True:
	    key[4] = chr(random.choice([x for x in range(33, 127) if x <= ord(key[3])+5 ])) 
	    if ord(key[4]) > 69: 
	        break

    # Generates a number between (key[2]+2)+1 and 9. +1 ensures the range begins from greater number.
    key[5] = random.choice([x for x in range((key[2]+2)+1, 10) ])

    key[6] = random.choice([x for x in string.ascii_lowercase if ord(x) < ord(key[3]) - 3 ])
    
    key[7] = random.choice([x for x in string.ascii_uppercase if ord(x) < ord(key[4]) - 4 ])

    my_key = "".join(map(str, key))
    return my_key

while True:
	my_key = gen_key()
	valid = check_key(my_key)
	if valid == True:
		print(my_key)
		break

Run the script to generate a key. key generation

Connect to the server. connect and get flag

Library

I’ve been developing this little book library app in Go, but I think something is wrong with the way it’s handling book requests. Some of the book titles seem to be giving unexpected results… Maybe you can figure it out?

package main

import (
	"html/template"
	"log"
	"net/http"
	"os"
)

type ReadBook struct{}

func (rb ReadBook) ReadBook(filePath string) string {
	content, err := os.ReadFile(filePath)
	if err != nil {
		return "Error reading file: " + err.Error()
	}
	return string(content)
}

var books = map[string]string{
	"1984":            "1984 is a dystopian social science fiction novel by George Orwell.",
	"brave_new_world": "Brave New World is a dystopian novel by Aldous Huxley.",
	"f451":            "Fahrenheit 451 is a dystopian novel by Ray Bradbury.",
}

type Book struct {
	Title string
	Param string
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
	var bookList []Book
	for key := range books {
		bookList = append(bookList, Book{
			Title: key,
			Param: key,
		})
	}
	tmpl := template.Must(template.ParseFiles("templates/home.html"))
	tmpl.Execute(w, bookList)
}

func bookHandler(w http.ResponseWriter, r *http.Request) {
	userBook := r.URL.Query().Get("book")
	bookContent, validBook := books[userBook]

	if validBook {
		tmpl := template.Must(template.ParseFiles("templates/book.html"))
		tmpl.Execute(w, bookContent)
		return
	}

	if userBook != "" {
		tmpl, err := template.New("book").Parse(userBook)
		if err != nil {
			http.Error(w, "Template parsing error: "+err.Error(), http.StatusInternalServerError)
			return
		}
		tmpl.Execute(w, ReadBook{})
		return
	}

	http.Error(w, "No book specified", http.StatusBadRequest)
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080" // Default to 8080 if no PORT environment variable is set
	}

	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
	http.HandleFunc("/", homeHandler)
	http.HandleFunc("/books", bookHandler)

	log.Println("Starting server on port " + port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

A simple web page built with Go and its template system.

The homepage lists all book titles in the library, each linking to a book page with a description. It uses a GET parameter, “book,” to pass the title and handle the request.

The file containing the logic is main.go, and two html files inside directory “templates”

The function of interest here is bookHandler, which processes the book requests.

This is what happens: Here’s the process:

userBook is assigned the GET parameter “book”. bookContent, validBook access and retrieve the content from the books map. If validBook is true, the book’s description is displayed in the book.html template. Nothing out of the ordinary. If validBook is false, it checks whether userBook is non-empty, creates a new template named book , and parses userBook, which can contain Go template syntax. Finally, it executes the template with an empty instance of ReadBook{}.

No input validation or sanitization. This allows template injetion.

In the beginning of the code, you can see the defined empty struct and then the defined function/method ReadBook() for the above-mentioned empty struct. The function has a parameter filePath, and the file at that location is opened, read, and the content returned.

Referring to Go’s html/template documentation, in order to do the injection we need to use the template syntax - {{ }} and . to access the template’s passed argument - ReadBook{} which can read a file.

Lets try books?book={{.ReadBook “/etc/passwd”}}

root:x:0:0:root:/root:/bin/sh
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
....

Or XSS injection.

<script>alert('Injection!')</script>

It works. Now the flag.

books?book={{.ReadBook “flag.txt”}}

inject and get the flag