ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL权限提升双路径:UDF与启动项提权原理及防御

MySQL权限提升双路径:UDF与启动项提权原理及防御 MySQL 数据库的权限提升问题几乎是内网安全评估里绕不开的一环。UDF 提权和启动项提权一个利用了 MySQL 扩展函数的信任机制一个利用了 Windows 启动项的自动执行逻辑两者思路截然不同但都是拿到数据库权限之后往系统权限跨的经典路径。这篇文章不以“攻击教程”为目的而是从安全研究和防御加固的视角把这两条路线的原理、触发条件、关键细节和对应的排查方法拆开揉碎讲清楚希望能帮做安全、做运维的朋友真正理解这件事的来龙去脉。1. UDF 提权的底层逻辑MySQL 为什么会听话执行系统命令1.1 MySQL UDF 机制到底是什么UDF 的全称是 User Defined Function也就是用户自定义函数。MySQL 本身内置了大量函数比如COUNT()、SUM()、NOW()但业务场景千奇百怪内置函数不一定够用于是 MySQL 给了开发者一个扩展口子允许用户自己写一个动态库文件Linux 下是.soWindows 下是.dll把编译好的文件放进 MySQL 的插件目录然后通过CREATE FUNCTION语句注册成可调用的函数。这个设计本身是合理的和 Linux 的动态链接库、Windows 的 DLL 机制没有本质区别。但问题在于MySQL 对“谁来加载这个库文件”的管理非常宽松。只要当前数据库账号拥有INSERT权限并且plugin_dir目录可写那么这个账号就能把一个精心构造的动态库文件写入插件目录再把它注册成函数。当这个函数被执行时它就跑在了 MySQL 进程的权限上下文中。换句话说如果 MySQL 是以root或SYSTEM权限启动的那么通过 UDF 调用系统命令时执行命令的进程就是 MySQL 进程权限自然也就是root或SYSTEM。这就完成了从“数据库账号”到“操作系统高权限账号”的越权。1.2 提权为什么能成功权限边界与信任模型我最早接触这个漏洞时一直有个疑问为什么数据库账号能写操作系统文件这中间是不是缺了一道墙后来想明白了MySQL 的设计里数据库权限和操作系统权限本来就被默认为“同一边的”。MySQL 的FILE权限给用户开放了SELECT ... INTO OUTFILE能力本意是允许用户把查询结果导出到文件这在数据备份、报表生成里是常见的操作。但“导出到文件”意味着什么意味着数据库用户可以往操作系统里写文件。再加上plugin_dir目录的写入路径一旦有账号能写文件它就能把动态库写进插件目录。而CREATE FUNCTION语句又允许这个账号把动态库里的符号注册成 SQL 函数。整个链条在业务逻辑上每一步都“合法”但组合在一起就变成了一条完整的提权链路。这里面最核心的信任模型问题是MySQL 默认信任了“能写数据库文件的人”就是“可信的人”。但在真实的内网环境里数据库账号往往因为应用配置不当、弱口令、Sql 注入等原因被攻陷攻陷之后的数据库账号并不是可信身份。所以这条信任链本身就站不住脚。1.3 触发条件自查清单不是任何 MySQL 环境都能提权成功的UDF 提权有几个硬性前置条件缺一个都不行。条件项说明MySQL 账号权限需要FILE权限以及INSERT权限用于写入文件同时mysql库上要有CREATE FUNCTION权限插件目录可写plugin_dir参数指向的目录需要被 MySQL 进程用户具有写权限换句话说 MySQL 能在这个目录里创建文件MySQL 进程权限MySQL 运行的操作系统用户越接近管理员权限提权后的效果就越强Windows 上常见的是SYSTEMLinux 上常见的是rootsecure-file-priv 限制如果该参数被配置为NULL或指向了特定目录则INTO OUTFILE会受限这会影响文件写入路径MySQL 版本不同版本对plugin_dir的默认路径和函数注册语法有差异部分新版本限制了plugin_dir的写入这些条件里最容易被人忽略的是secure_file_priv。很多管理员知道要设这个参数但只设成了一个固定目录结果这个目录恰好是 MySQL 的数据目录带来了新问题。后面我会专门讲怎么配置才算合理。2. UDF 提权的核心实操细节还原2.1 先还原一个最基础的测试环境做安全测试我的习惯是先搭一个干净的实验环境把攻击链路完整走一遍才能理解每一步的限制条件在哪里。推荐用 Windows Server 虚拟机装 MySQL 5.7老的 5.5 和 5.7 版本对plugin_dir的写入限制最松最容易复现然后把 MySQL 注册成 Windows 服务以SYSTEM权限启动root账号密码设为空或极弱密码。你可能会问真实环境里哪有这么理想的条件但安全测试的意义恰恰在于先确认在理想条件下攻击链路能走通再逐步加条件限制看哪一步会被拦下来。我在实际评估里见过太多案例问题不是出在攻击手段不够高明而是防线配置里存在一个不起眼的疏漏比如root账号的空密码配了一个外网可访问的端口。2.2 编译和准备 UDF 库文件理解原理比拿到现成文件更重要UDF 提权需要一个动态库这个库文件里至少要导出一个函数比如sys_eval、sys_exec、sys_get之类的。最核心的函数是sys_eval()它接收一个字符串参数把它当成系统命令执行然后返回执行结果。另一个常用的是sys_exec()只执行命令但不回显结果适合用于反弹 Shell 或写入后门。网上能直接找到很多已经编译好的 UDF 库但我必须说一句自己从头编译一次或者至少在测试环境里打开源码看一眼非常值得。为什么因为 UDF 库和 MySQL 的版本、操作系统的位数32/64位、字符集都有关系版本不对直接加载失败。另一个原因是安全层面的——你无法验证网上流传的二进制文件里有没有夹带私货我从来不建议在任何真实环境里直接加载来历不明的动态库这是底线问题。自己写一个最简单的 UDF 库并不复杂。核心需要实现 MySQL 要求的几个接口xxx_init、xxx_deinit、xxx。其中xxx是函数主体接收 SQL 层传进来的参数在 C/C 中调用system()或popen()就可以完成命令执行。我这里不展开恶意代码写法重点是想说明整个机制中的危险点不是 SQL 语句本身而是“允许数据库用户注册一个可以执行系统命令的函数”这一能力和“把动态库写进插件目录”这一能力的组合。2.3 把 UDF 文件上传到目标 MySQL 的几种方式和限制文件上传是整个流程里最讲究的一环。MySQL 提供了SELECT ... INTO OUTFILE作为写文件的途径所以我最早测试时先确认plugin_dir的路径SHOW VARIABLES LIKE plugin_dir;拿到插件目录绝对路径后思路就明确了利用INTO OUTFILE把 UDF 库的二进制内容写入到这个目录里。但是二进制文件怎么通过 SQL 语句写入有两个办法一种办法是把 UDF 库的十六进制编码硬编码到 SQL 语句里用SELECT 0x4D5A9000... INTO OUTFILE 路径这种方式写。这种办法对 SQL 语句的长度限制有要求max_allowed_packet默认是 4MB 或 64MB超出会失败所以库文件不能太大。另一种办法是先把库文件转成文本格式比如 Base64 编码然后分块写入再用工具还原。这种方式更稳但操作步骤更多。这里有一个非常关键的权限边界INTO OUTFILE能创建的目录必须是 MySQL 进程用户有权限写的目录。默认情况下MySQL 的安装目录对SYSTEM或root都是可写的所以这一关通常是通的。这也是为什么很多实战环境里plugin_dir就是最理想的上传目标。2.4 注册函数和版本差异为什么同样的语句在 5.7 和 8.0 上结果不一样文件写进plugin_dir后下一步就是注册函数。老版本 MySQL5.x的注册语句是CREATE FUNCTION sys_eval RETURNS STRING SONAME udf.dll;注意这里用的是SONAME关键字指定动态库文件名。到了 MySQL 8.0 里SONAME被弃用了需要用CREATE FUNCTION sys_eval RETURNS STRING SONAME udf.dll的替代写法实际新语法要求使用... SONAME ...已经不再支持官方建议直接使用动态库的路径或名称字段。这就导致同一个 UDF 库在 5.7 能秒加载换到 8.0 就报错。还有一点容易被忽略MySQL 8.0 默认移除了plugin_dir的可写性吗其实没有plugin_dir是否可写取决于目录的 ACL 设置和版本没有必然关系。但 MySQL 8.0 在安全默认值上确实更严格了比如secure_file_priv在 8.0 中如果未显式配置默认是NULL禁止导出。这是两个不同的限制维度别混为一谈。注册成功后执行命令极其简单SELECT sys_eval(whoami);如果回显是SYSTEM或root说明提权链路成功。如果报错说函数不存在或者找不到符号那就回过去查动态库文件是否真的被完整写入、位数是否匹配、MySQL 版本兼容性是否满足。3. 启动项提权UDF 之外的另一种思路3.1 为什么启动项能成为提权入口UDF 提权依赖 MySQL 本身具备执行系统命令的合法接口但并不是所有 MySQL 环境都能找到合适的 UDF 库文件或者plugin_dir根本无法写入。这时候就要换个思路既然 MySQL 数据库账号有能力往操作系统里写文件那能不能把文件写到一个“系统会自动执行”的位置Windows 系统有一个机制启动文件夹里的内容会在系统启动时自动执行。如果把一个恶意程序或脚本写进启动文件夹系统下一次重启时就会自动运行它而运行它的权限就是当前登录用户的权限。如果 MySQL 恰好以SYSTEM权限运行同时又有写文件的权限那就能往启动文件夹里写入一个可执行文件或脚本等系统重启后以SYSTEM权限执行。这个思路的本质是“延迟执行”攻击者不要求在当下立刻拿到高权限命令执行而是把恶意文件预置到一个系统信任的执行路径里等待系统重启或用户登录来触发。3.2 常见可写启动项目录以 Windows 为例常见的启动项写入位置有这几个C:\Users\用户名\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\StartupC:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp通过注册表HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run或HKEY_LOCAL_MACHINE对应的 Run 键值写入MySQL 进程如果以SYSTEM权限运行理论上对上述所有位置都有写权限。从我实际测试的情况看最容易成功的是ProgramData下的启动文件夹因为它对当前登录的所有用户都生效只要系统里有用户登录就会执行。这里需要特别说明MySQL 的INTO OUTFILE写文件只能写在服务器本机而且是用 MySQL 进程的操作系统身份写的。所以写入启动目录的前提是MySQL 进程对这个目录有写权限。SYSTEM权限下通常是满足的。3.3 利用的关键步骤与常见坑点用启动项提权我踩过不少坑有几个细节特别需要注意。第一个坑是文件内容格式。INTO OUTFILE写入的是文本数据如果要把一个.bat脚本写进去需要注意换行符。Windows 下要求的是\r\n如果 SQL 语句里直接写\n生成的脚本可能在某些解释器下报错。我当时用的是CHAR(13,10)来拼换行符这个细节能省很多排查时间。第二个坑是权限生效的时机。写进启动文件夹不会立刻执行必须等系统重启或者用户重新登录。所以在测试环境里写好之后需要重启虚拟机才能验证效果。但是真实的内网环境中你不可能为了验证一个概念去重启业务服务器所以启动项提权更适合作为持久化手段的备选方案而不是第一时间的利用路径。第三个坑是杀软和行为检测。现在很多终端安全软件对启动目录的写入行为非常敏感数据库进程往启动目录写文件本身就是高危行为很容易触发告警。我见过一些案例就是攻击者把文件写进启动目录之后还没来得及重启文件就被安全软件隔离了。所以现在的攻防视角下更多人倾向于用“服务创建”或者“计划任务”的方式做持久化检测难度相对更高。4. 从防御视角出发怎么判断环境是不是“看起来安全实则高危”如果你是一个数据库管理员或者安全工程师看到这里更关心的一定是怎么防。我用一个实际评估案例来展开说明。有次给一家企业做内网安全评估对方数据库管理员自信地说“我们设置了强密码外网也不开放”但我打开配置一看root账号是只有一个弱密码没错但应用的连接账号居然也有FILE权限。应用账号一旦被注入攻击利用照样能走 UDF 提权链路。“密码强”不等于“权限边界清晰”这两件事差远了。4.1 最小权限原则从账号权限上切掉提权路径防 UDF 提权的第一件事不是去调plugin_dir而是把所有应用连接账号的权限收敛到最小。绝大多数业务场景根本不需要FILE权限更不需要mysql库上的CREATE FUNCTION权限。具体可以这样检查当前环境里有哪些账号权限异常SELECT user, host, file_priv, insert_priv, create_func_priv FROM mysql.user;普通业务账号的file_priv和create_func_priv都应该是N。如果你发现某个账号同时拥有这些权限那不管密码多强这个账号都是高风险账号因为 SQL 注入一旦发生攻击者拿到的是一个“具备提权能力”的账号。4.2 secure-file-priv 与 plugin_dir 的合理配置secure_file_priv参数控制LOAD DATA和INTO OUTFILE等文件读写操作。合理的设置是把它指向一个非 MySQL 程序目录的专用目录secure_file_priv/var/lib/mysql-files/如果把secure_file_priv留空表示不限目录或者设成NULL表示禁止导出都各有风险。留空意味着任意目录都能写配合其他漏洞就是灾难设成NULL虽然最安全但某些数据导出业务会跑不起来。折中方案就是指向专用导出目录。plugin_dir也是一样检查一下它指向的路径是否有额外的写入访问控制。如果 MySQL 以root运行plugin_dir默认目录对 root 当然可写所以关键还是让 MySQL 以最小权限的系统账号跑而不是root或SYSTEM。提示在 Linux 下创建专门的mysql系统用户来跑 MySQL 服务并把数据目录、插件目录的所有权都划拨给该用户这一步能直接把 UDF 提权的杀伤力从“root 权限”降低到“mysql 用户权限”。4.3 日常检测与排查如果你怀疑环境已经被“照顾”过可以执行下面的 SQL 检查 UDF 函数是否被注册过SELECT * FROM mysql.func;正常情况下这个表应该是空的。如果发现有sys_eval、sys_exec、sys_get之类的自定义函数基本可以确认是妥妥的疑似入侵行为。此时应立刻确认这个函数是谁注册的、什么时候注册的同时检查plugin_dir目录下是否有可疑的动态库文件。Windows 下还可以检查系统启动目录下是否有异常的脚本、可执行文件。可疑的.bat、.vbs、.exe都需要重点排查尤其是创建时间集中在同一个时间段内、文件名伪装成系统服务的文件。5. 常见问题与排查经验速查我整理了一张表把实际操作中最高频的几个问题列出来方便你直接对照排查。现象可能原因排查和解决思路CREATE FUNCTION报错“此函数需要动态库”动态库文件没有被复制到plugin_dir确认写入路径是否正确静态检查SHOW VARIABLES LIKE plugin_dirUDF 函数执行时报“找不到符号”动态库位数不对或函数名不匹配确认数据库是 32 位还是 64 位查看动态库导出符号表注册成功但执行无回显调用的命令没有把输出返回给 SQL 层确认函数实现用的是popen还是system前者回显后者不回显INTO OUTFILE写入失败secure_file_priv限制了写入路径检查secure_file_priv参数值确认目标目录是否在白名单内写入后重启系统脚本没有执行启动目录不对或脚本格式有误确认写入的是当前用户的启动目录还是公共启动目录检查换行符杀软拦截了写入行为终端安全管理软件对敏感目录写入做了行为监控这不是系统漏洞问题而是已处于被检测能力覆盖的范围5.1 我对排查工作最深的三点体会第一点先看版本再看报错。MySQL 5.x 和 8.x 在函数注册语法、plugin_dir默认路径上都有差异很多网上找的文章还停留在老版本照搬就踩坑。确认版本是一切排查的前提。第二点安全意识要落实到“权限边界”而不仅仅是密码强度。我去过很多企业密码都满足复杂度要求了但各类账号权限松得像筛子。UDF 提权能做成功绝大多数情况下不是攻击者技术多厉害而是账号权限给得太松。第三点安全测试一定要留痕和授权。文章里所有内容都应该在你有书面授权的测试环境里操作。在未授权系统上尝试这些方法是明确的违法行为。任何时候安全能力都应该服务于建设而不是破坏。写在最后的个人经验我入行这么久见过很多环境里的 MySQL 都是“一把 root 跑天下”数据目录、插件目录、导出目录全放一起。UDF 提权和启动项提权之所以能一用一个准本质原因不是漏洞多深而是安全基础太薄。把该收敛的权限收敛掉把该设的参数设对这两条提权路径就断了一大半。剩下的就是靠安全团队和运维团队对异常行为的敏感度了。真心建议所有维护数据库的同行花五分钟检查一下你的mysql.func表看看你的secure_file_priv再看看你的启动目录——这几个点能堵住绝大多数你想象不到的麻烦。
返回列表