Tuesday 12 June 2012

Nmap NSE Howto: MySQL Auth Bypass

A recently disclosed critical vulnerability in MySQL authentication on some platforms gave me just the excuse I needed to write my first Nmap NSE script. @jcran produced a metasploit module to find and exploit the MySQL bug so I thought I'd try and fill a gap in the Nmap world.

First thing I needed was a vulnerable host to scan. I didn't have anything in my VM collection already so I took advantage of some Free Tier Amazon EC2 time and fired up a 64-bit Ubuntu 12.04 AMI. Specifically I fired up a micro instance of ami-e1e8d395 which is the suggested Ubuntu image on the wizard screen. I left everything as default and once it was running ssh'ed in.

MySQL isn't installed by default on this image so I had to install it. Installing it is as simple as:
sudo apt-get install myql-server

I specifically didn't run an apt-get update on this server before I installed MySQL just in case I ended up with a patched version. The version I've got is 5.5.22-0ubuntu, anything later and it's probably fixed. I quickly verified it was vulnerable before proceeding:

The first time you see this work you realise just how scary this bug is. I also can't help but wonder how long bad people have known about it.

With a confirmed vulnerable installation I set about configuring it for remote access.

Edit /etc/mysql/my.cnf, find the line which says:
bind-address            =

and change it to:
bind-address            =

and restart MySQL:
sudo /etc/init.d/mysql restart

Ignore all the Ubuntu rubbish about using Upstart, yada-yada, whatever, init wasn't broke but thanks for fixing it.

The next hurdle is that, by default, the root account is the only one and it is not authorised to connect from any host other than localhost. I don't want to develop on the EC2 instance and I also want to verify it'll work for hosts across a "proper network". To solve this I created an empty database and a user with access to it from any IP address.
mysql> create database nsetest;

mysql> grant all on nsetest.* to nse@'%' identitifed by 'dodgypass';

The % in the above is the wildcard character meaning any host. Running our bash for loop from above against the remote database this time and using the nse user verified the vulnerability existed remotely.

With the testing lab ready I turned my attention to writing the NSE script. As ever, the best place to start is with something you know works. On my BT5 VM I had a look in /usr/local/share/nmap/scripts to see what there was already for MySQL.

I decided mysql-empty-password.nse was the closest to what I was trying to do so I made a copy in my ~/Development directory and started hacking away at it. The actual process of writing a LUA script is pretty hard to describe but what I'll do now is break the script down into different sections and try and explain what is happening. As with all these sorts of things, there are standards involved (anyone who's ever coded for Metasploit will know exactly what I'm talking about).

If you want to follow along at home, the entire script is in our Github repo at https://github.com/7Elements/nmap-nse-scripts/blob/master/mysql-auth-bypass.nse. The file starts with information about the script, its capabilities, author, output example and license:

Of the above probably the most important thing to get right is the categories. When you are running Nmap NSE scripts you can specify to run all scripts of a certain category. If you write a script like this which exploits a bug but you put it down as 'safe' you're inviting a whole world of trouble. A full list of categories is available at http://nmap.org/nsedoc/categories/.

Next up are library imports. LUA, like most languages, allows the creation of library files in which to group common functions. I used the following:

From what I can tell, you'll pretty much use shortport in every NSE script you'll ever write. It provides common functions for managing network connections.

stdnse provides various handy and common functions including those which handle printing output.

mysql provides simple MySQL functions like login and query execution.

unpwdb is a really interesting library. Nmap NSE comes with a built in 'database' of common usernames and passwords along with this set of functions to interact with it.

We then add version information as a comment. Comments in LUA are preceded with -- (double dash).

Each NSE script must contain one of the following four functions:
portrule(host, port)

I won't rehash the documentation too much here but we are interested in a portrule which will run after the specified nmap scan has completed. A portrule runs when you identify a port which meets a certain criteria. In our case we want an open tcp port 3306 (the MySQL default).

