CVE-2019-15666 Ubuntu / CentOS / RHEL Linux Kernel 4.4 - 4.18 privilege escalation

by Vitaly Nikolenko


Posted on January 15, 2020 at 4:38 PM


A 0-day LPE (kernel) in CentOS 8(.1) was finally fixed today (4.18.0-147.3.1.el8_1). CentOS 8 and RHEL 8 kernels up to and including 4.18.0-80.11.2.el8_0 are vulnerable.

Red Hat Enterprise Linux 8 on the other hand patched this vulnerability in October last year. Ubuntu 14.04 and 16.04 server distributions running 4.4 kernels were the first to patch this bug in late June last year. Ubuntu 18.04 (4.15.x kernels) patches were backported approximately a month later.

The bug itself affects a large range of Linux kernels going back to 3.x kernels. The reason only above distributions are listed as vulnerable is because they allow unprivileged user namespaces required to reach the vulnerable code path. However, the reported out-of-bounds array access is harmless. There exists another execution path leading to use-after-free and reliable privilege escalation.

The first public patch report appeared in late February 2019.

--- a/net/xfrm/xfrm_user.c
+++ b/net/xfrm/xfrm_user.c
@@ -1424,7 +1424,7 @@ static int verify_newpolicy_info(struct xfrm_userpolicy_info *p)
 	ret = verify_policy_dir(p->dir);
 	if (ret)
 		return ret;
-	if (p->index && ((p->index & XFRM_POLICY_MAX) != p->dir)) [1]
+	if (p->index && (xfrm_policy_id2dir(p->index) != p->dir))
 		return -EINVAL;
 
 	return 0;
-- 

p->index is user-controllable (unsigned 4-byte int), XFRM_POLICY_MAX is set to 3 and valid policy directories (p->dir) are 0, 1 and 2. In [1], the index is folded and checked against the policy directory. For example, index 1006 corresponds to policy directory 2 (XFRM_POLICY_FWD).

The OOB bug was initially triggered with UBSAN due to this path executed on timer expiry:

static void xfrm_policy_timer(struct timer_list *t)
{
...
        dir = xfrm_policy_id2dir(xp->index);      [2]
...
        if (!xfrm_policy_delete(xp, dir))
                km_policy_expired(xp, dir, 1, 0);
...

int xfrm_policy_delete(struct xfrm_policy *pol, int dir)
{
        struct net *net = xp_net(pol);

        spin_lock_bh(&net->xfrm.xfrm_policy_lock);
        pol = __xfrm_policy_unlink(pol, dir);
...

static struct xfrm_policy *__xfrm_policy_unlink(struct xfrm_policy *pol,
                                                int dir)
{
        struct net *net = xp_net(pol);
...
        list_del_init(&pol->walk.all);
        net->xfrm.policy_count[dir]--;            [3]

        return pol;
}

In [2], xfrm_policy_id2dir now folds the policy index against 7 (not 3!). The same index 1006 would now result in policy directory 6:

static inline int xfrm_policy_id2dir(u32 index) {
	return index & 7;
}

Then in [3], 6 is used as the index into the policy_count array leading to OOB decrement by one (the size of policy_count array is 6). policy_count is an array of 4-byte integers and OOB by one decrement of the adjacent struct member does not result in any exploitable conditions.

We will make our technical report detailing the original UAF vulnerability (leading to privilege escalation) along with the PoC public. We haven't checked all distributions (the bug was initially discovered on Ubuntu 18.04 / 4.15 kernel) and it is possible that there are other distributions that still haven't backported the patch. The rule of thumb: if your kernel build date is earlier than July last year, unprivileged user namespaces are supported then your distribution is probably vulnerable.

UPDATE: technical report and poc are available here.