OverTheWire Natas Level 27 Walkthrough

This post covers level 27 of OverTheWire’s Natas challenges. This level is a bit more of a technical gotcha than previous levels (which covered more classic OWASP-style vulnerabilities). Let’s get started!

What is Natas?

Natas is an online hacking game meant to help you learn and practice security concepts.

OverTheWire is a website with a number of “war games”, which are online hacking games that allow you to practice security concepts. If you are looking for a beginner introduction to web security (albeit an older tech stack), then Natas is a great place to start.

Natas is hosted on different subdomains following the pattern of http://natas<level#>.natas.labs.overthewire.org. As you progress through the levels, you’ll need to increment the level number in the URL in order to view the correct level.

Each level requires the levels below it to be solved, so you will need the level 27 flag found in level 26 to begin this walkthrough. As before, make sure you keep notes and write down the passwords as you find them!

Level 27 ➔ 28

Once again, we open up the challenge (at http://natas27.natas.labs.overthewire.org/, using username natas27 and password 55TBjpPZUUJgVP5b3BnbG6ON9uDPVzCJ from the previous level) and are faced with another login screen:

We’re given the source code, so let’s check that out

Source Code Analysis

The source code is written in PHP (same as all of the Natas levels thus far).

If the request includes a username and password (in other words, if we fill out the form and hit submit), we’ll hit this part of the code:

A connection to the database will be opened. Next, the program will check if the username is valid using validUser():

If so, the credentials (username and password) will be checked via checkCredentials():

If there’s a valid match, all information related to the user will be dumped (dumpData()).

If not, a new user will be created with that username/password (createUser()):

Strategy

Each of the functions above use mysql_real_escape_string() to escape our input. That means no SQL injection. I found a few posts describing edge cases in which you could bypass mysql_real_escape_string() but none that apply here. It’s possible there are bypasses, but I was not able to find any while researching this rabbit hole.

That leaves some kind of logic flaw, then. Presumably, we’re looking to get information for username natas28.

The function checkCredentials() matches a username and password against the database, whereas validUser() and dumpData() only match the username.

So the username existence is what determines if a new user is created or not. If the correct credentials for the username are provided, the data matching that username (but not necessarily that password) is dumped.

Whitespace padding

I’m cutting out a number of rabbit holes that I went down, but the main thing that I tried that turned out to be useful later was experimenting with natas28 or natas28%00 (null-terminated). That would return the message:

Wrong password for user: natas28

Which means that the space or null-termination was being truncated, so what was actually going through was just natas28. In other words, the database is removing trailing whitespace for us, foiling our plan. We’ll get into more detail regarding how this fits into the solution in just a minute.

MySQL issues

Without going into discussion of other (unfruitful) rabbit holes, the logic flaw in this program is found in a part of the code I have not shared yet:

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

The users table schema has been defined for us in previous levels too, although I’ve largely ignored it. There are two issues here:

  1. The username and password are limited to 64 characters (which is reasonable) but there is no code preventing input >64 chars in length.
  2. If you’re familiar with SQL, you might also have noticed that there’s no restriction on the username being unique.

So what happens when you provide a value longer than the varchar limit?

If strict SQL mode is not enabled and you assign a value to a CHAR or VARCHAR column that exceeds the column’s maximum length, the value is truncated to fit and a warning is generated.

… if strict mode is enabled, values that exceed the column length are not stored, and an error results.

This is from the MySQL documentation. The source code we’re provided does not definitively say if strict mode is enabled or not, but it’s worth a shot.

Natas Level 27 Solution

Our strategy here is to:

  1. Submit something that is interpreted by the validUser() function as a new user, triggering the createUser() function.
  2. The input to the createUser() function will overflow the varchar limit, and result in an identical natas28 user, since there’s no restriction on the usernames being unique.
  3. We’ll then login with the secondary natas28 credentials that we just made.
  4. This will pass the checkCredentials() function, and then will be interpreted as the original natas28 user for the dumpData() function.

Crafting the username

If we submit [natas28 username][random chars], it will be interpreted as an entirely different username. We need something that the database cleanly interprets as just natas28.

If you remember from earlier, whitespace gets truncated. So what happens when you submit [natas28 username][tons of whitespace that overflows the varchar limit]?

You guessed it, another Wrong password for user: natas28 error meaning that it’s interpreted as the original natas28.

That’s because it’s being truncated before the validUser() check, and fails. If we put something at the end of our long string, it will not truncate the username during the validUser() check:

natas28 [... lots of whitespace...] x

This will result in the validUser() function returning nothing, meaning that a new user will be created. But the long string entered into the database will be truncated before the x due to the varchar limit, and the remaining whitespace will also be truncated, leaving us with just natas28.

I found an online MySQL editor/compiler to test this out, but it didn’t work online. Maybe they have strict mode enabled. Out of other ideas, I tried this directly on the Natas 27 level, using BurpSuite.

First, I figured out how many spaces I would need (url-encoded as +):

$ python -c "print('+' * (64 - len('natas28')))"
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Then I sent that as a POST request. To recap, that’s a username starting with natas28, a long string of spaces, followed by an X (and a password of password). The X will prevent the whitespace from being truncated right away. Then during user creation, the X (and spaces) will be truncated, leaving us with a duplicate natas28 user.

The response back says that username natas28 [...57 spaces...] x.

Let’s see if we can login using natas28 and password password:

And there’s our flag!

Takeaway: MySQL unique keys and character lengths are important and need to be enforced by code.

Resources: