Intro

I was part of the CTF challenges admin team, where we worked on creating a CTF competition for our company. I didn’t have much experience with CTF before, so it was interesting to get involved in creating the challenges. There were four main topics I focused on when designing the problems. Btw, the problem titles are movies I recommend.

Cryptography

I like cryptography, and creating challenges for this CTF was really fun. I came up with two problem ideas:

  1. SSH Public Key Authentication

We all use SSH public key authentication frequently. Can it be brute-forced? I wanted to give this idea by using the RSA encryption algorithm with a smaller key size. In real life, RSA keys are typically 2048 bits, which are nearly impossible to break. However, for this challenge, I used a 256-bit key, making it relatively easy to crack.

The issue I ran into was that OpenSSH requires keys to be at least 1024 bits, which prevented me from using a 256-bit key directly. I even tried older versions of OpenSSH, but they also required key sizes greater than 256 bits. In the end, I modified the challenge: instead of using SSH, I provide a publickey.pem file and an encrypted message. The participants will need to decrypt the message using the public key.

  1. Group message encryption

How does group chat encryption work in real life? Typically, group chats use symmetric key encryption to secure messages. But how is the shared key calculated, and how do participants share it securely?

I want to propose an idea: using a Merkle tree to represent the shared group key. Each user would contribute their secret to the Merkle tree after agreeing on the encryption scheme. The root of the Merkle tree would then serve as the shared key for the group.

But what happens when someone leaves or is added to the group? We need to update the shared key. Fortunately, due to the structure of the Merkle tree, it is relatively easy to update the tree and recalculate the group key. Only the affected parts of the tree need to be recalculated, making it efficient to manage changes in group membership.

Reverse

I don’t have much experience with reverse engineering, but one of my friends, byamb4, is really is hecker. We discussed some ideas:

  1. About Binary

In languages like C or Go, after a program is compiled, it becomes a binary, and all the program logic is embedded within it. But can we reverse-engineer the binary to recover the original logic? It’s a much more complex topic, so we created a simpler problem.

The idea is based on salt and hashing. When saving passwords, we often use salt to make the hashes more secure. For this challenge, we used a binary that runs the password_hash function. If a user provides a password string, the binary returns a hashed version of the password with a salt. The challenge is that we’ve lost the original password, and participants need to reverse-engineer the binary to find it. The salt is stored as a static string in the binary, making it relatively easy to recover.

The second problem is relatively more difficult. Participants will need to reverse-engineer both the logic and the parameters. In this challenge, there’s a random password generator algorithm that two parties use to generate one-time passwords (OTPs). I got the idea from a movie, though I can’t remember which one. In the movie, the password was recalculated every 15 minutes. For this problem, participants have access to the binary and need to figure out the algorithm, find the seed, and determine the current password.

Forensic

In this challenge, we explored two problems:

  1. Disk Storage

How does a computer store data on a disk? Do files get completely erased after being deleted? The answer is often no, because we need to by lazy sometimes to keep energy little as possible. In this task, there is a disk image containing many deleted files. Can you recover the flag from one of them?

  1. File Identification

How do we know if a file is music, video, or text? We typically look at the file extension, but what happens if the extension is removed? In this task, you’ll start with a base64-encoded string. Once you decode it to binary data, you’ll discover it’s a zip file. Unzipping it will reveal another binary file, which could be a zip, tar, gzip, 7z, or another format. Can you correctly identify and extract the contents to find the flag?

Web

Web is the topic I’m most familiar with, but ironically, it was the hardest one to create challenges for. I wanted to keep the problems straightforward, so one focuses on URL injection and the other on automation. By the way, it’s challenging to come up with real-life, yet interesting, problems for CTFs.

Conclusion

Creating CTF challenges has been an interesting experience. Thanks to Byamb4 for sharing valuable ideas that helped shape some of the problems. As engineers, it’s crucial for us to be more aware of vulnerabilities and understand how they can be exploited. Working on these challenges highlighted the importance of not only writing secure code but also recognizing potential weaknesses in real-world applications. Through exercises like these, we can improve our ability to protect systems from various types of attacks. Below this, there’s a code and solution for the all problems that i recommended.

Problems

Crypto - 1. Finding nemo

Marlin is trying to find Nemo, but he encounters an octopus who knows cryptography. She asks him to find the hidden message in the given text. We need to help Marlin. The only clue is a public RSA key and the octopus’s name, Otto.

