BSides Tampa 2025

As May 17, 2025 came to an end another iteration of the BSides Tampa conference had ended. I'm always amazed how a great group of people (The ISC2 Tampa chapter) can put on an event each year raising the bar each time.
One of these years I hope for my talk to be accepted, so this year I was just attending as a regular attendee excited to hear some great talks.
One thing I haven't really focused too much is the challenge that exists on each and every badge given. This year I wanted to spend some time attempting to solve the challenge, but pretty quickly I ran into an Xbox friend from a long time ago and we caught up on some stories instead. Always a great time to catch up with someone from the era of Halo 2-3, Xbox, Xbox 360 and all the drama that occurred during that era.
That is the benefit of these conferences - they are built for everyone to take their own track through them. You could attend a talk for every hour and head home full of knowledge. You could experiment with the vendor hall, the villages or various challenges throughout the event. You could socialize with folks and network around. You could take part in any or all of the 3 different CTFs present. You could grab lunch from one of the 4 different lunch options present. The point saying that everyone can schedule out the day to fit their preference from a large variety of things.
So once I got home I decided to take the puzzle for a spin and wanted to document it for others who might be confused on how these puzzles work.

Each attendee got the badge above which when attached with a battery had some light up LEDs to brighten the badge. As you scan the board you can see some text hiding right before the bells that looks a bit like gibberish.
bWlkbulnaHQuY29kZXMK
That text was above and pretty quickly an obvious first check to do is base64 decoding.
➜ echo "bWlkbulnaHQuY29kZXMK" | base64 -d
midn?ght.codes
This resulted in a domain that decoded to some Unicode spelling, but pretty quickly this was obviously "midn[i]ght.codes". The first option was "EASY" or "HARD" and this being my first badge challenge I took the easy route.

However, this was not my first CTF so it made sense what was going on pretty quickly. The pattern had started to evolve that each page had the hint of the flag at top of like - ./________.____
, then the hint below. So you knew exactly what format you were trying to obtain which went into the URL.
The first hint was eharpbqr.ugzy
which after doing enough Cicada 3301 puzzles is clearly some iteration of .html
so lets just apply every single ROT cipher (0-25) and see if anything makes this readable.
ROT-12: qtmdbncd.gslk
ROT-13: runecode.html
ROT-14: svofdpef.iunm
Sure enough ROT-13 spelled out the domain and we were on our way. The next puzzle just had an image dropped in front of us.

With any image the first thing I do is head to FotoForensics and upload it for investigation. It didn't take long exploring the tabs of this tool to find the flag was injecting directly into the image data.

Stage 3 offered a password prompt, but as I entered various characters I saw no network calls go outbound. So this code had to be client side and it didn't take long to find it.
const encoded = "637962657274656d706c65";
function encode(str) {
return Array.from(str)
.map(c => c.charCodeAt(0).toString(16).padStart(2, '0'))
.join('');
}
function checkPassword() {
const input = document.getElementById('password').value;
const inputHex = encode(input);
const match = inputHex === encoded;
document.getElementById('result').textContent = match ? "success" : "failure";
}
Our input is encoded and compared to a hash, so we just have to reverse this hash to identify the password which according to the hint is ./___________.html
We can tell our input is converted into the value of the ASCII character then converted to hex (padded to 2 characters).
In the era of AI, I could write a script to explode that string into pairs, undo the hex to code point to character, but also AI could as well. So I gave it the original function and asked AI to reverse it in 1 line.
(hexString => [...Array(hexString.length/2)].map((_, i) => String.fromCharCode(parseInt(hexString.substr(i*2, 2), 16))).join(''))("637962657274656d706c65")
Sure enough a little sample script that resulted in cybertemple
and we were on to stage 4.
A story with no beginning is hard to read...
DOWNLOAD
I'm not sure if this was supposed to be harder, but I opened up the PDF and the flag was printed in plain text in it.
./nullobelisk.html
Stage 5 offered a hint of bGFuZ2lzY2lsZXIK
, but the .html
was already known.
➜ echo "bGFuZ2lzY2lsZXIK" | base64 -d
langisciler
With another simple test of base64 we had a human readable string that wasn't right. However, I don't think "langisciler" is a real word. However reading it backwards was "relic" so I just applied rev
and ran it again.
➜ echo "bGFuZ2lzY2lsZXIK" | base64 -d | rev
relicsignal
This led us to stage 6 which just offered an SVG image and a hint. I intentionally changed the colors closer to white to make it more visible.

While on that journey to brighten up this image I thought a chunk of the SVG was intentionally hidden or the password was hidden among a layer. A by-product of wanting to brighten the image led me to discovering the flag.

