CVE-2023-32233
一、漏洞简介
CVE-2023-32233 是 Linux 内核 Netfilter 子系统中的一个释放后重用(Use-After-Free,UAF)漏洞,存在于 nf_tables 组件中。该漏洞源于匿名集(anonymous set)处理不当,导致在处理批量请求更新 nf_tables 配置信息时,可能发生内存释放后仍被访问的情况。攻击者可以利用此漏洞对内核内存执行任意读写操作,从而将自身权限提升至 root,完全控制目标系统。
UAF漏洞:UAF 漏洞全称是use after free,free是指函数是在堆上动态分配空间后不再使用该改数据从而被回收。但是由于程序员的一些不适当的操作,会导致攻击者能够操控已经被释放的区域,从而执行一些byte codes。
Linux Netfilter:Netfilter 是 GNU/Linux 下第三代防火墙,从 Linux Kernel 2.4 开始,就把 Netfilter 框架作为默认防火墙加入到 Linux 内核当中,即 Netfilter 是 Linux Kernel 的一个子功能,专门用来增强网络安全。
详细知识可参考:什么是 Linux Netfilter
二、环境安装
环境下载地址,网传做好的镜像,并包含了编译好的exp:Ubuntu-23.04,密码123456
https://pan.baidu.com/s/1yQUrToQTiaDXcsHYVeKeSQ?pwd=8888
三、漏洞复现
EXP下载地址:https://github.com/Liuk3r/CVE-2023-32233?tab=readme-ov-file
执行以下的命令安装编译环境
sudo apt install gcc libmnl-dev libnftnl-dev
编译文件:
gcc -Wall -o exploit exploit.c -lmnl -lnftnl

当然我们也可以直接使用下载的镜像中已经编译好的exp
./exp

可以看见我们已经提权,拿到了root权限

四、漏洞分析
漏洞成因是 nf_tables_deactivate_set 在释放匿名 set 时没有将 set 的标记设置为 inactive,导致它还能被此次 netlink批处理中的其他任务访问,从而导致 UAF,介绍该漏洞需要先对 netlink 的源码进行分析。
(源码版本: linux-6.1.1.tar.gz)
1、源码分析
用户态进程可以一次提交多个 netlink 请求给内核,这些请求在内存中按顺序存储,请求的存储结构为 struct nlmsghdr ,下发请求后内核通过 nfnetlink_rcv_batch 解析每个请求并处理

用户态填充和发送请求的大致代码如下:
struct mnl_nlmsg_batch *batch = mnl_nlmsg_batch_start(mnl_batch_buffer, mnl_batch_limit);
nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
table_seq = seq;
mnl_nlmsg_batch_next(batch);
// 在批处理中新建请求
struct nlmsghdr *nlh = nftnl_nlmsg_build_hdr(
mnl_nlmsg_batch_current(batch),
NFT_MSG_NEWSETELEM,
NFPROTO_INET,
NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK,
seq++
);
nftnl_set_elems_nlmsg_build_payload(nlh, set);
mnl_nlmsg_batch_next(batch);
// 发送请求给内核处理
if (mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
mnl_nlmsg_batch_size(batch)) < 0) {
err(1, "Cannot into mnl_socket_sendto()");
}
mnl_nlmsg_batch_stop(batch);
netlink 批处理消息的处理流程涉及两个线程,nfnetlink_rcv_batch 在进程的系统调用上下文中执行对请求处理后,将请求转换为 trans 通过 nf_tables_destroy_list 提交给 nf_tables_trans_destroy_work 内核线程做进一步处理。

