️🏴 MetaCTF November 2024 Flash CTF

A monthly mini CTF competition organized by MetaCTF and Antisyphon.

2024-11-21

#ctf

A monthly mini CTF competition organized by MetaCTF with support from Antisyphon Training and TCM Security. There will be 5 challenges and you will have 2 hours to solve as many as you can. This is a beginner-friendly CTF.

Details here: https://app.metactf.com/join/sep2024.

Slithering Security

Help me test my sssecurity, can you get the flag from this ssssecure sssscript?

Download the challenge file here.

The challenge's code is pretty brief:

#!/usr/bin/env python3

SECRET_FLAG=b"\x54\x57\x56\x30\x59\x55\x4e\x55\x52\x6e\x74\x6b\x4d\x47\x34\x33\x58\x7a\x64\x79\x64\x58\x4d\x33\x58\x32\x4e\x73\x4d\x57\x34\x33\x63\x31\x39\x33\x61\x54\x64\x6f\x58\x33\x4d\x7a\x59\x33\x49\x7a\x4e\x33\x4e\x7a\x63\x33\x4e\x7a\x63\x33\x4e\x39"
HASHED_PASSWORD = b'\x12\x1eW\x98\x00\xc1C\xff\xe3\xa9\x15\xde\xd9\x00\x9b\xc9'

from base64 import b64decode
from hashlib import md5

def check_password(password):
    m = md5()
    m.update(password)
    return m.digest() == HASHED_PASSWORD

def main():
    while True:
        inp = input("Please enter your passssssword: ").encode()
        if check_password(inp):
            print(f"Well done, your flag isssssss {b64decode(SECRET_FLAG).decode()}")
            exit()
        else:
            print("Passsssssword incorrect, please try again.")

if __name__ == "__main__":
    main()

The trick is in the check, insofar as it's pointless:

if check_password(inp):
    print(f"Well done, your flag isssssss {b64decode(SECRET_FLAG).decode()}")

If the password is correct, SECRET_FLAG is base64-decoded. That's it.

>>> import base64
>>> print(base64.b64decode(SECRET_FLAG).decode())
MetaCTF{d0n7_7rus7_cl1n7s_wi7h_s3cr37sssssss}

Admin Portal

I'm writing a webpage for admins to check on their flags, can you do me a favor and check it out to make sure there aren't any issues?

Check out the website here.

The site proclaims, in red letters no less:

"Access denied. This page is only available by administrators."

The site is pretty minimalist with no obvious means to authenticate. However, in the HTTP headers:

 curl http://adminportal.chals.mctf.io/ \
     --include \
     --head
HTTP/1.1 200 OK
Date: Fri, 22 Nov 2024 22:07:06 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 983
Connection: keep-alive
Set-Cookie: role=user; Path=/

…the role is set via a cookie:

 curl http://adminportal.chals.mctf.io/ \
     --include \
     --silent \
     --header "Cookie: role=admin" | rg flag
      <p>Your flag is: <b>MetaCTF{co0ki3_p0wer3d_p0rt4l}</b></p>

Steg64

You've heard of Base64, but I present to you Steg64!

Download the challenge file here.

The file appears to contain a series of base64-encoded lines:

 head steg64.txt
VEh=
QT==
Tl==
S5==
IF==
Wd==
Tx==
VY==
IF==
SA==

…but when decoded, that's not apparently all that's required:

 while read i
 do
     echo -n ${i} | base64 -d
 done < steg64.txt
THANK YOU HACKER!

BUT OUR FLAG IS IN ANOTHER CASTLE

Damn. That's all there is to the challenge: base64 and a file…right?

>>> base64.b64decode(b"VEh=")
b'TH'
>>> base64.b64encode(b"TH")
b'VEg='

Errr…what?

The bits for TH look like this:

>>> " ".join(f"{ord(c):b}".zfill(8) for c in "TH")
'01010100 01001000'

Breaking that down to the 6 bytes for each character in a base64-encoded string:

>>> bits = "".join(f"{ord(c):b}".zfill(8) for c in "TH")
>>> [bits[i:i+6] for i in range(0, len(bits), 6)]
['010101', '000100', '1000']

Okay, padding that last binary string to its required 6 bits, then converting it to an ASCII characters results in:

>>> import string
>>>
>>> chars = string.ascii_letters + "+/"
>>> [chars[int(bits[i:i+6].ljust(6, "0"), 2)] for i in range(0, len(bits), 6)]
['v', 'e', 'G']

…the wrong answer. Quick fix:

>>> chars = string.ascii_uppercase + string.ascii_lowercase + string.digits + "+/"
>>> [chars[int(bits[i:i+6].ljust(6, "0"), 2)] for i in range(0, len(bits), 6)]
['V', 'E', 'g']

…and that demonstrates the issue: TH encodes to VEg but something has been added in the two bits that are used to pad that last sextet of bits, specifically 01:

