
前阵子在一台已授权测试环境的内网机器上做安全复盘MySQL root 权限已经拿到下一步要往系统权限走。同组的同事问我UDF 和启动项提权你先试哪个我当时的回答是看环境不是看名气。这两个名字在 MySQL 安全讨论里几乎总是成对出现但它们的适用条件、稳定程度、暴露风险其实差别很大。这篇文章就围绕 mysql 数据库的 UDF 提权和启动项提权展开把我自己踩过的坑、常用的判断思路、落地细节一起写出来希望能给做渗透测试、安全运维的朋友一些参考。先说清楚这篇文章讨论的是授权测试环境里的技术复盘不是鼓励任何人在未授权系统上乱试。提权这条路方向对了是测试方向错了是事故。下面内容里我不会只给命令还会把每条命令为什么这么用、什么条件下会失败、失败后怎么排查都讲透。1. 先说清楚为什么UDF和启动项提权总被摆在一起1.1 两件事的本质都围绕“扩大命令执行边界”MySQL 提权这件事本质上不是“破解密码”而是“把数据库权限翻译成操作系统权限”。UDF 的思路是让 MySQL 进程内多出一个能调用系统命令的函数等于给数据库装了一条通到 shell 的管道启动项提权的思路则是利用数据库能写文件的能力把一个可执行脚本或命令塞进系统开机自启的位置等系统重启之后自动执行。这两条路之所以经常被放在一起讨论是因为它们对前置条件的要求高度重叠你至少得有一个能写文件的数据库账号。更准确地说是得拿到 FILE 权限或者 root 权限。MySQL 的权限体系里写文件能力被 secure_file_priv、plugin_dir、目录写权限这几道闸门卡得很死所以能不能成很大程度上不取决于攻击技巧而取决于对方配置得有多随意。我见过不少刚入门的朋友一上来就搜“UDF 提权 exploit”下载一个现成的 so/dll 文件往插件目录一丢然后创建函数失败就开始怀疑工具不对。其实多半是没搞明白 MySQL 对插件文件版本的强依赖。这个坑我后面会专门展开。1.2 提权不是万能钥匙先看前提条件UDF 翻译过来是“用户自定义函数”本意是让懂 C/C 的开发者扩展 MySQL 功能它本身不是漏洞。能被用来提权是因为 MySQL 进程的运行身份太高且允许普通用户往插件目录写文件。反过来想如果 MySQL 是以低权限账号运行的就算你成功创建了函数能执行的命令也受系统账号限制很多目录写不进去提权效果会大打折扣。启动项提权同样依赖“进程有没有写目标目录的权限”。Windows 下常见目标是当前用户的 Startup 文件夹或注册表 Run 键Linux 下常见目标是 /etc/rc.local、/etc/init.d、cron、systemd 服务。如果 MySQL 是 Windows 服务且运行在 SYSTEM 身份那写入 HKLM...\Run 或者 ProgramData 下的 Startup 目录基本畅通无阻如果只是普通用户身份那就只能影响当前用户登录时的自启项真正拿到系统权限还得再走一步。所以我的经验是别把 UDF 和启动项当成两个并列的“工具”而是当成两条受环境制约的路径。动手之前先回答三个问题当前数据库账号具备什么权限MySQL 进程以什么身份运行系统允许数据库写到哪些目录这三个答案出来路径基本就定了。2. UDF提权的完整链路与那些让我踩过坑的细节2.1 前置条件插件目录、权限、secure-file-priv一个都不能少UDF 提权的常规链路是把一个编译好的动态库文件Linux 下是 .soWindows 下是 .dll放进 MySQL 插件目录然后通过 CREATE FUNCTION 注册成自定义函数调用它来执行系统命令。听起来简单但每一步都有硬性条件。插件目录由 plugin_dir 变量控制默认一般在 MySQL 安装目录的 lib/plugin 下。你可以用下面这条命令确认SHOW VARIABLES LIKE plugin_dir;如果插件目录本身对 MySQL 进程不可写后面全白搭。Linux 下这个目录经常属于 mysql 用户而很多部署方式习惯用 root 启动 MySQL这时候反而是写文件最容易的。Windows 下插件目录通常在 Program Files\MySQL 下默认权限对服务账号通常可写但具体还得看安装方式。另一个关键变量是 secure_file_priv。它管的是 SELECT ... INTO OUTFILE 和 LOAD_FILE 这类文件读写操作。这个值有三种可能NULL 表示完全禁止文件导入导出空字符串表示不限制指定路径表示只能读写该目录。很多配置文件里声明了 secure_file_priv 却忘了赋值或者赋了 NULL都会导致“能查函数表却写不进任何文件”的怪象。SHOW VARIABLES LIKE secure_file_priv;我遇到过一个最典型的场景对方 MySQL 5.7 跑在 Windows 上root 密码被爆破成功secure_file_priv 配置为空字符串插件目录可写。这种情况下 UDF 基本是首选方案因为 sys_exec 这类函数一旦注册成功执行命令的权限直接继承 MySQL 服务账号如果是 SYSTEM那一次性就到顶了。2.2 手工落地一个UDF的完整操作过程我平时做授权测试时不会一上来就传网上下载的现成二进制因为版本和平台不匹配的坑实在太常见。更稳的做法是自己编译或者从可信项目下载源码后在本地编译好再传。下面以 Linux 上编译 lib_mysqludf_sys 为例。首先准备编译环境需要 gcc 和 MySQL 客户端开发头文件。不同发行版包名不一样Debian/Ubuntu 下一般是 libmysqlclient-devCentOS/RHEL 下是 mysql-devel。编译命令大致长这样gcc -shared -fPIC -o lib_mysqludf_sys.so lib_mysqludf_sys.c -I/usr/include/mysql编译完得到一个 .so 文件。接下来把它传到目标机。传输方式很多最经典的是用数据库自身能力实现先建一张表把 .so 文件的十六进制内容读进去再通过 SELECT ... INTO DUMPFILE 写到插件目录。但这一步有前提就是 secure_file_priv 允许否则只能走其他上传渠道。我有一次图省事直接用了 mysql 客户端自带的 LOAD_FILE 去读 /tmp 下的 .so结果怎么读都是 NULL。排查了半天发现是 secure_file_priv 被配成了 /var/lib/mysql-files而我读的文件在 /tmp 下直接被拦了。后来把文件挪到允许目录下才成功。那次之后我对“先查变量再动手”这条原则执行得非常彻底。文件进入插件目录后注册函数CREATE FUNCTION sys_exec RETURNS STRING SONAME lib_mysqludf_sys.so;然后就可以通过 select sys_exec(whoami) 来执行命令。要注意函数返回的是状态码不是命令输出想要回显需要先把结果写到文件里再读。比如SELECT sys_exec(id /tmp/udf_test.txt);之后用 LOAD_FILE(/tmp/udf_test.txt) 或直接通过其他方式读取。这一套流程下来命令执行能力就有了。但如果你在写文件时碰到权限不足别第一时间怀疑 UDF 本身先检查目标目录是不是 MySQL 进程有权限写。2.3 编译失败与版本错位我亲历的三个典型坑先说版本错位。MySQL 的 UDF 动态库对 MySQL 主版本比较敏感5.5、5.6、5.7、8.0 的接口定义有差异。网上流行的老 UDF 工具很多是基于 MySQL 5.x 写的拿到 8.0 上一加载就报错日志里会出现 undefined symbol 或者 invalid ELF header 之类。我一度以为是自己编译环境缺依赖后来发现是用了老项目源码头文件接口对不上。解决办法是找与目标 MySQL 版本匹配的源码重新编译或者用高版本兼容写法。第二个坑是文件完整性。在传输 .so 文件时如果走了不稳定的中间链路或者用 SELECT ... INTO OUTFILE 写二进制文件时被文本模式转换干扰文件已经损坏但创建函数时 MySQL 不一定立刻报错往往到调用时才崩。这也是为什么建议用 INTO DUMPFILE 而不是 INTO OUTFILE 写二进制文件前者不会做转义和换行处理。我见过有人用 INTO OUTFILE 写 .so文件头被加了东西导致加载失败排查了很久才发现是这问题。第三个坑跟权限有关。有时候插件目录对 MySQL 用户不可写你会直接在写文件阶段失败。这时候很多人会立刻放弃 UDF其实还可以看看 MySQL 是不是以 root 身份启动的如果 /etc/my.cnf 里 userroot那 MySQL 写文件的能力会大很多这本身就是运维配置失误。但这种“配置失误”不应该被当成常规依赖项因为现在很多发行版默认使用 mysql 用户跑服务能成功属于小概率事件。3. 启动项提权从“开机自启”到命令落地的三种玩法3.1 Windows侧启动文件夹、注册表、计划任务的差异Windows 下启动项提权核心是找“当前账号能写、系统启动时会自动执行”的位置。最常用的是启动目录和注册表 Run 键。当前用户的启动目录一般在这个路径%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup所有用户的启动目录在C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp如果 MySQL 以 SYSTEM 身份运行那这两个目录都能写。写个批处理文件进去系统启动或用户登录时就会执行。注册表方面常用的是HKCU\Software\Microsoft\Windows\CurrentVersion\Run HKLM\Software\Microsoft\Windows\CurrentVersion\Run用 SQL 往注册表写值不如直接写文件方便但如果你有 UDF 的 sys_exec可以用命令去操作注册表。计划任务是更隐蔽的选择schtasks 命令可以创建开机触发或定时触发的任务同样能实现自启。我自己的判断是在 Windows 目标机上如果 MySQL 是服务方式运行且 SYSTEM 身份注册表 Run 键比启动目录更稳因为不需要等用户登录。但启动目录胜在简单直观写入后几乎不需要调试。计划任务虽然灵活但创建和清理的痕迹相对多。三者之间没有绝对优劣看环境判断。3.2 Linux侧rc.local、systemd、cron各自的使用边界Linux 下启动项提权的经典目标是 /etc/rc.local。这个文件在 systemd 时代依然存在但前提是 rc-local.service 被启用很多最小化安装默认不开。所以写入 rc.local 后最好确认一下服务状态否则重启后不生效。systemd 的话可以新建一个 service 文件放到 /etc/systemd/system/ 下然后执行 systemctl enable 把它设为开机自启。比 rc.local 的优势是可控性强失败会报日志缺点是文件结构复杂容易被运维发现异常。cron 是最常被钻空子的位置。reboot 定时规则可以在系统启动后立刻跑一次而且 crontab 配置分散在不同目录比如 /var/spool/cron/ 或 /etc/cron.d/平时不太容易被注意到。MySQL 写文件能力强的话可以直接把一段脚本写进 cron 目录。但要注意cron 文件的格式和权限要求很严格权限不对会被静默忽略我吃过这个亏。Linux 下还有一个思路是写 SSH 公钥到 authorized_keys严格说这不算启动项但持久化效果类似。这类做法依赖文件权限配置不当比如 mysql 用户对某用户家目录有写权限属于另一种典型提权路径。公众号和书籍里经常把 SSH key 和启动项放一起讲原因就是它们都利用了“能写文件”这个能力。3.3 为什么启动项提权“成功率”反而比UDF更高单看成功率启动项提权确实比 UDF 更稳前提是 MySQL 进程对目标目录有写权限。原因有几个UDF 受 MySQL 版本、插件接口、文件格式三重约束而启动项只要求“把一段文本/脚本写到指定位置”不存在编译兼容问题UDF 一旦加载失败可能导致 MySQL 进程崩溃影响面大启动项最多就是不生效不会直接影响数据库服务。举个例子我在一次测试里遇到 MySQL 5.7、root 权限、secure_file_priv 为空但插件目录权限被安全加固锁死写 .so 进去直接 permission denied。这种情况下 UDF 路线直接断掉但我发现 MySQL 进程是 root 启动的于是往 /etc/cron.d/ 写了一个反弹执行的任务重启后顺利拿到系统命令执行能力。那次之后我在判断路径时把“我能写哪”放在“我能执行什么”前面。不过启动项也有自己的短板一是需要系统重启或用户登录才生效时效性差二是写入的痕迹相对明显如果目标环境有文件完整性监控很容易暴露三是如果启动项脚本一次性执行完不清理会一直驻留容易造成生产事故。所以它适合做持久化不适合做“一击必杀”的临时提权。4. 动手前先做一轮信息收集和条件判断4.1 环境判断我平时会先确认的四张“底牌”很多朋友问我要“通用提权程序”实话讲没有。每条路径都是环境条件推出来的。我自己的流程是先确认四件事全部确认完再选路线。第一当前数据库账号权限。查询当前用户和权限可以用SELECT user, host FROM mysql.user WHERE user CURRENT_USER(); SHOW GRANTS FOR CURRENT_USER();重点是确认有没有 FILE 权限、有没有 root 权限、能不能创建函数。如果只有 SELECT 权限那 UDF 基本没戏启动项也大概率没戏。第二MySQL 进程身份。在系统层执行 ps/pstree 看进程归属或者看配置文件里的 user 字段。Windows 下可以看服务属性里的“登录为”一栏。进程是 root 或 SYSTEM和进程是 mysql/nobody提权后的价值完全两回事。第三secure_file_priv 和 plugin_dir 的现值。这两条 SQL 前面提过建议写进自己的信息收集清单里。如果 secure_file_priv 是 NULL 但你有 shell可以尝试直接操作目录如果没有 shellUDF 的文件写入就断在这一步。第四可以写哪些目录。这个要结合系统权限来看。拿到 FILE 权限后SELECT ... INTO DUMPFILE 能写的位置受 secure_file_priv 限制但如果配合系统命令执行能力实际可写范围取决于 MySQL 进程的系统身份。我常用的验证方式是用 UDF 或 SQL 尝试在 /tmp 写一个文件看是否成功。4.2 从现象反推可行路径信息收集完之后最重要的是组合判断。我列几种常见情况情况可用路径核心风险有 rootsecure_file_priv 为空插件目录可写UDF 优先版本兼容有 rootsecure_file_priv 为 NULL进程是 SYSTEM/root启动项/计划任务优先时效性差有 FILE 权限进程是普通用户先看普通用户能不能写启动目录提权价值有限只有查询权限两条路都走不了只能找其他漏洞别硬试从现象反推有一个好处不会在一条死路上浪费时间。比如你已经确认 secure_file_priv 是 NULL那就不必纠结为什么写不了 .so直接把力气挪到启动项或其他路径上。我见过最可惜的情况是目标机明明可以用启动项拿到系统权限测试人员却在 UDF 上卡了一天。这里补充一个细节MySQL 8.0 之后对 UDF 的限制更严格而且 plugin_dir 的默认权限也变了很多老教程直接失效。如果你扫描发现对方是 8.x别直接套 5.x 的 UDF 打法先关注版本再说。5. 攻防视角下的检测、清理与加固清单5.1 让UDF失效的几类常见截断手段其实不光是测试人员运维更需要关心 UDF 有没有被塞进来。检测的思路很简单查 mysql.func 表看有没有不该存在的自定义函数。SELECT * FROM mysql.func;正常业务系统里这张表基本是空的。如果看到 sys_exec、sys_eval、sys_get 这类函数基本可以判定被植入了。另外插件目录里如果出现陌生的 .so/.dll 文件也值得高度警惕。清理过程要彻底先删除函数再删除文件。删除函数用 DROP FUNCTION sys_exec删完之后把插件目录里的 .so 文件也清掉否则下次还能重新创建函数。这里有个小坑DROPFUNCTION 之后如果文件还在遇到二次植入时攻击者只需要重新 CREATE FUNCTION 即可等于门还开着。加固方面secure_file_priv 是重点。把这个变量显式设置成指定目录比如 /var/lib/mysql-files并确保该目录只允许 MySQL 写入自己需要的文件就能挡掉一大半文件写入路径。插件目录的权限也要收紧确保 mysql 系统账号对目录只有读权限除非确实需要在线安装插件。5.2 启动项区域的审计与清理启动项提权的痕迹多半分散在操作系统的各个自启位置。Windows 下可以用系统自带的 autoruns 类工具也可以手动检查启动目录和注册表 Run 键。Linux 下重点检查 /etc/rc.local、/etc/cron.d、/var/spool/cron、systemd 服务目录尤其是创建时间异常的文件。我自己在 Linux 上排查时会先看一遍 crontabcrontab -l ls -la /etc/cron.d/再看 rc.local 的内容确认没有可疑命令。systemd 的话不一定要逐个看 service 文件可以先看最近被 enable 的服务列表再对可疑项展开检查。关键是建立一条基线什么服务是本来就该有的什么文件是数据库创建的心里要有数。清理启动项后一定要把原始配置恢复否则可能导致系统无法正常启动。这一点在真实业务环境里尤其重要测试归测试不能把别人生产系统弄瘫。5.3 写给我的同行你要有的技术底线技术本身是中性的但使用技术的人得有边界。UDF 和启动项提权这类内容测试环境和生产环境完全是两回事。没有书面授权的系统碰都不要碰有授权的测试也要提前明确测试范围、时间窗口、风险等级。拿到权限之后优先做验证和记录而不是顺手留后门。真正专业的安全人员是用最短路径证明风险存在然后帮助对方把洞补上。我个人现在的习惯是每次做完这类测试都会写一份包含“路径判断、具体操作、影响范围、修复建议”的复盘文档。这样既方便自己下次参考也能直接交付给运维作为加固依据。技术能力会随着项目变强但职业口碑取决于你能不能把每一次操作控制在约定范围内。最后分享一个我个人很受益的小习惯所有 SQL 和命令的记录按时间线保存下来标注当时的判断依据。这个习惯帮我复盘了很多次“当时为什么选了这条路”的决策过程。提权这条路资料很多但真正值钱的是你在一台具体机器上的完整判断链。希望这篇内容能帮你少踩几个我踩过的坑。