Photo by Markus Spiske / Unsplash

WaniCTF 2024 - pow - Write-up

CTF Jun 24, 2024

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:

  1. Hash Calculation:
    1. 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.
  2. Server status update
    1. 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.

Tags