OTW Natas27

Natas27

Username: natas27
Password: 55TBjpPZUUJgVP5b3BnbG6ON9uDPVzCJ
URL:      http://natas27.natas.labs.overthewire.org
Natas info
Natas27 index

Another login?

<?

// morla / 10111
// database gets cleared every 5 min


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


function checkCredentials($link,$usr,$pass){
    $user=mysql_real_escape_string($usr);
    $password=mysql_real_escape_string($pass);

    $query = "SELECT username from users where username='$user' and password='$password' ";
    $res = mysql_query($query, $link);
    if(mysql_num_rows($res) > 0){
        return True;
    }
    return False;
}


function validUser($link,$usr){
    $user=mysql_real_escape_string($usr);

    $query = "SELECT * from users where username='$user'";
    $res = mysql_query($query, $link);
    if($res) {
        if(mysql_num_rows($res) > 0) {
            return True;
        }
    }
    return False;
}


function dumpData($link,$usr){
    $user=mysql_real_escape_string($usr);

    $query = "SELECT * from users where username='$user'";
    $res = mysql_query($query, $link);
    if($res) {
        if(mysql_num_rows($res) > 0) {
            while ($row = mysql_fetch_assoc($res)) {
                // thanks to Gobo for reporting this bug!
                //return print_r($row);
                return print_r($row,true);
            }
        }
    }
    return False;
}


function createUser($link, $usr, $pass){
    $user=mysql_real_escape_string($usr);
    $password=mysql_real_escape_string($pass);

    $query = "INSERT INTO users (username,password) values ('$user','$password')";
    $res = mysql_query($query, $link);
    if(mysql_affected_rows() > 0){
        return True;
    }
    return False;
}


if(array_key_exists("username", $_REQUEST) and array_key_exists("password", $_REQUEST)) {
    $link = mysql_connect('localhost', 'natas27', '<censored>');
    mysql_select_db('natas27', $link);


    if(validUser($link,$_REQUEST["username"])) {
        //user exists, check creds
        if(checkCredentials($link,$_REQUEST["username"],$_REQUEST["password"])){
            echo "Welcome " . htmlentities($_REQUEST["username"]) . "!<br>";
            echo "Here is your data:<br>";
            $data=dumpData($link,$_REQUEST["username"]);
            print htmlentities($data);
        }
        else{
            echo "Wrong password for user: " . htmlentities($_REQUEST["username"]) . "<br>";
        }
    }
    else {
        //user doesn't exist
        if(createUser($link,$_REQUEST["username"],$_REQUEST["password"])){
            echo "User " . htmlentities($_REQUEST["username"]) . " was created!";
        }
    }

    mysql_close($link);
} else {
?>

This uses the mysql_real_escape_string(), which I'm pretty sure is okay at preventing SQL injections for PHP. There is an issue when the encoding for unicode / 2 byte characters (GBK) turn into 's and bypass the filter, but I don't see any indication that it is part of this challenge.

mysql_fetch_assoc()

while ($row = mysql_fetch_assoc($res))

This function was depreciated in PHP 5.5.0, and taken out in PHP 7. Essentially it will fetch all of the rows that the query resulted in. This function is weird because the create user functionality checks for duplicate users before inserting a new user into the database. If this function prints multiple rows, that makes me think that it is possible to get multiple users with the same username into the database.

Exploring more functionality

Creating a user "a" with a blank password and logging in as them will return the following:

Dumping data for user 'a'

Trying to login as natas28 doesn't work either, big surprise.

Yehaw this is a good and difficult one for sure.

Thinking back to the mysql_fetch_assoc() function, I want to create another natas28 user, but also not fetch the existing user in the valid_user() function...

Because the validUser() and dumpData() functions both use the same query, my original thought was that it isn't possible to send a username that wouldn't appear from the validUser(), but not dumpData(). (This is wrong, but it took me a while, and a hint to get the answer).

Side rant

Also I'm not sure what's going on with the mysql docker I've been using to test, but it's been using all of my laptop's memory. Limiting the resources to anything <10gb will cause mysql to crash >:( so much for a lightweight testing backend...

Another Attempt

What happens when I put too much input into the username? (> 64 characters since we have the table information in a comment).

Debugging result of a long username

With verbose error printing, the query cannot be completed because the username is too long. This means that our username isn't inserted into the database so back to the drawling board.

Am I on the right track?

More information gathering

After a little digging on the OTW discord server I have gotten a second wind and I am putting off asking for any help at this point still. I think my original thought was correctish. Apparently by typing natas28 they got the password, which means someone was able to do what I thought wasn't possible... Specifically bypassing the validUser() check, or something with the checkCredentials()?

Back at it

Some progress was made, I had looked into a character encoding bug with mysql that allows for a bypass of mysql_real_escape_string(), but I couldn't get it to work. Looking into it again, I found an sqlmap script that automated a payload creation (prepending a unicode character in front of a ') and tried again, and I think I've got an injection that would work IF the vulnerability is with this encoding.

The payload of the injection is tricky since I have to create a valid payload for 2 different queries.

$query = "SELECT * from users where username='natas28' and '1'='0'";
$query = "INSERT INTO users (username,password) values ('natas28' and '1'='0','$password')";
ERROR 1292 (22007): Truncated incorrect DOUBLE value: 'natas28'

Attempting to use a UNION SELECT, subqueries, etc weren't working for me using only the username field... If the username isn't able to pull everything I want off, what about the password field.

A little more looking into the syntax for INSERT:

INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
    [INTO] tbl_name
    [PARTITION (partition_name [, partition_name] ...)]
    [(col_name [, col_name] ...)]
    { {VALUES | VALUE} (value_list) [, (value_list)] ... }
    [AS row_alias[(col_alias [, col_alias] ...)]]
    [ON DUPLICATE KEY UPDATE assignment_list]

Instead of only 1 username, password pair, we can add a list of values, so something like:

INSERT INTO users (username,password) values ('nonexistent',''), ('natas28', 'doot') creating 2 users with 1 INSERT of the createUser() function. Again in theory it would work, but I still hadn't been able to escape out of the query string with the mysql_real_escape_string() calls.

Actual solution: Truncation

I gave in and messaged the OTW discord to see if I was on the right track, I was not.

Short message history, big hints

To give myself some credit, I had read in the WAHH that there was an exploit on adding to an input such that it would exceed the maximum size the database could store, and I did give it an attempt, but I didn't mess with it too much.

The 22 minutes that passed before I was able to solve the challenge was spent trying to figure out what input would be treated as different to the stored username of natas28 such that it wouldn't show up in the validUser() check so it would be inserted into the database as another entry with natas28 as the username.

The username is just "natas28" + 58 spaces + %00 (urlencoded null byte)

natas28                                                          %00

Make sure to add a password that isn't blank just in case other players are attempting to solve the challenge, then by requesting natas28 with the new password will return the original password!

Natas27 success
PHSC138
The World Wide Web