15 KiB
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:
foowosdse 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-rootusasudopara exactamente eso, ymake run-root-nsconsigue 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:
- 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 asetreuid(0,0)(payload de 32 bytes). - Este laboratorio: root inicia el proceso → ruid == euid == 0 → el
guardián no tiene nada que resetear → el shellcode
execvenormal 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=, noeuid=/ruid=. El harness de prueba prueba unidvivo haciendo match de la forma literaluid=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
- Análisis estático —
objdump -dde./foowosd. Encuentravulnerable_handler,win(), ellea -0x50(%rbp)que direccionabuf, y el primerretdesnudo. A partir del desplazamiento calcularip_off = 80 + 8 = 88. Nada está hardcodeado; sobrevive a una recompilación. - Auto-introspección — lee su propio
/proc/self/mapsy hacedlsym()desystem/readpara aprender los offsets de libc. La base libc del objetivo esleaked_read − off_read, luegosystem = base + off_system, etc. Esa aritmética de delta es la razón por la que los exploits sobreviven a las versiones de libc. - Conecta — lee el banner/las fugas (
ids=,stack=,libc=,BUF=). - Construye el payload — para
shellcode: 23 bytes de código máquina, basura hastarip_off, luego la RIP guardada =buf(para que elretsalte dentro del código). Pararet2win/ret2libc: direcciones calculadas del análisis — no se necesita ejecución de pila. - El fix de alineación — un
retdesnudo secuestrado da a la calleersp ≡ 8 (mod 16), y el código SSE2 de glibc falla conmovapsen una pila mal alineada (el crash-reporter registrasi_addr=(nil)— la pista). foowosc inserta un gadgetretextra 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. - 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.-Lexiste solo para mostrar la barandilla; no lo uses en algo que importe. - El estado se registra en voz alta. Al arrancar imprime
ruid/euidy 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 stopdespués de cada sesión. Si el demonio es propiedad de root,stopte dice que ejecutessudo pkill -x foowosd. - Nunca ejecutes esto en un host que te importe. Existe para repartir shells
uid=0por la interfaz loopback.
Ejercicios
- Ejecuta
make run(demonio de usuario), luego./foowosc -t shellcode. ¿Por qué el shell no es root? (Compruebaids=en el banner — foowosc te lo dice antes de que siquiera te conectes.) 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 ahorauid=0(root).- Encuentra
drop_privs()enfoowosd.cy lee por qué el orden desetgroups → setgid → setuidimporta. Decide dónde enmain()encajaría, y qué se vuelve la superficie de ataque del laboratorio cuando de verdad se llama. - Calcula
rip_offa mano desdeobjdump -d foowosd: encuentra ellea -0xNN(%rbp)debufdentro devulnerable_handler, luegoNN + 8. foowosc hace exactamente eso; comprueba su cálculo contra el tuyo. make hardened && make test-hardened. ¿Qué técnica cae ante la canary, y cuál ante NX? ¿Por qué el endurecimiento no cambia lo quemake statusreporta sobre el proceso?- 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?
- 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.