禁用create function权限比删函数更重要,因为只要该权限存在,攻击者就能随时注册新udf并上传恶意.so文件;而删函数仅清除已有记录,无法阻止重建。

为什么禁用 CREATE FUNCTION 权限比删函数更重要
很多人一听说UDF提权,第一反应是查 mysql.func 表、删掉 sys_eval 或 lib_mysqludf_sys.so ——但这只是擦黑板,不是关灯。只要攻击者还能执行 CREATE FUNCTION,就能立刻注册新函数;只要文件系统可写,就能重新上传恶意 .so 文件。
真正起效的防线在权限层:
-
CREATE FUNCTION是注册 UDF 的必要前提,必须从所有业务账号中彻底回收 - 检查命令:
SHOW GRANTS FOR 'app_user'@'%';,重点看是否含CREATE ROUTINE(它隐含CREATE FUNCTION) - 回收语句必须用:
REVOKE CREATE ROUTINE ON *.* FROM 'app_user'@'%';(注意不是ON mydb.*) - 即使应用只读,也绝不能授予
EXECUTE权限——它允许调用已存在的 UDF
secure_file_priv 不是摆设,但配置错等于没设
@@secure_file_priv 控制 SELECT ... INTO OUTFILE 和 LOAD DATA INFILE 的读写路径,但它不是“开关”,而是“栅栏”。设错值反而会制造假安全感。
常见错误配置和修复方式:
- 值为
NULL或空字符串:MySQL 8.0+ 已禁止,但旧版本仍存在,必须在my.cnf中显式设为有效路径(如/var/lib/mysql-files/)并重启 - 设为
/或/tmp:攻击者可直接写入/tmp/lib_pwn.so,再通过符号链接绕过插件目录限制(MySQL 8.0.29+ 已禁用符号链接,但旧版本仍需人工检查) - 正确做法:设为仅
mysql用户可写的空目录(如/var/lib/mysql-udf-block/),然后chown mysql:mysql /var/lib/mysql-udf-block && chmod 700 /var/lib/mysql-udf-block - 验证命令:
SELECT @@secure_file_priv;和SELECT @@plugin_dir;必须分开确认,二者路径不能重叠或可交叉写入
UDF 文件本身要清理,但得按规则删
UDF 提权依赖两个实体:数据库里的注册记录(mysql.func 表)和磁盘上的共享库文件(.so 或 .dll)。只清表不删文件,下次 CREATE FUNCTION 就能复用;只删文件不清表,MySQL 启动时会报错但不崩溃,且攻击者可重新上传。
清理步骤必须闭环:
- 先查注册项:
SELECT name, dl FROM mysql.func;,对每个结果执行DROP FUNCTION IF EXISTS `name`; - 再找文件:
find /var/lib/mysql* -name "*.so" -o -name "lib_mysqludf_*" 2>/dev/null,逐个rm -f - 检查插件目录权限:
ls -ld $(mysql -Nse "SELECT @@plugin_dir"),确保属主是mysql,权限 ≤755(写权限只给mysql用户) - 重启 mysqld 后,再跑一次
SELECT name FROM mysql.func;确认为空
应用层 SQL 注入才是真正的入口,配置加固只是延缓器
所有 UDF 提权案例,起点都不是 DBA 忘记关某个开关,而是 Web 应用把用户输入拼进 SQL。哪怕你把 secure_file_priv 设得再严、CREATE FUNCTION 收得再死,只要存在一处 WHERE id = " . $_GET['id'] 这样的拼接,攻击者就能用 UNION SELECT 写文件、用报错注入读取敏感信息、甚至直接反弹 shell。
堵住这个口子没有捷径:
- PHP 必须用
PDO::prepare()+bindValue(),禁用mysql_query()和mysqli_query()直接拼接 - Python 用
cursor.execute("SELECT * FROM users WHERE id = %s", [user_id]),拒绝任何形式的f-string或.format()拼 SQL - Java 用
PreparedStatement,严禁Statement.execute(sql + input) - ORM 框架里禁用原生 SQL 方法:比如 Django 的
extra()、SQLAlchemy 的text()若含变量拼接,一律视为高危
最常被忽略的一点:开发人员以为“我用了 ORM 就安全了”,结果在日志埋点、动态排序字段、IN 查询构造时又回到字符串拼接——这些地方恰恰是 UDF 提权的跳板。