nfnetlink_rcv_batch 处理流程
-
消息循环处理
- 逐条解析 skb 中的 Netlink 消息,直到消息长度不足头部大小。
- 对每条消息:通过
nfnetlink_find_client根据type查找对应的客户端回调表(nf_tables_cb)。调用nc->call执行消息对应的处理函数(如增删表/链/规则等)。
-
事务提交阶段
所有消息处理完成后,调用ss->commit(即nf_tables_commit)。- 遍历
commit_list中的事务(trans): - 按事务类型处理:例如
NFT_MSG_DELSETELEM调用nft_setelem_remove删除集合元素。 - 调用
nf_tables_commit_release: - 将
commit_list中的事务转移到nf_tables_destroy_list。 - 调度工作队列
nf_tables_trans_destroy_work异步处理销毁逻辑。
- 遍历
nf_tables_trans_destroy_work 销毁流程
- 事务销毁准备
- 将
nf_tables_destroy_list合并到本地链表head,避免并发冲突。
- 将
-
遍历
head中的每个事务(trans):- 调用
nft_commit_release(trans):根据事务类型执行最终的资源释放操作(如内存回收、引用计数更新等)。
- 调用
关键机制分析
-
异步销毁设计
- 通过工作队列延迟资源释放,避免在主处理路径(如规则配置)中阻塞,提升性能。
- 分离事务提交(commit)与销毁(destroy)阶段,确保原子性和一致性。
-
链表操作与同步
- 链表转移:
list_splice_tail_init将commit_list原子转移到destroy_list,防止中间状态不一致。list_splice_init在销毁线程中安全获取待处理事务。- 锁机制:隐含使用内核锁(如 RCU 或自旋锁)保护链表操作,需确保在并发场景下无竞态条件。
-
事务生命周期
-
创建:在
nc->call处理阶段分配trans并加入commit_list。 -
提交:
nf_tables_commit执行事务相关操作(如删除元素)。 -
释放:
nf_tables_trans_destroy_work最终释放资源,确保无内存泄漏。
以 NFT_MSG_DELSETELEM 请求为例跟一下请求的处理路径加深理解,首先会进入 nf_tables_delsetelem 进行处理,处理后会分配 trans 并将其放到 commit_list 中
trans = nft_trans_elem_alloc(ctx, NFT_MSG_DELSETELEM, set);
if (trans == NULL)
goto fail_trans;
nft_trans_elem(trans) = elem;
nft_trans_commit_list_add_tail(ctx->net, trans);
然后 nf_tables_commit 会处理 trans
case NFT_MSG_DELSETELEM:
te = (struct nft_trans_elem *)trans->data;
nf_tables_setelem_notify(&trans->ctx, te->set,
&te->elem,
NFT_MSG_DELSETELEM);
nft_setelem_remove(net, te->set, &te->elem);
if (!nft_setelem_is_catchall(te->set, &te->elem)) {
atomic_dec(&te->set->nelems);
te->set->ndeact--;
}
break;
最后在 nf_tables_trans_destroy_work --> nft_commit_release 完成最后的处理。
static void nft_commit_release(struct nft_trans *trans)
{
switch (trans->msg_type) {
case NFT_MSG_DELSETELEM:
nf_tables_set_elem_destroy(&trans->ctx,
nft_trans_elem_set(trans),
nft_trans_elem(trans).priv);
break;
2、漏洞触发
接下来看一下漏洞触发的代码路径和内存变化,触发 UAF 的步骤如下:
-
创建一个匿名 set (
pwn_lookup_set)并往set里面插入一个elem -
创建一个
rule,rule里面新建一个lookup的expr,lookup expr会引用pwn_lookup_set -
创建一个批处理其中包含两个请求:
- 使用
NFT_MSG_DELRULE删除上一步创建的rule - 使用
NFT_MSG_DELSETELEM删除pwn_lookup_set的elem
- 使用
-
在
nft_commit_release处理NFT_MSG_DELRULE时会释放rule里面的expr,然后在nft_lookup_destroy里面会释放匿名set -
在
nft_commit_release处理NFT_MSG_DELSETELEM就会访问到已经释放的set.
下面以图和代码结合的形式分析内存状态的变化,创建匿名 set 和 rule 后的内存关系如下:

请求提交给内核后,会在 nfnetlink_rcv_batch 获取相关对象的指针(rule、set 、elem 的指针),然后将其封装到 trans 对象中,最后在 nf_tables_trans_destroy_work --> nft_commit_release 完成具体的释放。

在 nft_commit_release 处理 NFT_MSG_DELRULE 命令时会同步释放 rule 里面的 expr,在释放 lookup expr 时会进入 nft_lookup_destroy 释放其关联的 set ,即 pwn_lookup_set
static void nft_lookup_destroy(const struct nft_ctx *ctx,
const struct nft_expr *expr)
{
struct nft_lookup *priv = nft_expr_priv(expr);
nf_tables_destroy_set(ctx, priv->set);
}
然后在处理 NFT_MSG_DELSETELEM时就会用到已经被释放的 set,因为内核无法知道其 trans 保存的 set 指针已经被释放
static void nf_tables_set_elem_destroy(const struct nft_ctx *ctx,
const struct nft_set *set, void *elem)
{
struct nft_set_ext *ext = nft_set_elem_ext(set, elem);
if (nft_set_ext_exists(ext, NFT_SET_EXT_EXPRESSIONS))
nft_set_elem_expr_destroy(ctx, nft_set_ext_expr(ext));
kfree(elem);
}
static void nft_commit_release(struct nft_trans *trans)
{
switch (trans->msg_type) {
case NFT_MSG_DELSETELEM:
nf_tables_set_elem_destroy(&trans->ctx,
nft_trans_elem_set(trans),
nft_trans_elem(trans).priv);
break;
最后总结一下:在 nfnetlink_rcv_batch 处理 NFT_MSG_DELRULE 和 NFT_MSG_DELSETELEM 会把分别需要用到的对象指针(rule 指针和 set 指针)保存到 trans,然后在 nf_tables_trans_destroy_work 处理 NFT_MSG_DELRULE 命令释放 rule 和 set 时,NFT_MSG_DELSETELEM 请求已经在队列中了,然后在处理 NFT_MSG_DELSETELEM 时就会拿到该 trans 里面保存的 set 指针,而此时该指针指向的对象已经被释放。
3、简单的总结
上面的分析可以进行一下简答的总结:
1)、核心机制
- 同步处理阶段:解析请求并生成事务对象(trans),加入提交列表。
- 异步销毁阶段:通过工作队列异步释放事务相关资源。
2)、漏洞成因
当两个存在依赖关系的请求(如 删除规则 和 删除集合元素)被放入同一批处理时,会引发以下问题:
步骤① 创建对象
用户创建匿名集合 pwn_lookup_set 和一条规则。
规则中的查找表达式(lookup expr)会引用该集合,形成 规则 → 表达式 → 集合 的依赖链。
步骤② 提交批处理
用户提交包含两个请求的批处理:
- DELRULE:删除规则(释放规则和表达式)
- DELSETELEM:删除集合中的元素(需访问集合)
步骤③ 内核处理
-
同步阶段:两个请求被封装为事务对象,加入
commit_list。 -
异步阶段:
- 处理
DELRULE事务时,释放规则及其表达式。由于表达式引用了集合,集合也被释放。 - 处理
DELSETELEM事务时,仍尝试访问已释放的集合,触发 UAF。
- 处理
3)、根本原因
内核未识别事务间的依赖关系,在异步销毁阶段错误释放仍被后续事务引用的资源。
五、补丁
官方修复补丁的关键修改位于 nf_tables_deactivate_set() 函数:
void nf_tables_deactivate_set(...) {
if (nft_set_is_anonymous(set))
nft_deactivate_next(ctx->net, set); // 标记匿名集合为“未激活”
set->use--;
}
效果:在批处理的准备阶段(NFT_TRANS_PREPARE),匿名集合会被标记为“未激活”,阻止后续操作引用其指针
六、参考文章
https://demonlee.tech/archives/2303002
https://www.cnblogs.com/qianfu/p/17530832.html
https://www.secrss.com/articles/54704?utm_source=chatgpt.com