raw Software

In the early 2000s, a compromised MySQL connection could sometimes become something much more serious than a stolen database. I used this technique myself in authorized security work: after gaining SQL access, I could cross the boundary into the operating system by installing a native user-defined function (UDF). The method was straightforward to implement and especially effective against installations that combined weak credentials with excessive privileges and permissive file access.

At the time, I also wrote several UDFs myself, including udf_infusion, a native extension that added statistical, string, mathematical, and utility functions to MySQL. Developing this performance-optimized native code also revealed the dangerous side of the same mechanism: code loaded to accelerate MySQL ran inside the database server process and could therefore cause damage with that process's privileges if abused.

The Boundary MySQL UDFs Cross

A normal SQL function is evaluated by the database server. A native UDF goes further: it is compiled machine code in a shared library, loaded into the mysqld process. Once loaded, it runs with the operating-system identity and access rights of that process. In other words, UDF loading deliberately crosses from SQL into native code.

This was useful for legitimate extensions such as specialized statistics, but it also created a dangerous escalation path. If an attacker could choose both the library bytes and the function registered from that library, SQL became an interface to attacker-controlled native code. The result was not automatically root access: it was code execution as the account running mysqld. Unfortunately, old installations sometimes ran the service as root, Windows LocalSystem, or another account with far more privileges than a database server required.

The Historical Four-Step Chain

  1. Find a way to execute sufficiently privileged SQL. This could be SQL injection through an application, or a directly reachable MySQL port combined with valid or weak credentials for a powerful MySQL account.
  2. Write the native library. Its binary bytes were represented as a hexadecimal SQL literal and written by a server-side SELECT 0x... INTO DUMPFILE operation to a new file that the MySQL loader could reach.
  3. Register the library function. CREATE FUNCTION ... SONAME ... associated a new SQL function name with the shared library.
  4. Call the new function through SQL. A subsequent SELECT new_function(...), potentially sent through the same SQL injection, entered the native code inside mysqld with the daemon's operating-system privileges.

These four steps still contain several independent security boundaries: initial SQL access, database authorization, server-side file placement, native code loading, and operating-system impact. A secure deployment only needs to stop one transition; a resilient deployment stops several.

Step 1: Finding a Way to Execute SQL

Some early deployments exposed TCP port 3306 broadly and retained a default administrative account with no password or a predictable password. Web applications made the situation worse by connecting as MySQL root and constructing queries by concatenating request data. A single SQL injection then inherited every privilege granted to that application account.

For many years, some MySQL instances remained directly reachable from the Internet on TCP port 3306 with a MySQL root account that required no password at all. Such deployments turned a database connection into an immediately privileged SQL session. Here root means the MySQL administrator account; it is distinct from the operating system's root identity.

SQL injection did not itself execute operating-system commands. It provided a channel through which the database evaluated attacker-selected SQL. The eventual impact therefore depended on the account behind the vulnerable application. An account limited to the application's tables kept the damage largely at the data layer. A global administrator exposed the server's file and extension mechanisms as well.

The SQL Account Also Needed Dangerous Privileges

The critical historical mistake was treating an application account like a server administrator. MySQL's FILE privilege authorizes server-side file operations. It is global rather than schema-scoped because files belong to the host, not to one database. UDF registration also writes to the mysql.func system table. Together, those capabilities allowed a database session to prepare native code and ask the server to load it.

On a system you administer, the following read-only inventory shows the current identity, effective grants, and the paths that govern file export and plugin loading:

SELECT CURRENT_USER(), USER();
SHOW GRANTS;
SELECT @@secure_file_priv, @@plugin_dir, @@version;

USER() describes the client identity presented at connection time, while CURRENT_USER() identifies the MySQL account actually used for privilege checks. That distinction was useful when host-based account matching selected a less obvious account than the client expected.

Step 2: Writing the Native Library

The native component followed MySQL's documented UDF application binary interface. A developer implemented exported initialization and evaluation functions, compiled them for the target operating system and CPU architecture, and produced a shared object or DLL. A malicious variant could make its evaluation function pass attacker-controlled input to an operating-system API. A command-executing implementation would turn this directly into a shell primitive and is not reproduced here. The same privilege boundary can be demonstrated safely with a real Unix UDF that returns the effective user ID of the mysqld process:

#include <mysql.h>
#include <unistd.h>

long long mysql_service_euid(
    UDF_INIT *initid,
    UDF_ARGS *args,
    unsigned char *is_null,
    unsigned char *error
) {
    (void)initid;
    (void)args;
    (void)is_null;
    (void)error;

    return (long long)geteuid();
}

