PEACTF 2019

Ce CTF s'est déroulé en ligne, sur 5 jours, à partir du lundi 22 juillet. J'ai été actif sur ce CTF les 2 premiers jours, et j'ai trouvé certains challenges assez cools.

Educated guess

On doit se connecter à l'adresse donnée. J'ai d'abord voulu regarder la page web avant d'analyser le code source. Voilà la réponse d'une requête GET :

Pas grand chose sur cette page, le serveur n'est pas très verbeux. J'ai ensuite regardé le code source de la page (le fichier en annexe de l’énoncé).

<!doctype html>
<html>
<head>
    <title>Secured System</title>
</head>
<body>
<?php

// https://www.php-fig.org/psr/psr-4/

function autoload($class)
{
    include $class . '.class.php';
}

spl_autoload_register('autoload');

if (!empty($_COOKIE['user'])) {
    $user = unserialize($_COOKIE['user']);

    if ($user->is_admin()) {
        echo file_get_contents('../flag');
    } else {
        http_response_code(403);
        echo "Permission Denied";
    }
} else {
    echo "Not logged in.";
}
?>
</body>
</html>

J'ai remarqué l'utilisation de cookie et la déserialisation d'objets, j'ai décidé de tester avec le cookie user=test.

Curieux, le serveur renvoie une erreur 500, essayons de comprendre pourquoi. On voit que le serveur déserialise le cookie user, et regarde si la fonction is_admin() renvoie true. En l'occurence mon cookie n'est absolument pas un objet sérialisé, le serveur rencontre une erreur lors de la déserialisation et revoie une 500.
Ensuite, j'ai vu que le serveur allait chercher automatiquement dans une classe externe, pour sérialiser un objet correct il nous faudra alors le bon namespace, par intuition je suis allé voir si la classe User existait.

Bonne intuition pour une fois . Mais par curiosité j'ai décider de tester avec un objet sérialisé valide, avec des valeurs à la con.

#objet sérialisé
O:1:{s:4:"test";i:1;}
#url-encoded pour le cookie
O%3A1%3A%7Bs%3A4%3A%22test%22%3Bi%3A1%3B%7D

Bon je m'y attendais à cette 500, mais on ne sait jamais.
J'ai ensuite essayer d'afficher le 'Permission Denied' pour construire un objet qui renvera true avec la fonction is_admin(). J'ai utilisé le namespace vu plus haut :

#objet sérialisé
O:4:"User":1:{s:4:"test";i:1;}
#url-encoded pour le cookie
O%3A4%3A%22User%22%3A1%3A%7Bs%3A4%3A%22test%22%3Bi%3A1%3B%7D

Nickel, j'ai réussis à crafter un objet valide, il ne reste plus que la fonction is_admin().
Pour ce faire j'ai effectué des tests avec différents attributs et valeurs, mais une des premières qui est passé par mon esprit était amdin:true, j'ai donc testé avec cet objet :

#objet sérialisé
O:4:"User":1:{s:5:"admin";b:1;}
#url-encoded pour le cookie
O%3A4%3A%22User%22%3A1%3A%7Bs%3A5%3A%22admin%22%3Bb%3A1%3B%7D

Note : les objets suivants permettent de valider le challenge.

O:4:"User":1:{s:5:"admin";b:1;}
O:4:"User":1:{s:5:"admin";i:1;}
O:4:"User":1:{s:5:"admin";s:1:"1";}
flag : flag{peactf_follow_conventions_3b2a868a8a16589704dc755276fb11fd}

Phillips And Over

Dernier challenge du CTF, celui qui valait le plus de points, mais pas le plus difficile à mon avis. Pour valider le challenge il faut se connecter en tant qu'admin du serveur. On se connecte à la page web :

Tout de suite je vois le bouton de connexion en haut à droite. Allons-y.

OK, je teste avec les creds basiques admin:admin.

Bon, au moins j'aurai essayé. Je décides de retourné la page de connexion, après avoir essayé deux trois tricks d'injection, je vois un lien pour mot de passe oublié, d'habitue en CTF cette fonctionnalité n'est pas monnaie courante, donc je ne m'attends pas à grand chose. Et pourtant :

Il y a bien une page d'oubli de mot de passe ! J'essaie alors de modifier le mot de passe admin en mettant '1337' en réponse.

Pas de chance ce n'était pas la bonne réponse . Peut-être qu'un autre utilisateur à une question plus facile, essayons avec l'utilisateur iamnotafakeuser.

Bon on sait déjà si un utilisateur existe sur le serveur ou non. On sait que admin y est bien, il n'y a sûrement que lui d'ailleurs. À ce moment-là j'ai commencé à manquer d'idées, j'ai ouverts l'outil de développement (Ctrl + Shift + I), en inspectant le formulaire de mot de passe oublié j'ai remarqué un input masqué <input type="hidden" name="debug" value="0">, j'a décidé de mettre cette valeur à 1 et de tester avec l'admin :

Ah ! Bah voilà le jus d'o- la SQLi ! J'ai essayé plusieurs payloads pour comprendre à quel type de SQLi on avait à faire. J'ai compris que la page renvoyait soit 'User does not exist' soit 'Your answer to the security question is not correct. We have sent $username an email to notify this incident.'.
Le second message n'apparaît que si l'input $username renvoie une valeur valide (true par exemple). J'ai alors essayé de trouver le mot de passe admin avec la payload admin' and password like "%";-- - de cette manière :

Cette payload fonctionne car dans la table des utilisateurs il existe une colonne username, une password, et une answer, comme mon injection sélectionne l'utilisateur avec le nom admin et comme mot de passe "%" (joker en SQL), l'utilisateur est bien dans la base de données, mais ma réponse à la question est mauvaise donc j'ai la page qui précède.
En continuant ainsi avec les payloads :

admin' and password like "a%";-- -
admin' and password like "b%";-- -
admin' and password like "c%";-- -
admin' and password like "d%";-- -
...

On en déduit le mot de passe de l'utilisateur :

Il me suiffi alors de me connecter avec ce mot de passe :

flag : flag{peactf_E_>_A_5c9619c4043bbe41e4d586746e57fbff}