diff --git a/README.md b/README.md index 6805fa8..ecfc288 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,4 @@ UE4 : * [TP Sécurité des BD (12h)](tp5-bd.md) * [IoT (12h)](tp6-iot.md) * [LDAP (10,5h)](tp7-ldap.md) +* [Passwords](td-passwords.md) diff --git a/td-passwords-files/skeleton.py b/td-passwords-files/skeleton.py new file mode 100644 index 0000000..7797a4c --- /dev/null +++ b/td-passwords-files/skeleton.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 + +import time +import sys +from toolbox import * + +# You should tweak these values during the work +nblogins = 10 # would be larger in real-life +nbpasswords = 1000000 # would be larger in real-life +nbiterations = 10 # 10000 is currently recommended, should be adapted to the usecase and changed with time (improvement of computation power), like a key size + + +############################################ +# Part of the script to edit # +############################################ + +# Hint : you can call decrypt(key,data) to decrypt data using key +def crackencrypted(database): + key = readfile("enckey")[0] + crackeddb = [] + for i in database: + # i[0] is the login, i[1] is the encrypted password + #... + crackeddb.append((i[0],i[1])) # second argument should contain cleartext password + return crackeddb + +# Hint : - genshahashes(passwords) returns all the hashes of passwords dictionary +# - getpassfromshahash(hashes, hash) returns the password which hashed as "hash" in hashes +def cracksha(database): + global nbpasswords + passwords = getPassDict(nbpasswords) # passwords contains a dictionary of passwords + #... + crackeddb = [] + for i in database: + # i[0] is the login, i[1] is the hashed password + #... + crackeddb.append((i[0],i[1])) # second argument should contain cleartext password + return crackeddb + +# Hint : salthash(password, salt) return the salted hash of password +def cracksaltedsha(database): + global nbpasswords + passwords = getPassDict(nbpasswords) + crackeddb = [] + for i in database: + # i[0] is the login, i[1] is the hashed password, i[2] is the salt + #... + crackeddb.append((i[0],i[1])) # second argument should contain cleartext password + return crackeddb + +# Hint : pbkdf2(password, salt, nbiterations) returns the pbkdf2 of password using salt and nbiterations +def crackpbkdf2(database): + global nbpasswords + passwords = getPassDict(nbpasswords) + crackeddb = [] + for i in database: + # i[0] is the login, i[1] is the hashed password, i[2] is the salt, i[3] is the iteration count + #... + crackeddb.append((i[0],i[1])) # second argument should contain cleartext password + return crackeddb + + + +############################################ +# Nothing to change after this line ! # +############################################ + + +if __name__ == '__main__': + # When called with init + if len(sys.argv) > 1 and sys.argv[1] == "init": + initworkspace(nblogins,nbpasswords,nbiterations) + print("Workspace initialized in files/ subdirectory") + exit(0) + + # Test whether init has been called before + try : + readfile("plain") + except FileNotFoundError: + initworkspace(nblogins,nbpasswords,nbiterations) + print("Workspace initialized in files/ subdirectory") + + # test plain DB + print("\n============\nPlain storage:") + plaindb = readfile("plain") + print("Plain DB is : " + str(plaindb)) + print("Authenticating with plain DB : " + str(authplain(plaindb[0][0],plaindb[0][1],plaindb))) + + #test encrypted db + print("\n============\nEncrypted storage:") + encdb = readfile("enc") + print("Encrypted DB is " + str(encdb)) + print("Authenticating with encrypted DB : " + str(authencrypted(plaindb[1][0],plaindb[1][1],encdb))) + start = time.time() + crackedenc = crackencrypted(encdb) + end = time.time() + print("Time to crack encrypted DB : " + str(end-start) + " seconds") + print("Cracked encrypted DB is " + str(crackedenc)) + + #test SHA db + print("\n============\nSHA storage:") + shadb = readfile("sha") + print("SHA DB is " + str(shadb)) + print("Authenticating with SHA DB : " + str(authsha(plaindb[0][0],plaindb[0][1],shadb))) + start = time.time() + crackedsha = cracksha(shadb) + end = time.time() + print("Time to crack SHA DB : " + str(end-start) + " seconds") + print("Cracked SHA DB is " + str(crackedsha)) + + #test Salted SHA db + print("\n============\nSalted SHA storage:") + saltedshadb = readfile("saltedsha") + print("Salted SHA DB is " + str(saltedshadb)) + print("Authenticating with Salted SHA DB : " + str(authsaltedsha(plaindb[0][0],plaindb[0][1],saltedshadb))) + start = time.time() + crackedsaltedsha = cracksaltedsha(saltedshadb) + end = time.time() + print("Time to crack salted SHA DB : " + str(end-start) + " seconds") + print("Cracked salted SHA DB is " + str(crackedsaltedsha)) + + # test PBKDF2 DB + print("\n============\nPBKDF2 storage:") + pbkdf2db = readfile("pbkdf2") + print("PBKDF2 DB is " + str(pbkdf2db)) + print("Authenticating with PBKDF2 DB : " + str(authpbkdf2(plaindb[0][0],plaindb[0][1],pbkdf2db))) + start = time.time() + crackedpbkdf2 = crackpbkdf2(pbkdf2db) + end = time.time() + print("Time to crack PBKDF2 DB : " + str(end-start) + " seconds") + print("Cracked PBKDF2 DB is " + str(crackedpbkdf2)) diff --git a/td-passwords-files/toolbox.py b/td-passwords-files/toolbox.py new file mode 100644 index 0000000..f931275 --- /dev/null +++ b/td-passwords-files/toolbox.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 + +# Avoir une lib avec des briques prêtes +# Avoir un script qui crée les fichiers de password SHA/PB/etc. pour pouvoir les manipuler en texte +# TD : associer les briques pour évaluer les attaques sur un pass / une base +# mettre un « except ImportError » et ressayer avec « Cryptodome » a la place de « Crypto » + +import re +import time +import random +import hashlib +# tweak to (try to) handle different crypto lib naming across systems (Linux, Mac, Win) +try: + from Crypto.Cipher import AES + from Crypto import Random +except ImportError: + try: + from crypto.Cipher import AES + from crypto import Random + except ImportError: + from Cryptodome.Cipher import AES + from Cryptodome import Random +import base64 +import os +import urllib.request +import string + +# returns an array of a dictionary of passwords +def getPassDict(nbpasswords): + try: + f = open("files/passwords.txt") + except FileNotFoundError: + print("Downloading a passwords list...") + urllib.request.urlretrieve("https://github.com/danielmiessler/SecLists/blob/master/Passwords/Common-Credentials/10-million-password-list-top-1000000.txt?raw=true", "files/passwords.txt") + print("Done !") + f = open("files/passwords.txt") + passwords = [] + #nbpasswords = 10000 + passtogen = nbpasswords + for password in f: + passwords.append(password.strip()) + passtogen-=1 + if passtogen == 0: + break + return passwords + +def genRandomPassword(): + length = 6 + chars = string.ascii_letters + string.digits + return ''.join(random.choice(chars) for i in range(length)) + +# reads/writes shadow-style files +def readfile(filename): + f = open("files/"+filename) + res = [] + for line in f: + output = line.strip().split(":") + res.append(output) + return res + +def writeFile(filename, array): + f = open("files/"+filename,'w') + for line in array: + towrite = "" + for item in line: + towrite+=item + ":" + towrite = towrite[:-1] + f.write(towrite) + f.write('\n') + + +# Plain storage +def genplain(nblogins,nbpasswords): + passwords = getPassDict(nbpasswords) + logins = [] + for i in range(0,nblogins): + login = "user" + str(i) + if (random.randint(0,10) < 4): + logins.append((login,passwords[random.randint(0,len(passwords)-1)])) + else: + logins.append((login,genRandomPassword())) + return logins + +def authplain(login, passwd, database): + for i in database: + if i[0] == login: + current = i[1] + return (current == passwd) + +# Encrypted storage +def genencrypted(logins): + encdb = [] + key = Random.new().read(16) + iv = Random.new().read(AES.block_size) + f = open("files/enckey",'wb') + f.write((base64.b64encode(key))) + f.write(b":") + #f = open("files/enciv",'wb') + f.write((base64.b64encode(iv))) + for i in logins: + cipher = AES.new(key, AES.MODE_CFB, iv) + enc = (base64.b64encode(cipher.encrypt(i[1].encode('utf-8')))).decode("utf-8") + encdb.append((i[0],enc)) + #print(enc) + return encdb + +def authencrypted(login, passwd, database): + for i in database: + if i[0] == login: + current = i[1] + keyiv = readfile("enckey")[0] + key = base64.b64decode(keyiv[0]) + iv = base64.b64decode(keyiv[1]) + #key = base64.b64decode(readfile("enckey")[0][0]) + #iv = base64.b64decode(readfile("enciv")[0][0]) + cipher = AES.new(key, AES.MODE_CFB, iv) + return (passwd == cipher.decrypt(base64.b64decode(current)).decode('utf-8')) + +def decrypt(keyiv,data): + key = base64.b64decode(keyiv[0]) + iv = base64.b64decode(keyiv[1]) + cipher = AES.new(key, AES.MODE_CFB, iv) + return cipher.decrypt(base64.b64decode(data)).decode('utf-8') + + +# SHA storage +def gensha(logins): + db = [] + for i in logins: + csum = hashlib.sha256(i[1].encode('utf-8')).hexdigest() + db.append((i[0],csum)) + return db + +def authsha(login, passwd, database): + for i in database: + if i[0] == login: + current = i[1] + return (current == hashlib.sha256(passwd.encode('utf-8')).hexdigest()) + +def genshahashes(passwords): + hashes = [] + for passwd in passwords: + hashes.append([hashlib.sha256(passwd.encode('utf-8')).hexdigest(),passwd]) + return hashes + +def getpassfromshahash(hashes, hash): + for j in hashes: + if j[0] == hash: + return j[1] + return None + +# Salted SHA storage +def gensaltedsha(logins): + db = [] + for i in logins: + salt = str(random.randint(0,65535)) + csum = hashlib.sha256((i[1]+salt).encode('utf-8')).hexdigest() + db.append((i[0],csum,salt)) + return db + +def authsaltedsha(login, passwd, database): + for i in database: + if i[0] == login: + current = i[1] + salt = i[2] + return (current == hashlib.sha256((passwd+salt).encode('utf-8')).hexdigest()) + +def salthash(password,salt): + return hashlib.sha256((password+str(salt)).encode('utf-8')).hexdigest() + +# PBKDF2 storage +def genpbkdf2(logins,nbiterations): + db = [] + for i in logins: + salt = str(random.randint(0,65535)) + csum = base64.b64encode(hashlib.pbkdf2_hmac('sha256',i[1].encode('utf-8'),str(salt).encode('utf-8'),nbiterations)).decode('utf-8') + db.append((i[0],csum,salt,str(nbiterations))) + return db + +def authpbkdf2(login, passwd, database): + for i in database: + if i[0] == login: + current = i[1] + salt = i[2] + nbiterations = int(i[3]) + return (base64.b64decode(current) == hashlib.pbkdf2_hmac('sha256',passwd.encode('utf-8'),str(salt).encode('utf-8'),nbiterations)) + +def pbkdf2(password,salt,nbiterations): + nbiterations = int(nbiterations) + return base64.b64encode(hashlib.pbkdf2_hmac('sha256',password.encode('utf-8'),str(salt).encode('utf-8'),nbiterations)).decode('utf-8') + + +# Generate shadow-style files +def initworkspace(nblogins,nbpasswords,nbiterations): + print("Generating " + str(nblogins) + " logins and " + str(nbpasswords) + " passwords") + try : + os.mkdir("files") + except FileExistsError: + pass + plaindb = genplain(nblogins,nbpasswords) + writeFile("plain", plaindb) + encdb = genencrypted(plaindb) + writeFile("enc", encdb) + shadb = gensha(plaindb) + writeFile("sha", shadb) + saltedshadb = gensaltedsha(plaindb) + writeFile("saltedsha", saltedshadb) + pbkdf2db = genpbkdf2(plaindb,nbiterations) + writeFile("pbkdf2", pbkdf2db) + + + +# Unit tests +if __name__ == '__main__': + # create shadow files + initworkspace(10,100,1000) + + print("======\nUnit tests of the toolbox, you must work in skeleton.py\n=========") + + # test plain DB + print("\n============\nPlain storage:") + plaindb = readfile("plain") + print("Plain DB is : " + str(plaindb)) + print("Authenticating with plain DB : " + str(authplain(plaindb[0][0],plaindb[0][1],plaindb))) + + #test encrypted db + print("\n============\nEncrypted storage:") + encdb = readfile("enc") + print("Encrypted DB is " + str(encdb)) + print("Authenticating with encrypted DB : " + str(authencrypted(plaindb[1][0],plaindb[1][1],encdb))) + + #test SHA db + print("\n============\nSHA storage:") + shadb = readfile("sha") + print("SHA DB is " + str(shadb)) + print("Authenticating with SHA DB : " + str(authsha(plaindb[0][0],plaindb[0][1],shadb))) + + #test Salted SHA db + print("\n============\nSalted SHA storage:") + saltedshadb = readfile("saltedsha") + print("Salted SHA DB is " + str(saltedshadb)) + print("Authenticating with Salted SHA DB : " + str(authsaltedsha(plaindb[0][0],plaindb[0][1],saltedshadb))) + + # test PBKDF2 DB + print("\n============\nPBKDF2 storage:") + pbkdf2db = readfile("pbkdf2") + print("PBKDF2 DB is " + str(pbkdf2db)) + print("Authenticating with PBKDF2 DB : " + str(authpbkdf2(plaindb[0][0],plaindb[0][1],pbkdf2db))) + + print("\n======\nUnit tests of the toolbox, you must work in skeleton.py\n=========") diff --git a/td-passwords.md b/td-passwords.md new file mode 100644 index 0000000..09940f2 --- /dev/null +++ b/td-passwords.md @@ -0,0 +1,102 @@ +# TD : Stockage et authentification par mot de passe + +Ce TD présente le stockage et l'authentification par mot de passe côté serveur. Il s'agit typiquement du problème rencontré par une application web qui souhaite authentifier ses utilisateurs, ou de ce qui est mis en œuvre dans une base de mots de passes système (/etc/shadow ou LDAP). Pour cela, l'application va stocker en base les comptes existants ainsi que le moyen de les authentifier. Comme il n'est évidemment pas souhaitable de stocker les mots de passe des utilisateurs en clair, nous allons analyser comment résoudre ce problème. + +Contexte général +================ + +Le scénario d'attaque est une exfiltration des fichiers de l'application (dont le fichier de la base de données ou le fichier `/etc/shadow` par exemple). Vous pouvez consulter une liste non exhaustive de bases ayant été dérobées sur le site [HaveIBeenPwned](https://haveibeenpwned.com/PwnedWebsites) pour vous rendre compte de l'étendue du problème. Dans ce cadre, nous posons les points suivants : + +* Le serveur est déjà compromis, l'obtention d'un compte valide sur ce serveur n'a pas d'intérêt pour l'attaquant. +* Les victimes potentielles sont les utilisateurs du site qui y ont enregistré un compte. En effet, un attaquant pourrait alors essayer de se connecter en leur nom sur des services tiers grâce aux informations récupérées. + +Pour limiter ce risque, deux approches complémentaires doivent être mises en place : + +1. Le serveur doit compliquer autant que possible la tâche de l'attaquant qui a volé la base en maximisant le temps nécessaire pour obtenir des informations valides à partir de la base (objet de ce TD). +2. Les utilisateurs, n'ayant pas la possibilité de connaître les contre-mesures mises en place par le serveur, doivent limiter l'impact de cette compromission en utilisant des mots de passes différents, idéalement un pour chaque site (ce qui passe souvent par un gestionnaire de mots de passe, non abordé dans ce TD). + +Ces deux mesures sont bien complémentaires car il est du devoir de chaque site de protéger les mots de passes des utilisateurs n'appliquant pas les meilleures pratiques et de chaque utilisateur de protéger au mieux de ses capacités ses mots de passes. Dans le cadre de ce TD, nous analysons la mesure (1), à appliquer côté serveur. + + +Squelette de code fourni +======================== + +Vous devez télécharger le squelette de code [ici](td-passwords-files). Vous pouvez récupérer l'intégralité du dépôt en tapant `git clone https://git.kaz.bzh/francois.lesueur/LPCyber.git`, puis aller dans le dossier `td-passwords-files`. Vous pourriez avoir besoin d'installer la bibliothèque python PyCryptodome (de préférence, et nécessaire avec Python 3.8) ou PyCrypto (dépréciée, mais a priori fonctionnelle jusque Python 3.7). Par exemple avec pip3 pour avoir PyCryptodome uniquement (les deux ne peuvent pas coexister sur le système) : + +``` +pip3 uninstall PyCrypto +pip3 install -U PyCryptodome +``` + +* `toolbox.py` est la bibliothèque contenant la boîte à outils, il peut être intéressant d'aller la consulter mais elle n'est pas à modifier ; +* `skeleton.py` contient le programme à écrire. + +En début de TD, vous devez lancer `./skeleton.py init` pour créer votre espace de travail. Cette commande, que vous pourrez rappeler plus tard, génère les fichiers suivants (lisibles) dans le sous-dossier `files` : + +* `plain` contient la base login/password en clair +* `enc` contient la base chiffrée en AES (la clé, volée également car nécessairement accessible à proximité sauf cas particulier est dans `enckey`) +* `sha` contient la base hashée avec SHA256 +* `saltedsha` contient la base hashée/salée avec SHA256 +* `pbkdf2` contient la base avec n itérations de hash salé (PBKDF2) + +Vous pouvez visualiser tous ces fichiers dans un éditeur de texte classique et analyser les différents champs présents. Vous pouvez ensuite exécuter `./skeleton.py` (sans argument) qui affichera, pour chaque schéma de stockage : + +* le contenu de la base stockée +* le résultat d'un test unitaire d'authentification (doit toujours être vrai) +* l'appel (chronométré) à une fonction pour casser la base (fonctions non implémentées dans le squelette fourni) + + +Analyse des différents schémas +============================== + +Pour chaque schéma (clair `plain`, chiffré `enc`, hash `sha`, hash salé `saltedsha`, hash salé coûteux `pbkdf2`), vous devez : + +* analyser le processus de l'ajout d'un compte et de la vérification d'un mot de passe +* proposer la procédure de récupération pour un utilisateur qui a perdu son mot de passe +* évaluer l'information révélée directement par la base de mots de passe +* évaluer le coût de cassage d'un mot de passe isolé +* évaluer le coût de cassage de la base entière +* implémenter la fonction pour casser la base + + + + + + + + + +Pour approfondir +================ + +**Les schémas vus dans ce TD, de manière similaire à ce que nous avons vu côté RSA, sont simplifiés pour comprendre le principe (textbook)**. Pour référence plus précise, vous pouvez ensuite consulter (et garder) : + +* [SOPHOS : Serious Security: How to store your users’ passwords safely](https://nakedsecurity.sophos.com/2013/11/20/serious-security-how-to-store-your-users-passwords-safely/) +* [OWASP : Password Storage Cheat Sheet](https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet) +* [Micode : 30 000 MOTS DE PASSE CRACKÉS EN 5 MINUTES ! (vidéo)](https://youtu.be/_1ONcmFUOxE) +* [Des exemples de dictionnaires](https://weakpass.com/wordlist)