sgkdev/ipv6_frag_escape
GitHub: sgkdev/ipv6_frag_escape
针对 CentOS/RHEL 10 的非特权容器逃逸 PoC,利用已修复的 IPv6 分段 slab 溢出漏洞实现从容器内非特权进程到宿主机 root shell 的完整利用链。
Stars: 51 | Forks: 14
# IPV6_FRAG_ESCAPE
一个针对 CentOS / RHEL 10 的可靠非特权容器 / 沙箱逃逸概念验证。
它利用了 `__ip6_append_data()` 中一个现已修复的 IPv6 分段漏洞(上游已通过
`38becddc` 关闭,无 CVE),这是一个位于数据包自身 head 对象尾部的
`skb_shared_info` 中的 slab 线性溢出。本 README 仅记录了漏洞利用链。
并不涵盖触发过程。
## 范围
- 目标:CentOS / RHEL 10(内核 6.12.x,例如 `6.12.0-242.el10`)。
- 起点:位于网络隔离容器内的非特权进程,并具有访问
非特权 user namespace 的权限。
- 结果:在宿主机的初始 namespace 和 root 文件系统中获得一个交互式 root shell。
## 注意事项
这是一个概念验证,刻意并非一键式武器。为了可靠性的脚手架
被有意省略:没有针对每 CPU 的 PCP 内存整理,没有跨缓存 SLUB 加固,并且
`skb` head 被放置在一个共享的、并非那么安静缓存中。所提供的代码触发频率
足以证明该利用链,仅此而已。它仅限于 CentOS / RHEL 10。
## 要求
- `CONFIG_INIT_ON_ALLOC_DEFAULT_OFF`,这是 RHEL / CentOS 的默认设置。此 PoC 在未初始化的 slab 字节中植入一个过期
指针;如果设置了 `init_on_alloc=1`,该槽位会被清零,并且这种
特定技术只会导致内核崩溃(一个 NULL 解引用)。这是该
技术的限制,而不是漏洞本身的限制:一旦修复和 CVE 公开,一个涵盖这些内核的完整漏洞利用程序,将会使用不同的
技术随之而来。
- 5 级分页(LA57,57 位线性地址)。页表遍历是为五级
硬编码的,并且启动检查在缺少它时会拒绝运行。4 级 CPU 需要重构
遍历逻辑。
- 存在 `/sys/kernel/btf/vmlinux` 且对所有用户可读(RHEL / CentOS 默认提供)。
## 已测试的内核
| 发行版 | 内核版本 | 结果 |
| ----------------- | ------------------ | ---------------------- |
| CentOS Stream 10 | `6.12.0-242.el10` | root,容器逃逸 |
| RHEL 10 | `6.12.0-228.el10` | httpd_t 上下文逃逸 |
## 漏洞利用链
1. **Slab 内溢出转为 self-UAF。** 该漏洞允许控制 `skb_shared_info` 中的单个 `nr_frags`
字节。释放路径 (`skb_release_data()`) 会遍历
`frags[0 .. nr_frags)` 并对每个条目执行 `put_page()`,而 `frags[]` 从未被初始化。
我们通过受控的缓存复用,将一个指向 pipe buffer page 的 `struct page *` 预先植入到该 slab 槽位中,
然后将 `nr_frags` 设置为 1,因此销毁过程会释放一个我们仍然拥有的
页面的引用。线性溢出变成了一个 page use-after-free。该溢出从不触碰
`frags[0]`,因此不需要针对内存喷射进行精确计时,这也是使其具有高容错性的主要原因。
2. **Page UAF 转为 Dirty-Pagetable。** 被释放的 pipe page 通过触发一个新的匿名映射,被重新分配为最后一级页表。一个物理页面现在既是活跃的叶子
页表,也是 pipe 仍然进行读写的页面。向 pipe 写入八个字节
会安装一个伪造的 PTE;读取 pipe 则会读回该表。这是一个有限的任意
物理读写,每个表大约有 460 个 PTE 窗口。
3. **绕过 KASLR。** 借助物理读取功能,我们扫描了固定在低内存的 SMP trampoline
页表,KASLR 从未重定位它;其内核半区的条目指向
`level4_kernel_pgt`,即内核的物理基地址。从那里开始,`init_top_pgt`(可通过其自引用的条目 511 识别)成为了一个通用的虚拟到物理转换器,
恢复了内核虚拟基地址并将我们的地址空间与物理内存绑定在一起。
4. **从有限到无限读写。** 我们在叶子表中伪造了一个指向
该表自身物理地址的 PTE。匹配的虚拟窗口随后别名为页表
本身,因此 PTE 变成了普通内存:无限的、随机访问的内核读写,且循环中不需要
pipe。Ring 3 的 TLB 一致性是通过使用超大的 `mprotect()` 强制进行完整的 mm 刷新来处理的。
5. **解析偏移量并获取凭证。** 结构体成员偏移量是在运行时从
`/sys/kernel/btf/vmlinux` 读取的,而不是硬编码的。我们通过 vmemmap `struct page` 遍历和 `mm_struct.owner` 定位自身的 `task_struct`,
将 credential id 清零,并填充 `cred` 和 `real_cred` 上的所有
capability 集。符号地址来自一个内部解析器,该解析器通过我们的任意读取
解析内核自身的 `kallsyms` 表;只有在该操作失败时,我们才会回退到将 `cred.user_ns` 指向
`init_user_ns`(这样 `CAP_SYSLOG` 在初始 namespace 中仍然有效),并从 `/proc/kallsyms` 读取真实地址。
6. **在不翻转 enforcing 的情况下禁用 SELinux。** 我们使用 `xor eax, eax ; ret` 存根覆盖了
`avc_denied()` 的序言(在 IBT 下跳过 `endbr64`)。
现在,每个拒绝请求都会在整个进程范围内返回已批准,而 `getenforce` 仍然报告
`Enforcing`。这也是逃逸的先决条件:否则,核心转储处理程序的跨
域重新执行将被拒绝。
7. **通过 core_pattern 逃逸。** 我们用一个通过 `/proc//root`(崩溃任务的容器 chroot)指向
我们自己二进制文件的 `|` 前缀处理程序覆盖全局 `core_pattern`,然后使一个子进程崩溃。内核将处理程序作为
usermodehelper(初始 namespace 中的 root)运行,我们通过绑定在容器内部的 Unix socket 中继一个交互式 root shell。处理程序诞生于初始
namespace 内部,因此不需要原位 namespace 修改,也没有任何 PID namespace 带来的风险。
标签:VX技术, Web报告查看器, 内核漏洞, 安全, 安全渗透, 客户端加密, 容器逃逸, 本地提权, 超时处理