Input:

  • public_key.pem
  • message(base64): tjzcxU2SBzcDJl76ZTWBSBL+o4ijbkRXT7VZKOzXquI=

Solution:

RSA security relies on the fact that factoring large numbers (like n, which is the product of two large prime numbers p and q) is hard. In this case, we need to factor n to find p and q. Tools like Msieve or YAFU can help factor large numbers, or you can use factordb.com, an online tool for factoring integers.

  1. Get RSA parameters from public key
with open('public_key.pem', 'rb') as key_file:
    public_key = serialization.load_pem_public_key(key_file.read())

if isinstance(public_key, rsa.RSAPublicKey):
    public_numbers = public_key.public_numbers()
    modulus = public_numbers.n  # Modulus N
    exponent = public_numbers.e  # Public exponent e

    print(f"Modulus (N): {modulus}")
    print(f"Public Exponent (e): {exponent}")
else:
    print("The loaded key is not an RSA public key.")
  1. Find the p,q using yafu or factordb
  2. build private_key.pem
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from sympy import mod_inverse
import sympy  # For factorization and modular inverse

# Given modulus N
N = 88241730478298554455297400401663645747594464022843286938863438781971498192411
p = 129766800833396739358807081789224332651
q = 680002357394856057528752849081299295761

print("p, q:", p, q)

assert p*q == N
phi_N = (p - 1) * (q - 1)

e = 65537

assert sympy.gcd(e, phi_N) == 1

d = int(sympy.mod_inverse(e, phi_N))

print("d:", d)

private_key = rsa.RSAPrivateNumbers(
    p=p,
    q=q,
    d=d,
    dmp1=(d % (p - 1)),
    dmq1=(d % (q - 1)),
    iqmp=int(mod_inverse(q, p)),
    public_numbers=rsa.RSAPublicNumbers(e=e, n=N)
).private_key()

# Serialize private key to PEM format
private_key_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)

# Create public key object
public_key = private_key.public_key()

# Serialize public key to PEM format
public_key_pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# Save to files
with open('private_key.pem', 'wb') as f:
    f.write(private_key_pem)

with open('public_key.pem', 'wb') as f:
    f.write(public_key_pem)

print("Keys have been saved to private_key.pem and public_key.pem")
  1. decrypt message (or we can use online tools)
base64 -d encrypted_flag_base64.txt > decrypted_flag.bin
openssl rsautl -decrypt -inkey private_key.pem -in decrypted_flag.bin

Generate problem

#!/bin/bash
echo "flag{IM_A_FISH}" | openssl rsautl -encrypt -inkey public_key.pem -pubin -out encrypted_flag.bin

base64 encrypted_flag.bin > encrypted_flag_base64.txt

cat encrypted_flag_base64.txt
base64 -d encrypted_flag_base64.txt > decrypted_flag.bin
openssl rsautl -decrypt -inkey private_key.pem -in decrypted_flag.bin
ssh-keygen -t rsa -b 256 -f private_key.pem
openssl rsa -in private_key.pem -pubout -out public_key.pem

openssl rsa -pubin -in public_key.pem -modulus -noout

Crypto - 2. Stalker

The chat room uses a key rotation mechanism based on a Merkle tree. Each room member contributes a secret, which is hashed into a tree. The root of the Merkle tree is used as the shared secret key for encrypting messages. When a member leaves or joins, the tree is recomputed, and a new shared key is generated.

Alexander were once part of this room, and his secret was his name. However, after you left, the remaining members added a few new people, and a new key was generated. Unfortunately, the key rotation algorithm is flawed. If you know the names of the current members, you can predict the new key and decrypt the message.

You have intercepted an encrypted message. Your task is to regenerate the current shared secret key using the Merkle tree key rotation algorithm and decrypt the message.

Inputs:

  1. Member’s Secret: Each member’s secret is their name (case-sensitive).
  2. Hash Function: The hash function used is SHA-256.
  3. When a Member Leaves or Joins: The tree is rebalanced, and a new root is calculated as the new shared secret key.
  4. Room Members: Faime Jurno,Alisa Freindlich,Raimo Rendi,Nikolai Grinko,Anatoly Solonitsyn,Natasha Abramova,Andrei Tarkovsky
  5. Ecnrypted message: 2f36c0e23e4e4921454521fd98b364400a03dacf2e0a171cfade9903b15db958

Generate Problem and Solution

import hashlib
import binascii
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad, pad
from typing import List
import itertools
import random

