318 lines
15 KiB
Markdown
318 lines
15 KiB
Markdown
# 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](https://cwe.mitre.org/data/definitions/120.html) | 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](https://cwe.mitre.org/data/definitions/134.html) | 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](https://cwe.mitre.org/data/definitions/271.html) | 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](https://cwe.mitre.org/data/definitions/200.html) | 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.
|