Write-Ups
On 7th March’22, security researcher Max Kellermann published the vulnerability nicknamed ‘Dirty-Pipe’ which was assigned as CVE-2022-0847. This vulnerability affects the Linux kernel and its successful exploitation allows the attacker to perform a local privilege escalation.
The vulnerability arises from the incorrect UNIX pipe handling which allowed the attackers to overwrite the files on the system with arbitrary data (modifying sensitive files potentially including root passwords and SUID binaries). Dirty-Pipe affects all the Linux-based systems, including Android, with a Kernel version 5.8 or later.
In this blog, we have tried to break-down the Dirty-Pipe vulnerability with a relatively high-level view. A more technically detailed explanation is available on Max Kellermann’s blog.
Fortunately, the patches have been rolled out and this vulnerability has been fixed in the latest kernel versions – namely 5.16.11, 5.15.25, and 5.10.102. Be mindful about updating your systems including any Android devices.
Let us first go through a quick rundown on some of the pre-requisite concepts required to understand the working of the Dirty-Pipe exploit.
When the CPU needs to work with a process, it retrieves the data of that process from the secondary memory (like a hard drive) into the main memory.
This is done because the RAM (main memory) is way faster than the secondary memory, and so the data access speeds of the RAM can cope up with the CPU speed.
At the core, the operating system is responsible for memory management, which is simply the dynamic allocation and deallocation of the memory portions to the required processes in an efficient manner so as to achieve the best performance.
Paging is one of the memory management schemes which allows non-contiguous memory allocation. The smallest unit of memory controlled by the CPU is called a page — these are usually about 4KB in modern systems. Main memory is divided into chunks of equal size called frames. So when the CPU needs to compute a process, the whole process is divided into equal chunks also known as pages and then loaded into the main memory.
Furthermore, when the CPU first reads data from storage media like hard drives, Linux also stores this data in the unused areas of memory, which acts as a cache. This copy in the page cache remains for some time, from where it can be used again when needed, avoiding expensive hard disk I/O, until the kernel decides it has better use for that memory. Page cache is advantageous in both read & write operations :
Reading: If this data is read again later, it can be quickly read from this cache in memory, and do not have to be read from the hard disk again.
Writing: If data is written, it is first written to the Page Cache and then later eventually written into the underlying storage device.
A page that is modified in the cache and not yet updated in secondary memory (resulting in the two copies being different) is referred to as a "dirty page"
(This is partly responsible for its resemblance in the vulnerability nickname ‘Dirty-Pipe’)
As quoted on its man page :
Pipes and FIFOs (also known as named pipes) provide a unidirectional interprocess communication channel. A pipe has a read end and a write end. Data written to the write end of a pipe can be read from the read end of the pipe.
Let’s try to understand through an example :
dotguy@kali>> echo hello | wc -c
6
In the above example, we have used an ‘anonymous pipe’.
The output of the first process (echo hello) is passed into the pipe, which is later used by the process wc -c as input.
So basically, the pipe takes the output of a process and writes it into a pipe from where it can be read as an input for the next process in the chronology.
There also exists a pipe flag “PIPE_BUF_FLAG_CAN_MERGE”, which signifies that the data buffer inside the pipe can be merged, i.e, this flag notifies the kernel that the changes which are written to the page cache pointed to by the pipe shall be written back to the file that the page is sourced from.
A system call is basically a programmatic way through which a program/process requests a service from the kernel of the operating system.
One such system call is splice(). This system call moves data between a file descriptor and a pipe, without requiring the data to cross the user-mode/kernel-mode address space boundary, which results in better performance.
On a higher level, splice() does this by not moving the actual data into the pipe, but the whereabouts or the reference to that data into the pipe. Now the pipe contains the reference to the location of the page cache in memory where the desired data is stored, rather than having the actual data itself.
Alright so let’s jump in and get a high-level yet sufficiently technical overview of Dirty Pipe’s working under the hood.
DirtyPipe is a local privilege escalation vulnerability in the Linux kernel that allows a local attacker to bypass any file permissions, and write arbitrary data to any file under certain conditions.
One of the major limitations of this exploit is that the user must at least have read permission over the file that he/she is targeting to overwrite, because the exploit makes use of the splice() system call. Another limitation of the exploit is that it can only modify after the first byte of the file through the end of the file. It cannot modify the first byte, or extend beyond the original file size.
Initially, the exploit reads the target file (which has read permission) so that it gets cached in the page cache.
Then, the exploit creates a pipe in a special way such that it has the PIPE_BUF_FLAG_CAN_MERGE flag set.
Next, the exploit uses the splice() system call to make the pipe point to the location of the page cache where the desired data of the file is cached.
Finally, we write arbitrary data into the pipe. This data will overwrite the cached file page & because PIPE_BUF_FLAG_CAN_MERGE is set, it ultimately overwrites the file on the disk, thus accomplishing our task.
There are many public exploits out for thisi vulnerability. We like the one that can be found here, because it is relatively safe to use. It does modify the /etc/passwd file, placing a password of “aaron” as the root password. However, before doing so it will create a backup and after exiting your shell it will restore the backup..
The /etc/passwd file contains information about a user, each piece being seperated by a colon. The second field is normally just “x”, which indicates the password hash for that user is stored in /etc/shadow. By placing a password hash where the “x” is, the /etc/passwd file will be used for authentication instead of the shadow file. Below is what each piece of the passwd file means, along with an example.
username:password_hash:user_ID:group_ID:user_info:home_directory:shell
root:x:0:0:root:/root:/bin/bash
The capability for /etc/passwd storing passwords is a legacy linux feature, in 1992 the shadow file was created in order to help prevent privilege escalation through cracking another users password. Thirty years later, the functionality for the /etc/passwd being capable of storing passwords still exists.
With knowledge of how the passwd file works, we can finally take a look at the exploit. The exploit starts by opening the /etc/passwd file and seeks 4 bytes, which places cursor at the first colon. As long as root is the first user, and there are no comments at the top of the passwd file, then this will be where the password hash should begin. If the passwd file does not begin with “root”, it is highly recommended to utilize a different exploit script as counting bytes to get to the exact spot to overwrite is trickier than it sounds, and if the exploit fails the passwd file can be left corrupt. Below is the snippet of code which seeks 4 bytes.
loff_t offset = 4; // after the "root"
const char *const data = ":$1$aaron$pIwpJwMMcozsUxAtRa85w.:0:0:test:/root:/bin/sh\n";
// openssl passwd -1 -salt aaron
printf("Setting root password to \"aaron\"...\n");
const size_t data_size = strlen(data);
The other interesting piece of code in this exploit script is that when it opens the file to overwrite, it will open it “READ-ONLY”. This may seem counter-intuitive because we want to write to this file. However, we are abusing a non-standard way to write to files with pipes() and splice(). By doing it this way, the kernel will perform the write for us without checking any privileges, even if we had marked the file immutable (chattr +i /etc/passwd), the file write would still happen. The pre-requisite for writing to the pipe is for us to have the file open, this is why /etc/passwd was used for the exploit instead of /etc/shadow. Since /etc/shadow is not world readable, we can’t even open it as read-only as a non-privileged user. Below is the code snippet for opening the file.
pwnmeow@DirtyPipe:/tmp$ git clone https://github.com/Arinerron/CVE-2022-0847-DirtyPipe-Exploit.git
Cloning into 'dirty_pipe...
remote: Enumerating objects: 32, done.
remote: Counting objects: 100% (32/32), done.
remote: Compressing objects: 100% (25/25), done.
remote: Total 32 (delta 16), reused 16 (delta 5), pack-reused 0
Receiving objects: 100% (32/32), 14.42 KiB | 1.20 MiB/s, done.
Resolving deltas: 100% (16/16), done.
That’s about all there is to this exploit, running this POC is extremely simple. Just download it, compile it, and run it. The exploit script does contain a compile script, but it's really just a GCC Command with no special flags. Just the “-o exploit”, which specifies compile it to the file exploit.
pwnmeow@DirtyPipe:/tmp$ cd dirty_pipe
pwnmeow@DirtyPipe:/tmp/dirty_pipe$ ls
LICENSE.txt README.md compile.sh exploit.c
pwnmeow@DirtyPipe:/tmp/dirty_pipe$ cat ./compile.sh
#!/bin/sh
gcc exploit.c -o exploit
pwnmeow@DirtyPipe:/tmp/dirty_pipe$ ./compile.sh
pwnmeow@DirtyPipe:/tmp/dirty_pipe$ ls
LICENSE.txt README.md compile.sh exploit.c exploit
It is worth noting, it is possible to perform this exploit with other file writes. Here is a great example which works by hijacking SUID Binaries to gain a root shell. Like the other script we mentioned this one is relatively safe to use. Instead of placing a shell ontop of the setuid binary it overwrites, it processes it:
Backup the SetUID Binary.
Use DirtyPipe to overwrite the SetUID Binary by placing code that copies /bin/bash to /tmp/sh and marks it SetUID.
Uses /tmp/sh to copy the SetUID binary back to its original state.
Uses /tmp/sh to provide a root shell.
Reminds you to delete /tmp/sh when done.
Exploiting cron’s can be tricky, if you wish to go this route you will need to overwrite the actual cron script and not the crontab file. This is because when you modify a file like /etc/crontab, the kernel will actually reload the cron daemon, importing the new file. Since we use the splice() command to perform the file write, the kernel doesn’t reload the crontab file and the new/modified cron will not run.
There’s good and bad news with detection. The good news is that if your organization utilizes auditd or sysmon for linux, chances are you won’t need to create any new rules to detect this exploit. The bad news is, you won’t be able to actually catch DirtyPipe from doing the file writes because it uses the splice() function which most likely is not logged, for the same reason modifying the crontab file doesn’t work.
However, you should be able to catch when these POC’s create backups of /etc/passwd (or SetUID) files. Additionally, if the attacker just overwrote a setuid file with an elf you should be seeing a unique MD5 Hash being executed in your environment.
The toughest thing to catch is if an attacker utilized DirtyPipe for all file writes, as it is possible to have an exploit that:
Utilizes DirtyPipe to create a backup over top of a benign file
Utilizes DirtyPipe to overwrite /etc/passwd
User obtains a shell through su/ssh
Utilizes DirtyPipe to restore both the backup and benign file
It becomes much more stealthy this way as a lot of the file writes would go unlogged, however with good monitoring of SSH/SU usage this should also be easily caught.
There is no known workaround other than upgrading the kernel. The bug itself is a memory corruption and writes to the disk upon syncing. Even if you managed to make all sensitive files immutable with chattr +i, DirtyPipe can still overwrite the file as its a direct write to the disk, so the kernel can't stop the write.
Run a system upgrade, for Debian based systems use: apt update && apt upgrade and for redhat based you can use dnf update && dnf upgrade. Afterward, verify that the kernel is not vulnerable according to this advisory or compile the exploit from this advisory and run the following commands:
As Root:
echo VXXX > /tmp/DirtyPipeTest
chmod 755 /tmp/DirtyPipeTest
As a low priv user:
./dirtypipe /tmp/DirtyPipeTest 1 uln
cat /tmp/DirtyPipeTest
If the cat command shows the output "Vuln", this means that your kernel is still vulnerable and you may want to try a dist-upgrade command or downgrade to a kernel that was not vulnerable.
Dirty pipe is similar to the Dirty-Cow vulnerability but the initial proof of concept exploits are much more stable. That isn’t to say DirtyPipe should be considered a stable exploit as with all Linux kernel exploits when it fails, a system crash is likely. However, it does appear to provide attackers a reliable way to escalate from user to root and can be difficult for defenders to detect due to the wide variety of files that can be manipulated to provide a path to root.
Thus it is highly recommended to upgrade the Linux kernel to one of the following versions 5.16.11, 5.15.25, 5.10.102, or later; and secure your systems.DirtyPipe (CVE-2022-0847) has been added to the list of CVE Exploitable machines available on our Enterprise Platform, learn more: https://www.hackthebox.com/business/dedicated-labs.