foo/wosuid/README.ES.md
2026-09-29 09:39:24 +02:00

318 lines
No EOL
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# El laboratorio `wosuid` — RCE root **sin** bit setuid
```
foowosd un demonio deliberadamente vulnerable que es root porque fue
*INICIADO* como root (puerto 2344, solo loopback por defecto)
foowosc el exploit: convierte un desbordamiento de pila en un **shell**
root ejecutando shellcode — los mismos 23 bytes que rompieron el
demonio user-level `food` del laboratorio principal
```
Este es el tercer laboratorio de la serie. La misma cadena de herramientas de
exploit, el mismo estilo, una diferencia fundamental:
| Laboratorio | cómo el proceso objetivo se vuelve root | ¿shell `uid=0(root)`? |
|----------|------------------------------------------------|----------------------|
| food/fooc | nunca — es un demonio de usuario normal | no |
| foosd/foosc | el bit SUID (`chmod u+s`) — euid 0, ruid 1000 | sí (exige `setreuid` en el shellcode, porque bash resetea euid→ruid) |
| **foowosd/foowosc** | **ninguno — root *inicia* el demonio** (sudo / systemd `User=root`) | **sí (shellcode `execve` normal)** |
El bit setuid es un *medio de transporte* de privilegios — no los privilegios
mismos. Un demonio iniciado por root tiene uids real, efectivo y guardado todos
iguales a 0. Para el kernel es root, punto; no puede ni quiere saber si el
proceso llegó ahí vía `+s` en un archivo o vía `sudo ./foowosd`. El
desbordamiento de un demonio iniciado por root es por tanto un exploit root —
*"no tengo binarios SUID" no es lo mismo que "no soy explotable".*
Esa es toda la lección de este laboratorio. Todo lo demás es el mecanismo.
---
## Inicio rápido (lo que pidió el usuario)
```
cd wosuid
make # compila el demonio, el exploit y el harness de prueba
```
### Lo auténtico — ejecuta el demonio como **root**
```
sudo make run-root # inicia foowosd como uid 0 (proceso, no estado de archivo)
make test-root # cada técnica debe dar ahora uid=0(root)
```
### ¿Sin sudo? El camino de kernel idéntico vía un user namespace
```
make run-root-ns # uid 0 en un user namespace — no se necesita contraseña
make test-root # los mismos veredictos; lo usa la CI y todo el que no tenga sudo
```
### Referencia — demonio como tu usuario normal (root en ninguna parte)
```
make run # foowosd corre con tus uids
make test # los exploits aterrizan shells, pero se espera que `root` sea MISSING
```
### Rito de limpieza (siempre: esto es un laboratorio de shell root)
```
make stop
```
`foowosd` *no* es setuid, y nada en este directorio hace nunca `chmod +s` —
ese es el punto. El estado peligroso es **el proceso**, no el archivo.
---
## Cuando te digan que pongas el bit SUID
No te lo van **a** decir. Este laboratorio no tiene deliberadamente ningún bit
SUID:
- `foowosd` se compila, te pertenece y tiene permisos normales como cualquier
otro programa.
- Se vuelve root como hacen los demonios reales — siendo *iniciado* por root.
- `make run-root` usa `sudo` para exactamente eso, y `make run-root-ns`
consigue un proceso uid 0 real sin nada de eso.
El bit Suid pertenece al laboratorio *hermano* (`foosd`). El contraste entre los
dos es el plan de estudios:
1. Laboratorio SUID: el bit da **euid 0, pero ruid 1000** → `execve("/bin/sh")`
es degradado por el guardián de bash (`euid != ruid` → reset) → el shellcode
debe llamar primero a `setreuid(0,0)` (payload de 32 bytes).
2. Este laboratorio: root **inicia** el proceso → **ruid == euid == 0** → el
guardián no tiene nada que resetear → el shellcode `execve` normal de 23
bytes conserva root.
El mismo desbordamiento. La misma técnica. *Origen* de privilegios diferente,
forma de payload distinta. Esa es la lección en miniatura.
---
## El protocolo
Sea cual sea el tipo de cliente que se conecta, foowosd lo saluda con:
```
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (el banner + las fugas)
BUF=0x7ffd… (la dirección del búfer)
```
`ids=euid/ruid` es el canal *"¿soy root?"*. foowosc imprime una advertencia
fuerte cuando euid no es 0 (es decir, iniciaste el demonio como usuario
normal): el payload sigue aterrizando, pero el shell se convierte en un shell
de usuario, y llamar al exploit "roto" sería falso — solo que no escala.
> Nota ortográfica: `ids=`, no `euid=`/`ruid=`. El harness de prueba prueba un
> `id` vivo haciendo match de la forma literal `uid=NNN(`, así que el banner
> nunca debe contener una subcadena que satisfaga ella misma la comprobación.
> (En el laboratorio SUID, exactamente esa trampa produjo un falso positivo
> espectacular.)
---
## Las técnicas de exploit (`foowosc -t …`)
Las cuatro rutas de exploit de abajo funcionan contra foowosd. Cuando el demonio
es root, **todo lo que hace spawn de algo da root** — a diferencia del
laboratorio SUID, donde ret2win/ret2libc eran degradados silenciosamente a uid
1000 por el guardián de bash. Aquí no hay desajuste que vigilar.
| `-t` | qué ocurre | cuando el demonio es root |
|---------------|---------------------------------------------------------------------|---------------------|
| `shellcode` | `execve("/bin/sh", NULL, NULL)` de 23 bytes corre en la pila. | **shell root** (por defecto) |
| `ret2win` | salto a `win()` → `execl("/bin/sh")` | **shell root** |
| `ret2libc` | ROP: `pop rdi; ret` → `"/bin/sh"` → `system()` | **shell root** |
| `demo` | solo desbordamiento de basura — espera un SIGSEGV en el log del demonio | crash, por diseño |
| `leak` | solo imprime fugas, no envía payload | n/a |
```
./foowosc -t shellcode # interactivo; objetivo por defecto 127.0.0.1:2344
./foowosc -t shellcode -n # enviar y reportar, sin sesión interactiva
```
Una sesión interactiva exitosa retransmite tu terminal al shell *en la víctima*
— hay exactamente un shell en el cuadro, y es `/bin/sh` corriendo como root
dentro de foowosd. Escribe `id` para ver `uid=0(root)`.
### Por qué no hay una técnica `ret2win-root` aquí
foosc tenía una — saltaba a un `win_root()` que llamaba a `setreuid(0,0)`
antes del exec, porque un proceso setuid corría con un uid real que todavía
decía 1000. Un proceso *iniciado* por root ya tiene el uid real en 0; no hay
nada que limpiar, así que la función y la técnica extra no enseñarían nada.
Eliminada.
---
## Qué hace foowosc, paso a paso
1. **Análisis estático** — `objdump -d` de `./foowosd`. Encuentra
`vulnerable_handler`, `win()`, el `lea -0x50(%rbp)` que direcciona `buf`, y
el primer `ret` desnudo. A partir del desplazamiento calcula
`rip_off = 80 + 8 = 88`. Nada está hardcodeado; sobrevive a una
recompilación.
2. **Auto-introspección** — lee su propio `/proc/self/maps` y hace `dlsym()` de
`system`/`read` para aprender los *offsets* de libc. La base libc del
objetivo es `leaked_read − off_read`, luego `system = base + off_system`,
etc. Esa aritmética de delta es la razón por la que los exploits sobreviven
a las versiones de libc.
3. **Conecta** — lee el banner/las fugas (`ids=`, `stack=`, `libc=`, `BUF=`).
4. **Construye el payload** — para `shellcode`: 23 bytes de código máquina,
basura hasta `rip_off`, luego la RIP guardada = `buf` (para que el `ret`
salte dentro del código). Para `ret2win`/`ret2libc`: direcciones calculadas
del análisis — no se necesita ejecución de pila.
5. **El fix de alineación** — un `ret` desnudo secuestrado da a la callee
`rsp ≡ 8 (mod 16)`, y el código SSE2 de glibc falla con `movaps` en una pila
mal alineada (el crash-reporter registra `si_addr=(nil)` — la pista).
foowosc inserta un gadget `ret` extra antes del objetivo real y restaura la
invariante. Una técnica de guante de seda en un laboratorio de shellcode,
pero es la diferencia entre un payload que "a veces funciona" y uno que
siempre funciona.
6. **Envía, luego retransmite** — el proceso víctima *es* el shell; este
proceso solo splissea bytes. Sin shell local, sin otro lector — el bug del
cursor de lectura único (un byte comido por trozo) está documentado en
`become_shell()`.
---
## Los errores deliberados del demonio (todos en `foowosd.c`, todas clases CWE reales)
| # | error | CWE | nota |
|---|-----|-----|------|
| 1 | `read(fd, buf, 512)` en un búfer de pila de 64 bytes | CWE-120 | el desbordamiento: 448 bytes más allá de `buf`, RIP guardada en +88 |
| 2 | solo `snprintf(line, …, "%.*s", …)`; pero `%` del atacante en el camino de eco | CWE-134 | la fuga es aquí el payload real; un `%n` en un proceso *root* sería write-what-where como root |
| 3 | los hijos conservan root mientras manejan entradas no confiables | CWE-271 | el `drop_privs()` correcto (setgroups→setgid→setuid, en ese orden, con verificación) está en el archivo, comentado, *deliberadamente nunca llamado* |
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` revelados a cualquier cliente | CWE-200 | sin estas fugas, las técnicas de shellcode y ret2libc no podrían calcular direcciones (ASLR las vencería) |
El handler tiene exactamente la misma forma `buf[64]`/`read(512)` que los otros
dos laboratorios, así que el pipeline común de reconocimiento basado en objdump
funciona sin cambios.
---
## Cómo inspeccionar el demonio (camino de aprendizaje)
```
make status # ¿corre? ¿como qué uid? se muestra el estado del archivo
make run-root # o run / run-root-ns
./foowosc -t leak # mira el banner y las fugas, no envíes nada
./foowosc -t demo # desbordamiento de basura -> SIGSEGV, registrado con RIP/rsp
./foowosc -t shellcode # el shell root interactivo
make test-root # matriz completa, todas las técnicas, --must-root
```
Crash-reporter: En SIGSEGV, el demonio registra la dirección de error, RIP y
RSP. Un `ret` dentro de un `0x4141…` no canónico falla en el `ret` mismo (RIP
como `0x4028xx`, `si_addr=(nil)`) — bueno saberlo antes de leer mal una línea
de log como desreferencia NULL.
---
## Por qué el shellcode es de 23 bytes, no de 32
```
31 f6 xor esi, esi ; argv = NULL
31 d2 xor edx, edx ; envp = NULL
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0"
57 push rdi
48 89 e7 mov rdi, rsp
6a 3b push 0x3b ; 59 = execve
58 pop rax
0f 05 syscall
```
El laboratorio SUID necesita `setreuid(0,0)` delante de esto. Este laboratorio
no, por la razón repetida en todas partes: **ruid ya es 0**, porque root inició
el proceso. `make verify` prueba que los bytes en `foowosc.c` son, byte a byte,
lo que `shellcode.S` ensambla.
---
## Mitigaciones — qué cambia `make hardened`
Build endurecida (`-fstack-protector-strong -fPIE -pie -z noexecstack`):
| técnica | `foowosd` vulnerable | `foowosd_hardened` endurecido |
|-----------|----------------------|------------------------------|
| `shellcode` | shell root (pila ejecutable) | SIGSEGV en la comprobación de canary / NX |
| `ret2win` / `ret2libc` | shell root | la canary aborta el `ret` — pero nota: una build *PIE* también vuelve aleatorias estas direcciones |
| `demo` | SIGSEGV, registrado | SIGSEGV, registrado |
`make test-hardened` lo demuestra en vivo. La observación importante no es solo
que las mitigaciones mataron las técnicas — es que **no** convirtieron al
demonio en "no-root". Una build endurecida que todavía se *inicia* como root
sigue siendo un demonio root; la mitigación solo sube el listón para el
atacante. Mínimo privilegio (`drop_privs()`) y seguridad de memoria son dos
errores distintos, y un demonio que no necesita root no debería tenerlo.
---
## Barandillas de seguridad (la misma política que el laboratorio SUID)
- **Solo loopback.** foowosd se niega a enlazarse a nada que no sea `127.0.0.1`
/ `localhost` / `::1`, salvo que des `-L`. Un demonio root en una interfaz
real es un *servicio root remoto*. `-L` existe solo para mostrar la
barandilla; no lo uses en algo que importe.
- **El estado se registra en voz alta.** Al arrancar imprime `ruid/euid` y si
esto es un proceso root, para que siempre sepas qué resultado de exploit
esperar.
- **Los veredictos vienen del código de salida** en `make test*` (el código de
retorno del harness pty), nunca de hacer grep de su stdout — la salida
grepable miente.
- **La pty debe correr en cooked + ECHO off**, si no, el harness ecoa su propia
línea de comandos y falsifica el marcador. El harness apaga ECHO y mantiene
ECHONL activo.
- **Rito de limpieza:** `make stop` después de cada sesión. Si el demonio es
propiedad de root, `stop` te dice que ejecutes `sudo pkill -x foowosd`.
- Nunca ejecutes esto en un host que te importe. Existe para repartir shells
`uid=0` por la interfaz loopback.
---
## Ejercicios
1. Ejecuta `make run` (demonio de usuario), luego `./foowosc -t shellcode`.
¿Por qué el shell no es root? (Comprueba `ids=` en el banner — foowosc te
lo dice antes de que siquiera te conectes.)
2. `make stop && sudo make run-root && make test-root`. Explica a partir de la
línea del banner por qué las cuatro técnicas dan ahora `uid=0(root)`.
3. Encuentra `drop_privs()` en `foowosd.c` y lee *por qué el orden* de
`setgroups → setgid → setuid` importa. Decide dónde en `main()` encajaría, y
qué se vuelve la superficie de ataque del laboratorio cuando de verdad se
llama.
4. Calcula `rip_off` a mano desde `objdump -d foowosd`: encuentra el
`lea -0xNN(%rbp)` de `buf` dentro de `vulnerable_handler`, luego `NN + 8`.
foowosc hace exactamente eso; comprueba su cálculo contra el tuyo.
5. `make hardened && make test-hardened`. ¿Qué técnica cae ante la canary, y
cuál ante NX? ¿Por qué el endurecimiento no cambia lo que `make status`
reporta sobre *el proceso*?
6. Compara los shellcodes de los dos laboratorios: 23 bytes aquí, 32 para
foosd. ¿Qué hacen los 9 bytes extra, y por qué solo se necesitan en el caso
SUID?
7. Lee el comentario de `become_shell()` sobre el cursor de lectura único.
Reconstruye mentalmente el estado de fallo: dos lectores en un socket
significa que el shell de login come un byte por trozo — "uid=1000…" llega
como "id=1000…". ¿Por qué un proceso relay no puede tener nunca este error?
---
## Archivos
```
foowosd.c el demonio root vulnerable (cada línea comentada)
foowosc.c el exploit (cada línea comentada)
shellcode.S el assembly de referencia para el payload de 23 bytes
tests/pty_wosuid_test.c el harness pty (marcador + comprobación estricta de forma id)
Makefile build / run / run-root / run-root-ns / test /
test-root / verify / hardened / clean …
```
Laboratorios hermanos: `../food.c`/`../fooc.c` (referencia user-level, puerto
2342) y `../suid/` (demonio root SUID `foosd`/`foosc`, puerto 2343). Los
puertos son deliberadamente distintos — puedes ejecutar los tres a la vez y
cruzar sus líneas `ids=` en el banner.