WaniCTF 2024 - pow - Write-up
Hey Guys,
This post is a write-up of the power challenge of WaniCTF 2024. This is an easy challenge; the server checks for a million numbers satisfying a condition but doesn't check if all these numbers are unique.
The Challenge
This is a web challenge, and when we first open the website, it shows us an incrementing counter, which increments by 1000 each frame, and a server response, which displays progress out of a million (yes, I counted all the zeroes).
A reasonable assumption is that we will get the flag once the Server response reaches a million.
After looking at the screen for some time, hoping it would get to a million, it dawned on me that it would take forever for that to happen, and I had to do something about it (sigh).
Understanding the Application
So, the first thing I did was look at the source of this page to find the code incrementing the server response and the client status.
function hash(input) {
let result = input;
for (let i = 0; i < 10; i++) {
result = CryptoJS.SHA256(result);
}
return (result.words[0] & 0xFFFFFF00) === 0;
}
async function send(array) {
document.getElementById("server-response").innerText = await fetch(
"/api/pow", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(array),
}
).then((r) => r.text());
}
let i = BigInt(localStorage.getItem("pow_progress") || "0");
async function main() {
await send([]);
async function loop() {
document.getElementById(
"client-status"
).innerText = `Checking ${i.toString()}...`;
localStorage.setItem("pow_progress", i.toString());
for (let j = 0; j < 1000; j++) {
i++;
if (hash(i.toString())) {
await send([i.toString()]);
}
}
requestAnimationFrame(loop);
}
loop();
}
main();
That's a lot. Let's break it down.
Two things of importance are happening in this code:
- Hash Calculation:
- For each number from zero, the function hash(input) iteratively calculates the SHA256 hash of the number 10 times and checks if the (first word & 0xFFFFFF00) gives zero.
- Server status update
- If it does, it sends the number as an element in a list (hint), which updates the server response.
Point to note: The server uses a cookie to keep track of the number of "correct" numbers the client has found.
At this point, I thought the hash function check was just a frontend validation check, and the server would update the response regardless of the number, which was not the case.
So, we need a correct value that passes the hash function test, which I got by placing a debug point at line 30 in the above script. This gave me 63001396 as a valid value.
So, how do we nudge this from here to a million? If you have a keen eye, you'll notice that we are sending a list of numbers, not one number. This gave me an idea. What if we send the same valid number a million times (whatever number we already have)?
The Minion Approach
The solution is pretty straightforward. I got the URL, the cookie, and the valid number and wrote a quick Python script to send the valid number a million times to the server, hoping it would give me the flag.
And, it crashed.
I realized a million numbers might be too many in a single request, and after some fine-tuning, I found the ideal number to be 85,000. Which meant the script looked something like this.
After adding a loop and running the script, I crossed the million barrier, and the server finally gave up the flag.
Final Thoughts
This challenge was pretty straightforward. Once I understood that the server accepts a list of numbers and they need not be unique, everything else fell into place.