Intro

Ажил дээр CTF-н тэмцээн саяхан болсон ба тэрхүү тэмцээний зохион байгуулах багт орон хэдэн бодлого дэвшүүлж үзлээ. CTF-н тухай ойлголт бага байсан ч сонирхолтой байлаа. Миний хувьд 4 төрлийн бодлого дэвшүүлэх гэж оролдсон ба тэр тухайгаа бичлээ. Гэснээс тухайн бодлогын нэртэй кинонуудыг үзээд үзээрэй

Cryptography

Cryptography-д надад сонирхолтой санагддаг ба энэхүү сэдвээр бодлого дэвшүүлэх хамгийн сонирхолтой нь байлаа. Ерөнхийдөө 2 бодлогын санаа байсан ба

  1. SSH Public Key Authentication

Бүгд л ssh public key authentication ашигладаг эсвэл ашиглаж үзсэн байх. Тэгвэл боломжийг нь шалгаад тайлах боломжтой юу? гэдэг санаан дээр тулгуурласан. Бодит байдал дээр бол 2048bit-н хэмжээтэйгээр түлхүүр нь үүсдэг ба энэ нь тайлах боломжгүй гэж хэлж болно. Харин бага хэмжээтэй тохиолдолд үүнийг тайлах боломжтой. Гэхдээ үүнийг зохиоход нэг асуудал гарсан нь OpenSSH-нь хамгийн багадаа 1024b хэмжээтэй байх ёстой гэсэн ба энэ нь хэтэрхий том. Хуучин хувилбарыг ашиглах гэж үзсэн боловч тийм ч их хэмжээ нь багасаагүй. Тиймээс энэ бодлогыг хувиргаад publicKey.pem өгөгдөхөд хэрхэн нууц захиаг тайлах вэ гэсэн бодлого болсон.

  1. Group message encryption

Групп чат хэрхэн encryption хийдэг вэ? Ихэнх нь дундаа нэг түлхүүр ашигладаг ба тэр түлхүүрийг хаана хэн яаж үүсгэх нь их сонин. Тэгвэл үүнээс сэдэвлэн Merkle мод ашиглан түлхүүрийг байгуулна. Шинэ гишүүн орох, хуучин гишүүн гарах үед тухайн модыг шинэчлэх нь энэ тохиолдолд хурдан. Ер нь энэ санаа их харагддаг. Хэрэглэгч бүр өөрийн нууцыг тухайн модонд оруулснаар бүх гишүүдээс хамааралтай нууц түлхүүр гарснаар үүнийгээ encrypt, decrypt хийхэд ашиглаж болно. Тэгвэл энгийн алгоритм байгаа тохиолдолд хэрхэн чатыг унших боломжтой вэ гэсэн бодлого болсон.

Reverse

Reverse-н тухайд надад туршлага байхгүй байсан ч манай гаргийн хэкэр найз byamb4-тай ярилцсаны дараа 2тын кодын тухай оруулахаар болсон

C, Go гэх мэт хэл нь compile хийсний дараа binary гаргаж өгдөг ба энэ нь шууд ажиллах боломжтой гэсэн үг билээ. Тэгвэл энэхүү машин кодоос анхны code-г гаргах боломжтой юу? гэсэн ерөнхий сэдвийн хүрээнд ба энэ нь нэлээн төвөгтэй билээ.

Эхний бодлогын хувьд salt, hash дээр суурилан гарсан ба нууц үгийг давс нэмээд hash-алдаг 2тын код өгөгдсөн бол тухайн hash-с анхны нууц үгийг олох юм. Salt нь static тэмдэгт мөр тул энэ нь харьцангуй олоход амархан байхаар хийсэн.

Дараагийн бодлогын хувьд арай хүнд ба энэ нь багадаа үзэж байсан нэг киноноос сэдэвлэсэн. Тухайн кино дээр нууц үг нь 15мин тутамд шинэчлэгддэг ба аль болох random байх шаардлагатай гэж гардаг. Тэгвэл энэ удаа 2 тал нэгэн программ солилцсон ба энэ нь нэг удаагийн нууц үгийг үүсгэдэг. Энэхүү программын гол хэсэг нь байгаа бол одоо байгаа нууц үгийг олох ёстой.

Forensic

Эндээр ярилцаад 2 бодлого дэвшүүлсэн ба

  1. Disk

Файлыг диск дээр хэрхэн хадгалдаг вэ? Бас файл устгалаа гэхэд тэрийг дискээс арчдаг уу гэвэл ихэнх үед үгүй ба учир нь залхуу байснаар энерги хэмнэнэ. Тэгвэл disk image өгөгдсөн ба тухайн диск дээр олон файл устгасан ба тухайн файлуудыг сэргээж тугийг олох ёстой.

  1. File таних

Яаж бид ер нь бичлэг, дуу эсвэл зураг гэдгийг ялгадаг вэ? Энэ нь мэдээж файлын сүүлийн хэсэг буюу extension-с шууд харж болдог? гэхдээ энэ нь худлаа эсвэл төр төрөл нь байхгүй бол яах вэ? Энэхүү даалгавар дээр base64 текст байгаа ба үүнийг 2тын файл болговол zip file байгаа. үүнийг задалбал zip, tar, gzip, 7z гэх мэт боломжуудаас гарч ирэх ба үүнийг нь таних гэсэн санаатай.

Web

Веб нь надад хамгийн ойрхон сэдэв байсан ч бодлого зохиоход хамгийн хэцүү нь байсан. Учир нь ерөнхийдөө ийм алдаа хүмүүс хийхгүйдээ гэж бодоод байгаа болохоор амьдралд ойр жишээ оруулмаар байсан болохоор байхаадаа. Тиймээс URL injection болон automation шаардсан 2 энгийн даалгавар оруулсан.

Дүгнэлт

CTF-н бодлого зохиох нь үнэхээр сонирхолтой байлаа. Санал зөвлөгөө өгсөн byamb4-даа баярлалаа. Инженер хүний хувьд хаана ямар асуудал байж болох вэ гэдгийг мэддэг байх нь хамгийн чухал чадваруудын нэг билээ. Тиймээс CTF-н даалгаврууд хийснээр эдгээр зүйлийг таних, олох чадвал илүү нэмэгдэх нь тодорхой биз ээ. Доор бодлогууд болон түүнийг хэрхэн гаргасан кодыг хуваалцлаа.

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