def sha256_hexdigest(data: str) -> str:
    return hashlib.sha256(data.encode()).hexdigest()

def build_merkle_tree(leaves: List[str]) -> str:
    def hash_pair(left: str, right: str) -> str:
        return sha256_hexdigest(left + right)

    nodes = [sha256_hexdigest(leaf) for leaf in leaves]

    while len(nodes) > 1:
        new_level = []
        for i in range(0, len(nodes) - 1, 2):
            new_level.append(hash_pair(nodes[i], nodes[i+1]))

        if len(nodes) % 2 == 1:
            new_level.append(nodes[-1])

        nodes = new_level

    return nodes[0]

def encrypt_message(message: str, key: str) -> str:
    cipher = AES.new(binascii.unhexlify(key), AES.MODE_ECB)
    padded_message = pad(message.encode(), AES.block_size)
    encrypted_padded = cipher.encrypt(padded_message)
    return binascii.hexlify(encrypted_padded).decode()

def decrypt_message(encrypted_hex: str, key_hex: str) -> str:
    encrypted_bytes = binascii.unhexlify(encrypted_hex)
    key_bytes = binascii.unhexlify(key_hex)

    cipher = AES.new(key_bytes, AES.MODE_ECB)
    decrypted_padded = cipher.decrypt(encrypted_bytes)
    decrypted = unpad(decrypted_padded, AES.block_size)
    return decrypted.decode()

def generate_all_permutations(keys: List[str], encrypted_message_hex: str) -> str:
    permutations = itertools.permutations(keys)

    for perm in permutations:
        perm_str = ','.join(perm)
        shared_key_hex = build_merkle_tree(perm)
        try:
            return decrypt_message(encrypted_message_hex, shared_key_hex)
        except Exception as e:
            continue

    return None

members = ["Andrei Tarkovsky",  "Anatoly Solonitsyn", "Alisa Freindlich", "Nikolai Grinko", "Faime Jurno", "Natasha Abramova", "Raimo Rendi"]
shared_key_hex = build_merkle_tree(members)

encrypted_message_hex = encrypt_message("flag{WE_DONT_TALK_ANYMORE}", shared_key_hex)
print("encrypted hex:", encrypted_message_hex)

random.shuffle(members)
print(",".join(map(str,members)))
decrypted_message = generate_all_permutations(members, encrypted_message_hex)

print("Shared key (hex):", shared_key_hex)
print("Decrypted message:", decrypted_message)

Forensic: 1. Lost in Translation

An admin accidentally deleted an important file containing a flag. The file was deleted from a system, and the admin believes it’s fine because the file was removed from the filesystem. However, you know that deleted files can often be recovered, and your task is to find and recover the deleted file to obtain the flag.

You are given access to a disk image of the system where the file was deleted. Your challenge is to analyze the disk image, recover the deleted file, and extract the flag.

Files Provided:

  • Disk Image: disk_image.img - A raw disk image file from which the file needs to be recovered.

Generate Problem

#!/bin/bash

# Create disk image
dd if=/dev/zero of=disk.img bs=1M count=50
mkfs.ext4 disk.img

# Mount disk image
mkdir -p /mnt/disk
mount -o loop disk.img /mnt/disk

# Create and add files
for i in $(seq 1 1000); do
  echo "ene_bol_${i}" > "/mnt/disk/ene_a_${i}.txt"
done

echo "flag{Scarlett_Ingrid_Johansson}" > "/mnt/disk/ene_a_392.txt"

umount /mnt/disk

sleep 1

mount -o loop disk.img /mnt/disk
# Delete some files
for i in $(seq 3 800); do
  rm "/mnt/disk/ene_a_${i}.txt"
done

# Unmount disk image
sudo umount /mnt/disk

echo "Disk image created with 400 files, some deleted for challenge."

Solution

hexdump -C disk.img | grep -A 2 flag

Forensic: 2. The Invisible Man

Find the flag from the given file

Generate Problem

import zipfile
import tarfile
import os
import bz2
import gzip
import shutil
import random
import uuid

def create_initial_file(file_name, content="This is the secret flag: FLAG{WHAT_U_CANT_SEE_CAN_HURT_U}"):
    with open(file_name, 'w') as f:
        f.write(content)

