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 compact, repeatable, and especially effective against installations that combined weak credentials with excessive privileges and permissive file access.

This is a historical reconstruction, not an exploitation recipe. The binary payload and the command-executing UDF are deliberately omitted. They are unnecessary for understanding the important part: five independent security boundaries had to fail in sequence. That architecture still matters because modern controls are designed to break the same chain at several points.

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 Five-Stage Chain

  1. Obtain SQL access. Weak or empty administrative passwords, an exposed port, or SQL injection in a web application supplied a database session.
  2. Reach dangerous privileges. The session needed broad rights, particularly server-level file access and permission to register a loadable function.
  3. Place a native library. MySQL's server-side file export was used to create a new shared-library file in a location from which the server could load it.
  4. Register the UDF. A row in MySQL's function registry associated an SQL function name with the shared library.
  5. Invoke native code. Calling the SQL function transferred execution to the library inside mysqld, inheriting the daemon's OS privileges.

Calling this a three-step attack hides the real lesson. Initial access, authorization, file placement, code loading, and operating-system impact are distinct transitions. A secure deployment only needs to stop one transition; a resilient deployment stops several.

Stage 1: Getting a Database Session

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.

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.

Stage 2: Finding the Required 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.

Stage 3: Turning a Query Result into a 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.

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.

Stage 4: 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.

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.

Stage 5: What Execution Actually Meant

Invoking the new SQL function caused MySQL to call native code in-process. 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.