As I was sitting there brightening the colors I saw two properties that my browser claimed were invalid (stroke-start
and start-end
). The values for both of those seemed like they were telling a story of hexmonolith
- which had no such luck of working.
I encoded monolith
into hex and tried a few other iterations, but then I tried removing /easy
from the URL and put hexmonolith
at the root and it worked. Had I just looked at the hint at the top of the page that had ../
(go back 1 directory) I would have known that, but I was blind to that aspect after a few stages of this.
I guess the easy and hard parts had connected based on that change. Now stage 7 was asking me to download a file called EchoChamber.zip
The truth is buried in layers.
Unwrap it carefully...
DOWNLOAD
However of course it wouldn't be that easy and the .zip
file had a password on it. It seemed the flag was inside so I just needed the password. I went back to the HTML page and saw a little HTML comment hiding on the page.
<!-- 4*xEhJo -->
Sure enough that password worked and we were on our way to stage 8.
➜ unzip EchoChamber.zip -d Stage8
Archive: EchoChamber.zip
[EchoChamber.zip] Layer1.zip password:
extracting: Stage8/Layer1.zip
extracting: Stage8/flag.txt
inflating: Stage8/log.txt
inflating: Stage8/log copy.txt
With another zip file and associated files, I knew the flag was going to be false and it was.
➜ cat flag.txt
Nice try; but you're not done yet.%
This time we had 2 large files called log.txt
and log_copy.txt
and I wondered what made them different. So I opened up Araxis Merge to get a diff report and sure enough there was 1 tiny change between the files.

This differences between the files were:
log.txt
-ВΤо𝟛еΙυ
log copy.txt
-BTo3eIu
It seemed the plaintext password was the password. This led to once again another layer.
➜ Stage8 unzip Layer1.zip -d Stage9
Archive: Layer1.zip
[Layer1.zip] flag.txt password:
extracting: Stage9/flag.txt
extracting: Stage9/Layer2.zip
inflating: Stage9/communications-extraterrestrial-intelligence.pdf
Before I even investigated the .pdf
file I listed out the next zip file and some random text spewed out into my console.
➜ unzip -l Layer2.zip
Archive: Layer2.zip
V2+m^o6
Length Date Time Name
--------- ---------- ----- ----
23 05-06-2025 23:22 flag.txt
778106 05-06-2025 23:02 Layer3.zip
2303 05-06-2025 23:20 d867ffd.eml
--------- -------
780432 3 files
I wondered if those characters were the password and funny enough - it was. So now I was onto stage 10 with a new set of files and another false flag. This time we had an email that appeared to be someone requesting the password from IT again.

Nothing looked obvious in the email, so I dumped the headers of the email to investigate the more hidden aspects of it.
Return-Path: <diane.mcpherson@cryptforge.local>
Received: from mail.cryptforge.local (mail.cryptforge.local [192.168.1.50])
by smtp.cryptforge.local (Postfix) with ESMTP id 9C2A3123456
for <it-support@cryptforge.local>; Mon, 06 May 2025 08:14:35 -0400 (EDT)
Date: Mon, 06 May 2025 08:14:33 -0400
From: Diane McPherson <diane.mcpherson@cryptforge.local>
To: IT Support <it-support@cryptforge.local>
Subject: Re: Password for Layer3.zip (again...)
Message-ID: <20250506081433.12345@mail.cryptforge.local>
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_Part_9847_482132571.1714996473000"
X-Mailer: CryptForgeMail 2.1
X-L3Z-Encoded: 4734652637407a
X-User-Complaint-Level: 2
X-Meta-Tag: routine follow-up
X-Auth-Check: passed
There was a lot of data here that seemed like it was possible to be hiding a secret, but this X-L3Z-Encoded
stood out. It wasn't base64 and since I didn't see any letter outside of a-f
I figured it was hex.
Thankfully still had xxd
memorized from the Cicada 3301 puzzles so another quick command to turn this back into ASCII.
➜ echo "4734652637407a" | xxd -r -p
G4e&7@z%
This indeed was the password and unzipped once again another layer that I referred to as stage 11.
➜ unzip Layer3.zip -d Stage11
Archive: Layer3.zip
[Layer3.zip] flag.txt password:
extracting: Stage11/flag.txt
extracting: Stage11/Layer4.zip
inflating: Stage11/logo.jpeg
This was the same deal with another fake flag and this time an image (the image you see as the featured image in this blog). Like the last image I opened it up in FotoForensics and went exploring.

This didn't take long to see the embedded exif data for "Comment", which was the password to another layer.
➜ Stage11 unzip Layer4.zip -d Stage12
Archive: Layer4.zip
[Layer4.zip] flag.txt password:
extracting: Stage12/flag.txt
extracting: Stage12/Layer5.zip
It seemed the puzzle was continuing, but the flag contained a URL that led to the end of the puzzle. I wondered if the Layer5 zip had another direction of the puzzle, but the flag.txt
was the same size outside and inside the zip. I figured since the puzzle ended with the URL contained in flag.txt
that I was done.

I probably should have picked the hard option, but that was my first badge CTF ever so I wasn't sure what to expect. There was also a DnD and regular CTF at this event so 3 unique different CTFs at once.
I enjoy making CTFs as much as doing them so maybe next year I can help out making one that increases in difficulty while guiding all levels of attendees to attempt it. I made one CTF way back in 2021 for work for all engineers to attempt, so it has been a solid few years since I've made one.
I'm excited every year for BSides and we have BSides Tampa, BSides Orlando and BSides St.Pete all within driving distance of myself. Too bad this year I'll be out of town for both St.Pete & Orlando.