π²DiceCTF 2024
My writeups, aimed to be more descriptive for beginners.
Last updated
My writeups, aimed to be more descriptive for beginners.
Last updated
5th weekend in a row doing CTF. Started feeling burned out so took a break in my place progression and just attempted the problems with the most solves.
Author: NotDeGhost70
Follow the leader.
This is a simple web game where you move the dice with WASD and you need to catch the goose (the black block). I couldn't move right for some reason in firefox, works fine in chromium. Looking through the code, there's the following line:
if (score === 9) log("flag: dice{pr0_duck_gam3r_" + encode(history) + "}");
This line means if the goose can be caught in 9 moves, then the flag will be printed. Typing "history", "encode(history)", etc. in the console will give an idea of what this means, but basically it will be a base64-encoded set of positions.
The dice starts at the top of the wall which is 9 blocks tall, and the goose starts 8 blocks to the right (although the dice moves first). So if the goose was controlled to move to the left every time, that would result in the desired score. The decision is controlled by a random value generated for the switch statement, so replacing it with a hard-coded value is all that's needed for the solve.
After winning with a score of 9, the flag is printed in the console:
flag: dice{pr0_duck_gam3r_AAEJCQEBCQgCAQkHAwEJBgQBCQUFAQkEBgEJAwcBCQIIAQkB}
Author: strellic
can you login as admin?
NOTE: no bruteforcing is required for this challenge! please do not bruteforce the challenge.
We're given a login page and the source code for the challenge, where the goal is to log in as an admin to see the flag. 100,000 accounts are generated with secure random usernames and passwords, and one of them is selected at random to be an admin.
I built and ran the docker image to add debug prints to learn exactly what was happening, see below:
After understanding what's happening, the next step was to try various SQL injection payloads. One that ended up working was:
' UNION SELECT 1--
This could be added to either the username or password field, and the number would be what "id" gets set to. It doesn't really matter what we set the id to as long as it's between 1-99,999 inclusive to pass the first check, if users[id] exists. The final check to pass in order to receive the flag is to have isAdmin[user] return true. If we put the injection in the password field, we have full control over what "user" is. I got stuck here for a long time, but finally found __proto__
which seems to be evaluated and return as true in the javascript. Using this as the login, we get the flag!
dice{i_l0ve_java5cript!}
Author: larry122
i made a ejs renderer, its 100% hack proof im using gpt to check all your queries!
please note that the backend for this challenge is not the same as the one given to avoid leaking our API key, but the queries to the OpenAI API are exactly the same.
We're given a link to create an instance (lasts 10 minutes, 30 second submit lockout, requires captcha) that will let us submit an ejs template to be executed if GPT 3.5 doesn't think it's a hacking attempt. It's not really feasible to solve this on the live instance since even after getting past GPT, the template still has to be valid.
Therefore, the first step was to strip out the GPT checking and run locally.
Note the setting of result to 'R', which is what we want GPT to return. I didn't bother with building the Docker image, I just removed openai from package.json, ran npm i
to install ejs, and then ran node index_mod.js
to access the app at localhost:8080.
Trying to add a require to use fs to access the file (goal is /file.txt) doesn't work, but with some googling, a template that ends up working is <%= include ('/flag.txt') %>. Note both the parentheses and quotes, also when I was testing locally, a static path is necessary so I was testing with a static path to the dummy flag.txt in the same directory.
Unfortunately, as expected, this is detected as a hacking attempt by GPT, so we need to slowly obfuscate this include method until GPT no longer marks it as a hacking attempt. I got to the following point with no luck:
The base64 is just /flag.txt encoded. I think I tried renaming encodedPath as well but it still wasn't working.
I then remembered that GPT seems to like following instructions in comments. Adding the following line at the top gets GPT to let it through.
Submitting this gives us the flag!
dice{wtf_gpt_i_thought_you_were_a_smart_waf}
Author: defund
A simple implementation of the Winternitz signature scheme.
nc mc.ax 31001
In the given server.py, a secret key is generated with os.urandom, which is 32 sets of 32-bytes. The public key is each of these 32 sets of bytes sha256 hashed 256 times. The way the Winternitz server generates a signature for a message is it will calculate the sha256 of the input message, and then the value of each of the bytes of the 256-bit hash (32 total) corresponds to the number of times the corresponding segment in the secret key is hashed (except the number of hashes is 256 minus the value of the 8-bit hash segment). The verify function does the same thing, but instead of (256 - <8-bit segment>), it is just <8-bit segment>, so in total, the secret key's segment will have been hashed 256 times and equal the public key.
Hope that makes sense, there may be better explanations online. Therefore, when we pass in a message and get a signature back, we know what each segment of the secret key equals when it is hashed the corresponding number of times based on the bytes of the hash of the input message. When we input a new message, we need to be able to provide the signature meaning we need to be able to apply the corresponding number of hashes to the private key that we don't know.
Since we can't reverse the operation of the sha256 hash, what this means is each byte of the hash of the message we enter the second time needs to be less than each byte of the hash of the first message. For example, if we were able to magically know an input message that hashes to 32 sets of "FF", the corresponding signature would be each segment of the secret key hashed once (256 - 255). Then, when we pass in the second message, we know the secret key sections with one hash applied, and just need to do the remaining number of hashes based on the hash of the second message.
Below is the code solution to find two messages where the bytes of the hash of one message are all lower than the bytes of the hash of the second message.
I wrote these in C because I thought it would take way longer to calculate two sha256 hashes where the bytes of one are all higher than the other, but it was near instant. My prior iteration randomly generated two hashes in python and checked if they satisfied the condition but nothing was found in 10 minutes.
After finding these two messages, here is the final script to create the forged signature.
I didn't automate the solution and there is a timeout so it required being a bit fast, but after putting in the initial message to the server and pasting the received signature in the script, the forged signature could be generated. After inputting the second message and forged signature, we get the flag!
dice{according_to_geeksforgeeks}
Author: clubby
Try 2024's hottest game so far - DiceQuest! Can you survive the onslaught? Custom sprites made by Gold
note: the flag matches the regex dice{[a-z_]+}
We're given a game where we can move with WASD, collecting dice seem to give points but take health, and there's a shop with upgrade purchases, with the top purchase requiring 10000 points. Aside from the 5 point upgrade, it doesn't seem possible to reach the next upgrade at 100 points before the game ends so it's clear the score needs to be hacked.
Similar to Cheat Engine for Windows, Linux has a minimal version called scanmem (and a GUI front-end called game conqueror which would ask for a password and not start for some reason). After installing it with apt (or similar for your Linux flavor), run ./dicequest
and then in another terminal run sudo scanmem -p $(pidof dicequest)
. You can also specify the pid manually after starting scanmem with "pid <pid found with ps>".
The way scanmem works is a number (in this case, the score) can be entered and all memory regions for the process can be searched, giving a certain number of matches. Then, when the number is incremented (collecting dice), the number can be searched again from the found matches, further narrowing down which memory location corresponds to the score for the process. With two or three iterations, there is only one candidate left. Normally a single "set 1000000" should be enough but for some reason, I needed to run it multiple times, maybe due to some protection. After increasing the score, all of the purchases can be bought from the shop which results in the game lagging heavily and the dragons forming the flag.
The picture shows the last part of the flag, the full one being:
dice{your_flag_is_not_in_another_castle}
Author: arxenix
may your code be under par. execute the getflag
binary somewhere in the filesystem to win
nc mc.ax 31774
Solution:
This is a jail problem where we can define a character set of 6 characters (not including *, ?, or `) and we can then send commands as long as we're only using that character set.
We can do an ls -al as a starting point and then a grep -r g (or r/e/p which are in the char set) to get an idea of what's on the system.
The ls showed us the run script and a y0u folder (not pictured). The recursive grep (checking if the flag file has a g in it) shows us where the flag is, although we see a "Permission denied". Assuming this isn't a mistake and assuming privilege escalation isn't feasible, that means this is a binary executable that doesn't have read permissions. Therefore, the goal is to execute this binary.
Normally we could do something like */*/*/*/* to execute the file, but * is blocked. The other common solution is using ? for each character, like ???/????/... but ? is also blocked. Luckily, there is another way to do a wildcard match on a character: [a-z]. This would have worked if the path had only lower case ascii, but since it has numbers and _ as well, we can do this instead: [--~]. What this does is match all ascii between - (0x2d) and ~ (0x7e). We could have started it at an earlier ascii but this lets us reuse the - character. We end up only needing 5 characters with this, the character set being [-~]/. Enter this and the payload to run the executable and get the flag!