>>> [chars[int(i, 2)] for i in ['010101', '000100', '100001']]
['V', 'E', 'h']

But "base64" itself doesn't care:

>>> base64.b64decode(b"VEg=")
b'TH'
>>> base64.b64decode(b"VEh=")
b'TH'

Oh, this is brilliant.

For every line in the steg64.txt file, of interest are the base64-decoding of that line.

Originally, I thought that the delta between that and the encoded version of that decoding was key. However, those extra bits can be derived purely based on knowing the number of padded bits:

decoded = base64.b64decode(line)

Ignoring the = padding characters, converting the characters from each input line back to their positions in the list of permissible base64 characters gives each sextet of bytes:

challenge_bits = "".join(f"{chars.index(c):06b}" for c in line.rstrip(b"="))

The extra bits can then be determined based purely on expanding those sextets back into bytes:

offset = (len(challenge_bits) // 8) * 8
bits = challenge_bits[offset:]

Putting that all together:

def get_extra_bits(line: bytes) -> str:
    decoded = base64.b64decode(line)
    challenge_bits = "".join(f"{chars.index(c):06b}" for c in line.rstrip(b"="))
    offset = (len(challenge_bits) // 8) * 8
    return challenge_bits[offset:]


with open("steg64.txt", "rb") as i:
    steg = i.read().strip().splitlines()

bits = "".join(map(get_extra_bits, steg))

…which gives:

>>> print("".join(chr(int(bits[i:i+8], 2)) for i in range(0, len(bits), 8)))
MetaCTF{4_f3w_3xtr4_b1t5}

Talk To Me

We managed to tap the headphones of a member of a prolific cyber actor group. Can you listen to their secret plan?

Download the wiretap here.

According to Wireshark, that pcap file is a capture of USB isochronous packets.

So that's a thing.

While looking for a way to dump the data via Wireshark, I came across this writeup of another CTF which presents a solution; modifying that slightly:

 tshark -T fields -e usb.iso.data -r talktome.pcap > talktome.hex

The output is oddly comma-separated hex-lines. Converting those to raw bytes:

with open("talktome.hex") as i, open("talktome.bin", "wb") as o:
    for line in i:
        o.write(bytes.fromhex(line.strip().replace(",", "")))

…and importing that as "raw data" into Audacity, mostly with defaults but some tweaks after some trial and error:

…yields and voice speaking far too quickly and saying something along the lines of:

"em-ee-tee-ey-see-tee-eff-curly-bracket-four-underscore-ell-one-ton-tee-ell…"

Playing around with playback speeds and enjoying the goofy baritone, I think it's spelling (although there's some educated guessing as "one tonne tee" doesn't quite make sense):

MetaCTF{4_l1ttl3_b1rd_t0ld_m3}

Pikalang

I just heard about this cool new esoteric programming language called Pikalang!

It has no silly strcpy()'s or heaps, so it must be *super effective* against hackers.

Download my interpreter here!

If you'd like to try running some Pikalang code, do so here: nc kubenode.mctf.io 30013

Neither Pikalang nor it's predecessor Brainfuck have IO capabilities, as far as I know so this has to be something in the provided pikalang.bin interpreter:

int32_t main(int32_t argc, char** argv, char** envp)
{
    setvbuf(stdin, nullptr, 2, 0);
    setvbuf(__TMC_END__, nullptr, 2, 0);
    setvbuf(stderr, nullptr, 2, 0);
    
    while (true)
    {
        ptr = &tape;
        memset(&program, 0, 0x10000);
        memset(&tape, 0, _init);
        puts("Enter a Pikalang program (or 'ex…");

The interpreter is essentially a series of if statements which manipulate the ptr:

    while (*(arg1 + var_80024_1))
    {
        int32_t var_80024_2;
        
        if (strncmp(arg1 + var_80024_1, "pipi", 4) || (*(arg1 + var_80024_1 + 4) != 0x20 && *(arg1 + var_80024_1 + 4) != 0xa && *(arg1 + var_80024_1 + 4)))
        {
            if (strncmp(arg1 + var_80024_1, "pichu", 5) || (*(arg1 + var_80024_1 + 5) != 0x20 && *(arg1 + var_80024_1 + 5) != 0xa && *(arg1 + var_80024_1 + 5)))
            {
            }
            else
                {
                    uint64_t ptr_1 = ptr;
                    *ptr_1 += 1;
                    var_80024_2 = var_80024_1 + 2;
                }
            }
            else
            {
                ptr -= 1;
                var_80024_2 = var_80024_1 + 5;
            }
        }
        else
        {
            ptr += 1;
            var_80024_2 = var_80024_1 + 4;
        }
        
        var_80024_1 = var_80024_2 + 1;
    }

Presumably, via manipulation of the pointer, the idea is to reference the position of system() and obtain RCE to grab the flag.

Alas, I'm admitting defeat on this one. For now.