def compress_nested(file_name, iterations=2000):
    current_file = file_name
    formats = ['zip', 'tar', 'bz2', 'gz']  # List of compression formats to alternate between

    for i in range(1, iterations + 1):
        ind = random.randint(0, len(formats)-1)
        format_type = formats[ind]

        g = uuid.uuid4()
        next_file = f'hidden_{g}'
        if i == iterations+1:
            next_file = 'hidden'
        print(next_file)
        if format_type == 'zip':
            with zipfile.ZipFile(next_file, 'w') as zipf:
                zipf.write(current_file)

        elif format_type == 'tar':
            with tarfile.open(next_file, 'w') as tarf:
                tarf.add(current_file)

        elif format_type == 'bz2':
            with open(current_file, 'rb') as input_file:
                with bz2.open(next_file, 'wb') as output_file:
                    shutil.copyfileobj(input_file, output_file)

        elif format_type == 'gz':
            with open(current_file, 'rb') as input_file:
                with gzip.open(next_file, 'wb') as output_file:
                    shutil.copyfileobj(input_file, output_file)

        os.remove(current_file)
        current_file = next_file

    return current_file

# Step 3: Generate the problem
def generate_problem():
    create_initial_file('flag.txt')  # Create the initial file with the flag
    final_file = compress_nested('flag.txt') 
    print(f"Final nested file is: {final_file}")

if __name__ == '__main__':
    generate_problem()

Solution

import zipfile
import tarfile
import bz2
import gzip
import shutil
import uuid
import os

def extract_file(file_name):
    with open(file_name, 'rb') as f:
        file_header = f.read(1024)

    if file_header[:4] == b'PK\x03\x04':
        with zipfile.ZipFile(file_name, 'r') as zipf:
            zipf.extractall()
            return zipf.namelist()[0]  # Return the name of the extracted file

    elif b'ustar' in file_header:
        with tarfile.open(file_name, 'r') as tarf:
            tarf.extractall()
            return tarf.getnames()[0]  # Return the name of the extracted file

    elif file_header[:3] == b'BZh':
        extracted_name = f'hidden_{uuid.uuid4()}'
        with bz2.open(file_name, 'rb') as bz2f, open(extracted_name, 'wb') as output_file:
            shutil.copyfileobj(bz2f, output_file)
        return extracted_name

    elif file_header[:2] == b'\x1f\x8b':
        extracted_name = f'hidden_{uuid.uuid4()}'
        with gzip.open(file_name, 'rb') as gzfile, open(extracted_name, 'wb') as output_file:
            shutil.copyfileobj(gzfile, output_file)
        return extracted_name

    else:
        raise ValueError("Unknown file format or unsupported file type.")

def decompress_nested(file_name, iterations=10000):
    current_file = file_name

    for i in range(1, iterations + 1):
        next_file = extract_file(current_file)
        os.remove(current_file)  # Clean up the current file
        current_file = next_file

    print(f"Final file content: {open(current_file, 'r').read()}")

if __name__ == '__main__':
    final_file = 'example'
    decompress_nested(final_file)

Reverse - 1. Saltburn

You have been provided with a binary executable compiled from C code that hashes numbers using a known hash function. The program is given a number 1 <= x <= 10^6, appends a static salt string to the number, and then hashes the result using a hash function.

Your task is to figure out the value of x that was hashed, given the hash and the salt.

Provided Information:

  1. hash_program
  2. hash: 3d7c3cef2491f23ca9a0b76fcc9afc93ad6bdd67d2812267a570eb492fe62247

Solution

  1. use tool like string to inspect the binary and extract the statis salt
strings hash_program | grep -i salt
  1. Brute force the value of x
import hashlib

def compute_sha256(data: str) -> str:
    return hashlib.sha256(data.encode()).hexdigest()

def find_number(target_hash: str, salt: str, max_number: int) -> int:
    for number in range(1, max_number + 1):
        combined = f"{number}{salt}"
        hash_value = compute_sha256(combined)
        if hash_value == target_hash:
            return number
    return None

target_hash = "3d7c3cef2491f23ca9a0b76fcc9afc93ad6bdd67d2812267a570eb492fe62247"
salt = "static_salt_oliver_quick"
max_number = 10**6

number = find_number(target_hash, salt, max_number)
if number is not None:
    print(f"The number is: {number}")
else:
    print("Number not found.")

Generate Problem

 gcc -o hash_program hash_program.c -lssl -lcrypto
#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>

#define SALT "static_salt_oliver_quick"

