0x01. CONTEXTE
Nous allons ici faire une petite application d’authentification sécurisée de type zero-knowledge : la base de donnée n’a aucune connaissance des identifiants utilisateurs.
L’avantage est qu’en cas de fuite de données, il n’est pas possible d’associer un contenu à un utilisateur. L’autre avantage est qu’en utilisant la pratique ci-dessous, elle rend caduc toute forme d’injection dans la base de données.
Le désavantage est que lorsqu’un couple login/pass est perdu, il l’est définitivement : la confidentialité est prioritaire sur la disponibilité.
0x02. CONCEPTS
Grain de sel (salt) : une clé est préfixée et/ou suffixée au mot de passe afin de protéger le hash du bruteforce de mot de passe.
Exemple :
- si le mot de passe de John est : Mot2Passe
- le mot de passe salé peut être : 2_dNW=>2Mot2Passerzh}EGz6
Poivre (pepper) : il s’agit un grain de sel spécifique à un utilisateur et peut être stocké dans un espace de stockage tiers.
Exemple :
- si le mot de passe de Alice est : Mot2Passe
- le mot de passe poivré peut être : iZ1Q)jUjMot2PasseW[IbA1VJ
- si le mot de passe de Bob est : ILoveB33Rz
- le mot de passe poivré peut être : ;VK@zGGtILoveB33Rz.DLI:GMn
Ainsi le mot de passe de John, salé et poivré ressemble à :
@4UGc6JN2_dNW=>2Mot2Passerzh}EGz67D>S=Ppw
Ce qui - sans connaitre le grain de sel ni le poivre est beaucoup plus difficilé à retrouver.
0x03. VERIFICATION DES DONNEES
Malgré que le nom utilisateur et le mot de passe soient "hashés", il reste important de vérifier que le username soit cohérant, afin toute autre forme d’injection (XSS & co.)
function check_user($user) { if( preg_match_all('/^[a-zA-Z][a-zA-Z0-9]{3,}$/',$user) ) return($user); return(NULL); } function check_pass($pass) { if( preg_match_all('/^[a-zA-Z0-9:=;,.\_()-[]{}]{4,}$/',$pass) ) return($pass); return(NULL); }
0x04. EMPREINTE DES DONNEES
Aucune donnée ne sera stockées en claire dans la base de donnée :
function sha512($txt) { return( openssl_digest( $txt , "sha512") ); } function get_user($user,$salt) { return( openssl_digest( $user . DB_SALT , "whirlpool") ); } function get_pass($pass,$salt,$pepper) { return( openssl_digest( gzdeflate($pass . $pepper . DB_SALT), "whirlpool") ); }
0x05. GRAIN DE SEL
Dans cet exemple, le grain de sel, sera dans les définitions (define). Il pourrait s’agir d’un fichier à lire sur le système de fichiers (hors DocumentRoot).
define('DB_HOST','localhost'); define('DB_USER','root'); define('DB_PASS',''); define('DB_NAME','test'); define('DB_SALT',base64_decode('PkEwcSwoeSNqcW1lbWNoTzpkeD01RDNQWm05ZyZBZm9zL3ksLzUwMiRzPWFfPlFLZTMpcFE9VmUrfXddRHI0UA==')); define('PEPPER_FOLDER','/opt/webapps/pepper_db/');
0x06. POIVRE
Le poivre sera stocké dans un fichier, en dehors du DocumentRoot mais auquel Apache a des droits de lecture, par exemple :
mkdir -p /etc/www-keys/appli.com/peppers/ chmod -R 511 /etc/www-keys # Seul apache a le droit d'écrire et d'entrer dans ce dossier. Il n'a pas le droit de lister (manque le flag r) et les fichiers crées sont forcés d'appartenir au groupe apache (g+s) chown root:apache /etc/www-keys/appli.com/peppers/ chmod g+s,u-rwx,g=wx,o-rwx /etc/www-keys/appli.com/peppers/
Il pourrait être dans une base de donnée tierce, un fichier SQLite3 par exemple, ou sur un autre serveur.
function get_pepper($user) { $file_pepper = PEPPER_FOLDER."/".md5($user); if( ! file_exists($file_pepper) ) { return; } $fh = fopen($file_pepper,"rt"); $pepper = fgets($fh); fclose($fh); return($pepper); } function set_pepper($user,$pepper) { $fh = fopen(PEPPER_FOLDER."/".md5($user),"wt"); fputs($fh,$pepper); fclose($fh); }
0x07. AJOUT D’UN UTILISATEUR
La subtilité réside dans le fait que dans cette base, les données concernant le nom d’utilisateur et le mot de passe stockés seront des empreintes, y compris pour les noms d’utilisateurs. Du coup, une injection, n’est pas possible, et même si elle était réussie, les données ne pourrait être retrouvées sans le grain de sel, qui implique un accès au code source.
function add_user() { $pepper = $_SERVER["UNIQUE_ID"]."-".$_SERVER["REQUEST_TIME_FLOAT"]."-".str_rand(96); $clear_user = check_user($_POST["user"]); $clear_pass = check_pass($_POST["pass"]); if( $clear_user == NULL ) { die("Invalid username"); } if( $clear_pass == NULL ) { die("Password doest not match prerequities"); } $user = get_user( $clear_user , DB_SALT ); $pass = get_pass( sha512($clear_pass) , DB_SALT , $pepper ); set_pepper("$user",$pepper); $ret = db_query("SELECT user FROM `".DB_NAME."`.`auth_hardened` WHERE user LIKE '$user'; "); if( isset( $ret->fetch_object()->user ) ) { die("User '$clear_user' already exists."); } db_query("INSERT INTO `".DB_NAME."`.`auth_hardened` (`id`, `user`, `pass`) VALUES (NULL, '$user', '$pass'); "); die(" '$clear_user' successfully registered."); }
0x08. AUTHENTIFICATION
L’authentification repose alors non pas sur la vérification d’un login et d’un mot de passe mais du hash de chacun plus du poivre.
1-a récupérer le nom d’utilisateur 1-b saler et hasher le nom d’utilisateur 1-c vérifier que son poivre existe et le récupérer 2-a récupérer le mot de passe 2-b poivrer, saler et hasher le mot de passe 3- vérifier la présence des 2 hash en base
function check_user() { $score_ok = 0; $clear_user = check_user($_POST["user"],ENT_QUOTES); $user = get_user( $_POST["user"] , DB_SALT ); $pepper = get_pepper("$user"); $pass = get_pass( sha512($_POST["pass"]) , DB_SALT , $pepper ); if( $pepper ) { $score_ok++; } $ret = db_query("SELECT user FROM `".DB_NAME."`.`auth_hardened` WHERE user LIKE '$user' AND pass LIKE '$pass';"); if( isset( $ret->fetch_object()->user ) ) { $score_ok++; } if( $score_ok == 2 ) { die(" User '$clear_user' successfully logged in."); } else { die(" Invalid user or pass."); } }
Les formulaires d’authentification et d’enregistrement :
function form_header() { echo 'No login ? <a href="?register">Register</a> | <a href="?login">Login</a><hr />'; } function form_register() { echo '<html>'; echo '<body>'; echo ' <center><h1>Register :</h1></center>'; echo ' <hr />'; echo ' <pre>'; echo ' <form action="?register" method="post" accept-charset="utf-8">'; echo ' <label>Username :<input type="text" name="user" value=""></label> '; echo ' <label>Password :<input type="text" name="pass" value=""></label> '; echo ' <label>Password :<input type="submit"></label> '; echo ' </form>'; echo ' </pre>'; echo '</body>'; echo '</html>'; } function form_login() { echo '<html>'; echo '<body>'; echo ' <center><h1>Login :</h1></center>'; echo ' <hr />'; echo ' <pre>'; echo ' <form action="?login" method="post" accept-charset="utf-8">'; echo ' <label>Username :<input type="text" name="user" value=""></label> '; echo ' <label>Password :<input type="text" name="pass" value=""></label> '; echo ' <label>Password :<input type="submit"></label> '; echo ' </form>'; echo ' </pre>'; echo '</body>'; echo '</html>'; }
Le process de traitement de la page :
form_header(); if( isset($_POST["user"]) && isset($_GET["register"]) ) { add_user(); die(); } if( isset($_POST["user"]) && isset($_GET["login"]) ) { check_user(); die(); } if( isset($_GET["register"]) ) { form_register(); } else { form_login(); }
0x09. EN BASE
En base, il y aura alors des données inexploitables :
| id | user | pass | ----------- | ----------------------- | ------------------------- | 1 | 351c546ca8e7e26dd [...] | 54044dfd6272afe187d [...] | 2 | 8268cfc50e44aeeb6 [...] | d67017e0bec13de1de6 [...]
0x010. TEST SUR L’UTILISATEUR


0x011. ENREGISTREMENT


0x012. AUTHENTIFICATION


=> Écrit par : Nicolas, le 01 décembre 2019