Since I got top 3 for SCTF and they required write-ups as proof for a couple of challenges, why not post them here as well?
Josh Template Viewer
This challenge was honestly pretty easy. After creating an account and logging in, we are directed to the following page:

A look at the code also tells us that the website uses JSON Web Tokens for handling authentication and authorization (signed using a presumably weak secret key), and that the homepage would show the flag if the uuid
field in the JWT payload was set to '0'
:
@app.route('/')def index(): try: if 'access_token' in request.cookies: access = jwt.decode(request.cookies['access_token'], app.config['SECRET'], algorithms=['HS256']) return render_template('home.html', access=access)
else: return render_template('index.html')
except: return render_template('error.html')
<div class="card"> {{ access }} <div class="card-body"> <h5 class="card-title">User Details</h5> {% if access.uuid == '0' %} <p class="card-text">UUID: sctf{REDACTED}</p> {% else %} <p class="card-text">Username: {{ access.username }}</p> {% endif %} </div></div>
Decoding the JWT using jwt.io also confirms the above:

Let’s grab the JWT from the access_token
cookie and try brute forcing it with John and rockyou.txt
:
$ echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiNTkzNWJiYTktMzdiOS00MjE2LTlmMGUtNGU3MGUwOTRjYzU2IiwidXNlcm5hbWUiOiJhIn0.1b86xVhe9u_KB8Vw2pEgGizfSlX7HR5hJQpNSnHr1l4 > jwt.txt$ john jwt.txt --format=HMAC-SHA256 --wordlist=/usr/share/wordlists/rockyou.txt
No dice. Let’s try again with the default wordlist:
$ john jwt.txt --format=HMAC-SHA256
I wasn’t sure how long this was gonna take, so I hopped off to play a match of Helldivers 2. When I came back a good 40 minutes later (I’m bad at keeping track of time ok I’m sorry), I was greeted with the following:
$ john jwt.txt --format=HMAC-SHA256 --pot=./john.potUsing default input encoding: UTF-8Loaded 1 password hash (HMAC-SHA256 [password is key, SHA256 128/128 AVX 4x])Will run 12 OpenMP threadsProceeding with single, rules:SinglePress 'q' or Ctrl-C to abort, almost any other key for statusAlmost done: Processing the remaining buffered candidate passwords, if any.Proceeding with wordlist:/usr/share/john/password.lst, rules:WordlistProceeding with incremental:ASCIIhehehaw (?)1g 0:00:01:04 DONE 3/3 (2024-03-10 13:22) 0.01560g/s 10668Kp/s 10668Kc/s 10668KC/s heenjez..hebina8Use the "--show" option to display all of the cracked passwords reliablySession completed
Awesome! Now that we have the secret, we can now forge our own JWT and set the uuid
field to '0'
:

Setting the access_token
cookie to the forged JWT and refreshing the page gives us the flag:

Note: I don’t think brute forcing the JWT Secret was the intended solution and that it was supposed to be path traversal, but hey if it works I’m not complaining.
BeepBoop
For some reason, this challenge felt way easier than Running Can. This challenge provides beepboop.zip
, which contains 2 audio files: beep.wav
and boop.wav
. The zip file is password-protected, but can be easily cracked with John the Ripper:
$ zip2john beepboop.zip > beepboop.hash$ john beepboop.hash --wordlist=/usr/share/wordlists/rockyou.txt
This tells shows that the password is bestmusic
(shocker). The file can now be unzipped:
$ unzip beepboop.zip
This gives us the two audio files. Running strings
and binwalk
on both files yield nothing interesting, so let’s open up Sonic Visualiser. Adding a spectrogram and tweaking the Window options shows the first part of the flag:

The second part of the flag can be found encoded in boop.wav
using LSB steganography, and can be extracted using the following script I got off a Medium article:
import wavesong = wave.open("boop.wav", mode='rb')# Convert audio to byte arrayframe_bytes = bytearray(list(song.readframes(song.getnframes())))
# Extract the LSB of each byteextracted = [frame_bytes[i] & 1 for i in range(len(frame_bytes))]# Convert byte array back to stringstring = "".join(chr(int("".join(map(str,extracted[i:i+8])),2)) for i in range(0,len(extracted),8))# Cut off at the filler charactersdecoded = string.split("###")[0]
# Print the extracted textprint("Sucessfully decoded: "+decoded)song.close()