Next we define an action function. This will be triggered by the portrule if our open port condition is met. This is where we get our hands dirty.

First thing to comment on, indentation. LUA does not require indentation but frankly, unless you're playing code golf you'd be crazy not to indent and make the code readable. I use a two space indent because I'm that way out.

So we're defining the action function. It takes two parameters, host and port which are given to it by NSE magic. We don't need to worry too much about that in this exercise.

Next we define four local variables. LUA has global and local variable scope. Anything not defined as local is global.

socket = nmap.new_socket() returns an NSE socket object.

catch is a function we will use if we encounter any exceptions. You can call this what you like. It just closes the socket.

try uses the Nmap new_try API call. new_try sets up an exception handler and, if passed a function as above nmap.new_try(catch) it will execute that function if an exception occurs.

Lastly we define an empty LUA table called results in which to store our results later.

The next few lines are pretty self-explanatory.

We set a socket timeout of 5000 milliseconds (that's 5 seconds y'all) in case something goes wrong with the connection.

The usernames line is important. This builds a table of usernames by calling the unpwdb.usernames() function. The unpwdb.usernames function keeps returning usernames from the in-built list (or your list if specified) until they are exhausted or timeout settings are reached.

Finally we set a password to use for all the login attempts. We set this to something we don't expect to work.

Now we enter a loop through the usernames table, for each username we try up to 300 login attempts with the same password.

for loops in LUA are easy: for condition do --something end. We have two above. The outer loop is iterating through the usernames table we built earlier, storing the returned value in username and then entering the loop.

stdnse.print_debug will print out the text Trying username if nmap debugging is set to 1 or more (nmap -d).

The inner for loop sets a variable i to 0, the maximum count to 300 and the step to increment as 1. Basically, it'll perform 300 (well 301 to be specific as I started at 0 - oops, off by one error) iterations of the upcoming code. As the MySQL bug is triggered on a 1 in 255 chance I figured this should be enough though I've seen elsewhere people having problems with this and ending up with numbers like 10,000 attempts.

If nmap debugging is set to 2 (nmap -d -d) then the attempt number will be printed.

local status, response = socket:connect(host, port) attempts a connection to host, port returning an error on the next line if status is not defined.

This part of the code uses the receiveGreeting function from the NSE MySQL library in order to handle the data sent back by the MySQL server. The following screenshot from Wireshark (click to enlarge) shows a decoded version of the MySQL greeting.

Pay particular attention to the salt as this is used in the next section of the code. A new salt is generated for every connection request, this is why we perform a new connection on each iteration of this loop rather than firing multiple authentication requests down a single TCP connection (yes, I learned that the hard way).

This is the meat and potatoes now. mysql.loginRequest is a part of the mysql NSE library and sends, as its name suggests, a login request. Note the use of our username and password variables and the salt from the response. This is all put together by the loginRequest function to create a MySQL login hash which is then sent over our socket.

If the errorcode returned in the response to our login request is 0 it means we had a successful login. If that's the case we use the LUA table.insert function to append a string containing details of the successful to the result table we created earlier. If we got a successful auth we also issue a break which stops the loop.

Next we close our loops and issue a socket:close() to tidy up our connection.

Finally we output our result table using the stdnse.format_output function which gives that pretty hierarchical view we put in the comments for @output right at the start.

And that's it. The script is done. By default, the nmap username database will not contain the value nse which we set up earlier as our vulnerable user so we will need to specify our own usernames file. To do this we can do the following:
echo nse > usernames.txt

Now we are all ready to run it against our vulnerable MySQL EC2 instance.

Nice. If you want a bit more verbosity then add the -v and -d (or -d -d) flags too.

After all this it turned out that someone on the nmap-dev mailing list had already put together a far more comprehensive solution which ports the automatic hash dumping of @jcran's metasploit module to boot. You can see that here http://seclists.org/nmap-dev/2012/q2/679.

However, for an hour's coding - and that really is all this took - I now know how to code some basic LUA and put together an Nmap script. This has to be a good thing.