/ crypto

abctf - Custom Authentication

Enoncé :
I just learned about encryption and tried to write my own authentication system. Can you get in?

Write up:

Interface

On commence par tenter de se connecter avec admin:admin.

Connect

Nous sommes bien connecté. Après plusieurs tentatives, tous les identifiants/mot de passe fonctionnent, mais avec toujours 'admin=false'. Le but ici est donc d'obtenir 'admin=true'.

Regardons de plus près le code JS qui nous est fournit :

var auth = {username: req.body.username, password: req.body.password, admin: admin};
auth = encrypt(JSON.stringify(auth));
res.append('Set-Cookie', 'auth='+auth+'; Path=/; HttpOnly');

Un cookie est donc généré, à partir des champs 'username' et 'password' saisis sur la page web.
Avec admin:admin, il est sous la forme :
{username: admin , password: admin, admin: false}

Cette valeur est ensuite chiffré en AES192 CBC via la fonction encrypt() :

var encrypt = function(data) {
  var cipher = crypto.createCipheriv('aes-192-cbc', secrets.key, secrets.iv);
  cipher.setAutoPadding(true);
  var ctxt = cipher.update(data, 'ascii', 'hex');
  ctxt += cipher.final('hex');
  return ctxt;
};

Et voici la valeur du cookie que nous récupérons :

auth=4292bcc1b8e6b8f9b2f45b1ec8afbde15c8a57d575f76cbb6fdf2b79c9581a2af45d3d4c746fc924dc446b910b64c51bb1aad644f8c99e345411f0bac1e2655d

Mais comment fonctionne l'algorithme de chiffrement AES192 CBC ?

C'est le principe du chiffrement par bloc. Chaque bloc fait 16 octets.

Pour les 16 premiers octets, nous calculons :
cipher_bloc_1 = AES(plaintext XOR IV)
IV : vecteur d'initialisation.

Puis pour chaque bloc de 16 octets suivants, nous calculons à partir du bloc précédent :
cipher_bloc_N = AES(plaintext XOR cipher_bloc_N-1)

Chiffrement CBC

Pour le dechiffrement, c'est le même principe.

Pour les 16 premiers octets, nous calculons :
plaintext_bloc_1 = decrypt_AES(cipher_bloc_1) XOR IV

Puis :
plaintext_bloc_N = decrypt_AES(cipher_bloc_N) XOR cipher_bloc_N-1

Dechiffrement CBC

Nous pouvons alors remarquer que comme nous utilisons le cipher_bloc_1 pour retrouvez plaintext_bloc_2, la modification d'un octet du bloc chiffré modifiera alors la valeur du texte en clair du prochain bloc. C'est le principe de la byte flipping attack.
Un schéma pour mieux comprendre :

Byte flipping attack

Revenons à notre chall. Nous pouvons effectuer la correspondance entre texte chiffré et le texte en clair ainsi :

4292bcc1b8e6b8f9b2f45b1ec8afbde1 {"username":"adm
5c8a57d575f76cbb6fdf2b79c9581a2a in","password":"
f45d3d4c746fc924dc446b910b64c51b admin","admin":"
b1aad644f8c99e345411f0bac1e2655d false"}

Le but est de modifier false en true. 'false' se trouvant sur le 4ème bloc, nous devons modifier le 3ème bloc. Mais nous allons avoir un problème, car modifier les octets du bloc précédent va aussi modifier la valeur en clair de ce bloc. Dans notre cas, le troisième bloc entier sera alors modifié et nous n'aurons plus le format attendu par l'application.

Il faut donc que le bloc précédent de 'false' soit entièrement dédié au mot de passe. Soit un username de 5 caractères et un mot de passe de 16 caractères.

username : admin
password : adminnnnnnnnnnnn
auth=4292bcc1b8e6b8f9b2f45b1ec8afbde15c8a57d575f76cbb6fdf2b79c9581a2a4d1d6a8b06785f30e4fd822be17072517dfee466b17763d9db133d59ed72903092fdb31379f254171d1790b6b8e72892
4292bcc1b8e6b8f9b2f45b1ec8afbde1 {"username":"adm
5c8a57d575f76cbb6fdf2b79c9581a2a in","password":"
4d1d6a8b06785f30e4fd822be1707251 adminnnnnnnnnnnn
7dfee466b17763d9db133d59ed729030 ","admin":"false
92fdb31379f254171d1790b6b8e72892 "}

false se trouve sur le bloc 4, du 12ème au 16ème octet. Nous devons alors modifier le bloc 3 du 12ème au 16ème octet.

true ayant seulement 4 caractères contrairement à false, nous remplaçons la dernière lettre par un caractère rejeté par la regex lors du déchiffrement.

var auth = decrypt(req.cookies.auth).replace(/[^0-9a-zA-Z{}":, ]+/g, '');

Sur la totatalité du texte chiffré, comme nous sommes en hexadécimal, ainsi que sur le bloc 3, les éléments à modifier commenceront à l'index 86.

Ci-dessous le script qui modifie false en true!

#!/usr/bin/env python
# -*- coding: utf-8 -*-

cipher =list("4292bcc1b8e6b8f9b2f45b1ec8afbde15c8a57d575f76cbb6fdf2b79c9581a2a4d1d6a8b06785f30e4fd822be17072517dfee466b17763d9db133d59ed72903092fdb31379f254171d1790b6b8e72892")

text1 = 'false' #valeur à remplacer
text2 = 'true!' #valeur que nous appliquons

index1 = 86 
index2 = 87

index = 0
for element in text1:
	tmp = cipher[index1]+cipher[index2]
	cipher_ascii = int(tmp, 16)    # Récupération code ascii de l'octet à remplacer (N-1)

	ascii_caract1 = ord(element)   # Récupération code ascii de la valeur que nous remplaçons ("f" puis "a" etc.)
	ascii_caract2 = ord(text2[index]) # Récupération code ascii du caractère que nous voulons appliquer ("t" puis "r" etc.)

	xor = cipher_ascii ^ ascii_caract1 ^ ascii_caract2  #  Modification de la valeur
	hex_xor = hex(xor)	# Passage en hexadecimal

	if len(hex_xor) == 3:
		cipher[index1] = str("0")
		cipher[index2] = str(hex_xor[2])
	else:
		cipher[index1] = str(hex_xor[2])
		cipher[index2] = str(hex_xor[3])
	index1 += 2
	index2 += 2
	index += 1

print "".join(cipher)

Notre nouveau cookie sera :

auth=4292bcc1b8e6b8f9b2f45b1ec8afbde15c8a57d575f76cbb6fdf2b79c9581a2a4d1d6a8b06785f30e4fd8239f26964157dfee466b17763d9db133d59ed72903092fdb31379f254171d1790b6b8e72892

Et donc :

flag

abctf{i_used_encryption_so_it_must_be_secure}

Source images AES CBC : http://resources.infosecinstitute.com