void compute_sha256(const char *str, unsigned char output[SHA256_DIGEST_LENGTH]) {
    SHA256_CTX sha256;
    SHA256_Init(&sha256);
    SHA256_Update(&sha256, str, strlen(str));
    SHA256_Final(output, &sha256);
}

void hash_number(int number, unsigned char output[SHA256_DIGEST_LENGTH]) {
    char buffer[256];
    snprintf(buffer, sizeof(buffer), "%d%s", number, SALT);
    compute_sha256(buffer, output);
}

void to_hex_string(const unsigned char *hash, char *hex_string) {
    for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
        sprintf(hex_string + (i * 2), "%02x", hash[i]);
    }
}

int main() {
    int number = 123456;
    unsigned char hash[SHA256_DIGEST_LENGTH];
    char hash_hex[2 * SHA256_DIGEST_LENGTH + 1];

    hash_number(number, hash);
    to_hex_string(hash, hash_hex);

    printf("Hash: %s\n", hash_hex);
    return 0;
}

Reverse - 2. Catch me if you can

Two parties, Alice and Bob, communicate using an RNG algorithm to generate a shared sequence of random numbers. Both parties use the same shared seed to initialize their RNG, but the exact algorithm used is unknown. Your task is to reverse-engineer the random number generator algorithm from the provided binary and find the seed that was used to generate a specific random number.

The first few random numbers have already been intercepted. Can you figure out the seed and predict the next random number in the sequence?

Given Information:

  • You are provided with a binary file rng_program.
  • The first few random numbers generated are:
    • Random number 1: 48271
    • Random number 2: 65535
    • Random number 3: 123456
  • The random number generator is initialized with an unknown seed (32-bit integer).
  • The RNG algorithm is built into the program, and you need to reverse-engineer the binary to find it.

Solution

  1. Reverse engineer the Binary
    1. Use tools like Ghidra, Radare2, or IDA Pro to reverse-engineer the binary and understand how the random numbers are generated.
    2. Analyze how the seed is initialized, how the RNG works, and how random numbers are generated.
  2. Brute-Force the Seed
    1. Once the RNG algorithm is identified, participants will need to brute-force the seed by simulating the RNG and comparing the first few numbers to the ones provided.
  3. Predict the Next Random Number
    1. After finding the correct seed, participants will use it to predict the next random number in the sequence.
class LCG:
    def __init__(self, seed, a, c, m):
        self.state = seed
        self.a = a
        self.c = c
        self.m = m

    def next(self):
        self.state = (self.a * self.state + self.c) % self.m + 13
        return self.state

def find_seed(a, c, m, intercepted_numbers):
    for seed in range(m):
        lcg = LCG(seed, a, c, m)
        if all(lcg.next() == num for num in intercepted_numbers):
            return seed
    return None

def test_rng():
    intercepted_numbers = [7424, 2729, 2104]

    a = 17
    c = 3212
    m = 7919

    seed = find_seed(a, c, m, intercepted_numbers)

    if seed is not None:
        lcg = LCG(seed, a, c, m)
        # Advance the generator to the fourth number
        for i in range(12345):
            print(i, lcg.next())

        next_number = lcg.next()
        print(f"Next predicted number: {next_number}")
    else:
        print("Seed not found")

test_rng()

Generate Problem

#include <stdio.h>
#include <stdint.h>

#define A 17
#define C 3212
#define M 7919

uint32_t custom_rng(uint32_t *state) {
    *state = (*state * A + C) % M + 13;
    return *state;
}

int main() {
    uint32_t seed = 247;
    uint32_t state = seed;

    printf("Random number 1: %u\n", custom_rng(&state));
    printf("Random number 2: %u\n", custom_rng(&state));
    printf("Random number 3: %u\n", custom_rng(&state));
    return 0;
}

Web: 1. Taxi driver

Find the flag from the server

Server

package main

import (
    "fmt"
    "math/rand"
    "strconv"
    "time"

    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/limiter"
    "github.com/google/uuid"
)

var problems = make(map[string]string)
var answers = make(map[string]int)
var adj = make(map[string]string)

func generateProblem() (string, int) {
    rand.Seed(time.Now().UnixNano())
    num1 := rand.Intn(100000)
    num2 := rand.Intn(100000)
    answer := num1 + num2
    problem := fmt.Sprintf("%d + %d", num1, num2)
    return problem, answer
}