MySQL calls the exported mysql_service_euid() symbol inside its own process. The return value is the numeric effective UID of the service account, just as reported by the operating system. No subprocess or shell is involved, but the example proves the crucial point: native UDF code executes in the security context of mysqld, not in a separate SQL sandbox. A production server should load only reviewed libraries installed by an administrator.

Compilation alone was not enough. The library had to match the target's architecture, object format, calling convention, MySQL UDF ABI, and dependent libraries. This is why the method was often prepared per platform. The resulting binary was encoded as a SQL binary literal and reconstructed by a server-side export operation. Historically, SELECT ... INTO DUMPFILE was preferred over INTO OUTFILE because DUMPFILE writes one value without text formatting or escaping bytes.

Schematically, the operation had the form SELECT 0x... INTO DUMPFILE 'new-library-file'. The ellipsis represents the complete, platform-specific library bytes and is deliberately not a usable payload. The important point is that MySQL interpreted the hexadecimal literal as bytes and the server process, not the SQL client, created the destination file.

Even old versions would not overwrite an existing file. The destination had to be new, writable by the mysqld OS account, and visible to the dynamic loader. These conditions were surprisingly common when the data directory, plugin directory, and service account were not separated carefully.

Step 3: Registering the Native Function

Once the library existed on the server, CREATE FUNCTION ... SONAME associated an SQL name with an exported symbol in that library. MySQL recorded the association in mysql.func and loaded the module into the server process. Older configurations could search general dynamic-library locations, so an attacker sometimes supplied a path or placed the file wherever the platform's loader would find it.

The registration statement had the conceptual form CREATE FUNCTION new_function RETURNS INTEGER SONAME 'new-library-file'. The SQL name and return type had to agree with the exported UDF interface; SONAME identified the library for MySQL's loader.

Current MySQL requires the shared library's base name and loads it from the directory configured by plugin_dir. A path component is rejected. The historical error is a concise record of that boundary becoming explicit:

ERROR 1124 (HY000): No paths allowed for shared library

The restriction does not make UDFs harmless. If plugin_dir is writable by mysqld and the same database account can export files and register functions, the essential chain may still exist. The directory must be read-only to the service account during normal operation, and application accounts must have neither global FILE nor write access to MySQL's system schema.

Step 4: Calling the Function Through SQL

Once registered, an ordinary-looking query such as SELECT new_function(...) caused MySQL to call native code in-process. If SQL injection supplied the original SQL channel, the same channel could issue this final query. There was no separate sandbox and no second operating-system login. The code could access whatever the mysqld process could access, and a crash in the function could crash the database server itself.

This final detail determines the real severity. A dedicated, unprivileged MySQL account with a narrow filesystem view limited the compromise. Running the daemon as root turned the same database mistake into complete host compromise. UDF abuse therefore demonstrated why the database privilege model and the operating-system privilege model must reinforce each other.

Why the Old Recipe Usually Breaks Today

No single release made the technique disappear. Defaults, privilege practices, and loader rules changed gradually, and modern installations commonly break several prerequisites:

With file operations confined, an attempted export outside the permitted directory produces an error such as:

ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement

That message proves only that this particular file operation was blocked. Likewise, the SONAME error proves only that a path was rejected. Neither is a security verdict for the whole server. An empty secure_file_priv value imposes no path restriction, and a writable plugin directory can reconnect the two stages. The right conclusion is that the historical recipe is unreliable against a well-configured current server, not that UDF abuse is impossible.

A Defensive Audit

The safest way to learn from this chain is to inspect each prerequisite without attempting to create files or load code. Run these checks only on systems you administer:

SELECT CURRENT_USER(), @@version;
SHOW GRANTS;
SELECT @@secure_file_priv, @@plugin_dir, @@local_infile;

SELECT USER, HOST
FROM mysql.user
ORDER BY USER, HOST;

SELECT *
FROM performance_schema.user_defined_functions
ORDER BY UDF_NAME;

Interpret the results as a chain, not as isolated settings:

The Lasting Lesson

The memorable part of this old technique was the UDF, but the UDF was only the final bridge. The real failure was privilege composition: a database account could write host files, modify the server's native extension registry, and rely on an over-privileged daemon to amplify the result. None of those capabilities looked like remote host administration when considered alone.

Modern MySQL gives administrators better boundaries, but they still have to be used together. Prevent injection, minimize SQL privileges, constrain server-side file access, protect the plugin directory, inventory native extensions, and run mysqld as if a database bug might someday reach the process. That layered model is what made the old attack stop scaling.