func handleAnswer(c *fiber.Ctx) error {
    id := c.Params("id")
    answer, exists := answers[id]
    if !exists {
        return c.SendStatus(fiber.StatusNotFound)
    }
    if answer == -1 {
        return c.SendString("flag{U_TALKING_TO_ME!!!}")
    }

    userAnswer := c.FormValue("answer")
    userAnswerInt, err := strconv.Atoi(userAnswer)
    if err != nil || userAnswerInt != answer {
        return c.SendStatus(fiber.StatusForbidden)
    }

    return c.SendString(fmt.Sprintf("/path/%s", adj[id]))
}

func handleDynamicEndpoint(c *fiber.Ctx) error {
    id := c.Params("id")
    problem, exists := problems[id]
    if !exists {
        return c.SendStatus(fiber.StatusNotFound)
    }
    if problem == "found" {
        return c.SendString("flag{U_TALKING_TO_ME!!!}")
    }
    return c.SendString(fmt.Sprintf("Problem: %s\nSubmit answer to /path/%s", problem, id))
}

func main() {
    app := fiber.New()

    app.Use(limiter.New(limiter.Config{
        Max:        300,
        Expiration: 1 * time.Second,
        KeyGenerator: func(c *fiber.Ctx) string {
            return c.IP() // Limit based on the client's IP
        },
        LimitReached: func(c *fiber.Ctx) error {
            return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
                "message": "Too many requests, please try again later.",
            })
        },
    }))

    current := ""
    for i := 0; i < 300; i++ {
        problem, answer := generateProblem()
        problems[current] = problem
        answers[current] = answer
        next := uuid.New().String()
        adj[current] = next
        current = next
    }
    fmt.Println(current)
    problems[current] = "found"

    app.Post("/path", handleAnswer)
    app.Get("/path", handleDynamicEndpoint)
    app.Post("/path/:id", handleAnswer)
    app.Get("/path/:id", handleDynamicEndpoint)

    app.Listen(":3000")
}

Solution

import requests
import re
import time

def extract_problem_and_answer(response_text):
    problem = re.search(r'Problem: (\d+) \+ (\d+)', response_text)
    if problem:
        num1 = int(problem.group(1))
        num2 = int(problem.group(2))
        answer = num1 + num2
        return answer
    return None

def main():
    base_url = "https://<url>"
    current_endpoint = "/path"

    s = 1/300
    i = 0
    while True:
        i += 1
        response = requests.get(base_url + current_endpoint)
        if response.status_code != 200:
            print(f"Error: {response.status_code}")
            break

        response_text = response.text
        print(f"Received: {response_text}")

        answer = extract_problem_and_answer(response_text)
        if answer is None:
            print("Failed to extract problem or calculate answer.")
            break

        time.sleep(s)
        post_response = requests.post(base_url + current_endpoint, data={"answer": answer})
        if post_response.status_code != 200:
            print(f"Error: {post_response.status_code}")
            break

        post_response_text = post_response.text
        print(f"Next response: {post_response_text} {i}")

        current_endpoint = post_response_text

        if "flag" in post_response_text.lower():
            print(f"Flag found: {post_response_text}")
            break
        time.sleep(s)

if __name__ == "__main__":
    main()

Web 2: Inception

Find the flag from the server

Server

# flag{minio_doesnt_pay_me}
from flask import Flask, request, send_from_directory, render_template
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import os

app = Flask(__name__)
upload_folder = './files'
app.config['UPLOAD_FOLDER'] = upload_folder
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 5MB

limiter = Limiter(
    get_remote_address,
    app=app,
    default_limits=["100 per minute"]
)

@app.route('/')
def index():
    return render_template('./index.html')

@app.route('/upload', methods=['POST'])
@limiter.limit("2 per minute")
def upload_file():
    if 'file' not in request.files:
        return 'No file part', 400
    file = request.files['file']
    if file.filename == '':
        return 'No selected file', 400
    if request.content_length > 1024 * 1024: # 1MB
        return f'File too large. Max size allowed is 1 MB', 413

    file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
    file.save(file_path)
    return f'File uploaded successfully! file path: {file_path}', 200

@app.route('/files', methods=['GET'])
@limiter.limit("2 per second")
def include_file():
    filename = request.args.get('file')
    if filename:
        file_path = os.path.join("./", filename)
        print(file_path)

        try:
            return send_from_directory("./", filename)
        except Exception as e:
            print(e)
    return 'File not found', 404

if __name__ == '__main__':
    app.run(host="0.0.0.0", debug=True)

Solution

curl https://<url>/files?file=files/../server.py