githubEditar

HackTheBox - VariaType

Writeup de la máquina VariaType de HackTheBox

  • Dificultad medium

  • Tiempo aprox. ~8h

  • Datos Iniciales: 10.129.9.137

Nmap Scan

Tras hacer un escaneo de puertos, habiendo añadido variatype.htb previamente a /etc/hosts, se encuentra lo siguiente:

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey: 
|   256 e0:b2:eb:88:e3:6a:dd:4c:db:c1:38:65:46:b5:3a:1e (ECDSA)
|_  256 ee:d2:bb:81:4d:a2:8f:df:1c:50:bc:e1:0e:0a:d1:22 (ED25519)
80/tcp open  http    nginx 1.22.1
|_http-title: VariaType Labs \xE2\x80\x94 Variable Font Generator
|_http-server-header: nginx/1.22.1
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
# Nada en UDP Top 200
  • 22/tcp (OpenSSH 9.2p1): Versión vulnerable a RegreSSHion, dificil de explotar así que no hay mucho que podamos hacer.

  • 80/tcp (nginx 1.22.1): Se anuncia como un "Variable Font Generator"

    • El código en medio (\xE2\x80\x94) parece ser la codificación UTF-8 DE ("em dash"). Nada relevante.

HTTP

variatype.htb

Al entrar, vemos una página que promete lo siguiente:

Generate production-ready variable fonts from your .designspace and master font files using industry-standard tooling.

La página permite subir archivos .designspace y .ttf/.otf para crear "fuentes variables". Tras una búsqueda para descubrir qué son estos archivos:

  • Los archivos .designspace son archivos de código fuente en XML. Se usan para diseñar tipografías con múltiples variables (Thin, Italic, etc.). No tienen dibujos de letras, solo info que le dice al compilador dónde encontrar archivos de dibujo (.ufo), cómo interpolarlos y qué ejes existen (anchura, inclinación, etc.).

  • Los archivos .ttf (TrueType Font) y .otf (OpenType Font) son los archivos compilados finales, contienen los contornos de las letras, info del espaciado y metadatos. Son las que se instalan en el sistema directamente.

  • Las fuentes variables son una evolución de las fijas que permiten que muchas variaciones de una sola fuente estén metidas en un solo archivo, en lugar de tener que tenerlas todas en archivos separados (p.ej, para thin, medium, bold, italic...).

Vhosts -> portal.variatype.htb

En la pestaña Services, debajo del todo, vemos un botón Email Us que nos lleva a mailto:studio@variabype.labs. Puede ser otro vhost? Si probamos con whatweb vemos que también lo detecta:

Así que probamos a añadirlo a /etc/hosts, y buscamos subdominios tanto para ese como para variatype.htb:

Vemos que con variabype.labs se nos redirige a variatype.htb, así que no nos ha servido de mucho.

Con el dominio original probamos a enumerar subdominios:

Y tenemos portal.variatype.htb, lo añadimos a /etc/hosts. Al entrar encontramos una página de login:

No parece ser el panel de login de ningún CMS o similar, tampoco parece haber nada en el código fuente ni se muestra una versión. Lo único que encontramos es un directorio files/ para el que no tenemos permisos, así que tenemos que volver atrás.

Vuelta a variatype.htb -> CVE-2025-66034

Sabemos que el sistema de VariaType permite subir archivos .designspace, .ttf y .otf, también sabemos que, por detrás, se usan librerías y herramientas como fonttools, fontmake, y gftools:

  • fonttools: Biblioteca de Python que puede leer, manipular y escribir archivos .ttf y .otf. Permite desensamblar una fuente binaria en un formato legible, modificar sus datos internos y volver a compilarla. Se usa en prácticamente todo lo relacionado con fuentes en el ámbito open source.

  • fontmake: Compilador de fuentes construido sobre fonttools. Toma archivos .designspace junto con los .ufo y los compila en archivos .ttf y otf o fuentes variables

  • gftools es un conjunto de herramientas oficiales de Google Fonts. Permiten verificar calidad, generar archivos de prueba y demás. Dado que es más algo "auxiliar", podemos ignorarlo en cierta medida.

Si nos centramos en fonttools y fontmake, que sabemos que se usan, y sabemos que en este caso crean fuentes variables, podemos encontrar específicamente las funciones o módulos que utilizan. Y tras una búsqueda:

fonttools usa el módulo varLib, concretamente la función build() de varLib, para crear fuentes variables, tomando un .designspace y generando la fuente.

Y, casualmente, si buscamos vulnerabilidades relevantes de fonttools: CVE-2025-66034arrow-up-right.

Vemos que este CVE permite a un atacante conseguir RCE mediante un path traversal y una inyección XML, usando un .designspace malicioso.

La idea es que, al compilar con fonttools, es posible indicar dónde queremos que se dejen los archivos resultantes, pero, en sus versiones vulnerables, la librería no comprueba bien el directorio y eso la hace vulnerable a un path traversal. Por otro lado, aprovechando esto, también es posible manipular ciertos elementos del XML para que se escriban tal cual en un archivo final.

En el PoC aparecen 2 .designspace's diferentes, cada uno mostrando un fallo (XML injection, Path traversal), pero la idea es unificar ambos en un solo archivo, p.ej:

Además, para que funcione, necesitamos los archivos source-light.ttf y source-regular.ttf que aparecen en el .designspace (porque antes de ejecutar nuestro payload fonttools los procesa para crear el archivo final), pero podemos crearlos con el script dado en el PoC.

Con todo esto, subimos los archivos, pero al ejecutarlo:

Buscando un directorio útil

Como no sabemos si verdaderamente se está usando /var/www/html u otro directorio, y no podemos probar con todos los directorios posibles por defecto (y custom) que el webserver podría servir, lo que podemos hacer es subir solo unas pocas rutas relativas, sin intentar llegar hasta la raíz (/) para luego bajar. En lugar de:

Usamos:

Ahora si probamos a mandarlo:

El problema es que si probamos a solicitar shell.php en cualquiera de estos sitios:

Todos devuelven 404 NOT FOUND, así que todavía no sabemos dónde se está guardando el archivo. Podemos probar a añadir otro ../ más, pero el resultado es el mismo. Si añadimos otro más, es decir:

Ahora se nos devuelve Font generation failed during processing., el mismo error de antes, así que probablemente hayamos salido a un directorio en el que ya no tenemos permisos de escritura, por lo que ../../ es el directorio más alto en el que tenemos permisos. Con esta info que vamos sacando podemos ir deduciendo dónde se están sirviendo los archivos.

Si probamos con filename="../../../../../../../../../../../tmp/shell.php sí funciona, así que tenemos permisos en /tmp. Esto significa que podemos saber nuestra distancia relativa a / quitando /..'s.

  • filename="../../../../tmp/shell.php funciona. -filename="../../../tmp/shell.php funciona. -filename="../../tmp/shell.php funciona. -filename="../tmp/shell.php funciona. -filename="./tmp/shell.php funciona.

Según esta lógica, el servidor compila y procesa nuestras fuentes desde /, lo que tiene 0 sentido, así que podemos deducir otra cosa que explica mejor la situación, y es que tenemos permisos de escritura en los directorios, y cuando ponemos pocos ../'s lo que sucede es que el servidor crea una carpeta tmp dentro de la ruta.

Como tenemos permisos para la ruta absoluta /tmp, necesitamos algo para lo que específicamente no tengamos permisos, como p.ej /bin. Si probamos con filename="/bin/shell.php veremos que no funciona, pero si lo hacemos con filename="../../bin/shell.php sí funciona, porque se crea el directorio. La idea es ver cuándo llegamos a /, para que se intente crear el /bin/shell.php, se nos deniegue el permiso, y entonces sepamos a cuánta distancia estamos de /.

Si probamos con filename="../../bin/shell.php funciona, pero si probamos con filename="../../../bin/shell.php no funciona, así que ya sabemos algo más:

  • La distancia relativa hasta el directorio raíz (/) es de 3.

Volviendo a pensar desde cero, si planteamos un directorio a 3 niveles de profundidad, contando con que nginx diferencia entre variatype.htb y portal.variatype.htb, posiblemente estemos ubicados en /var/www/variatype.htb o /var/www/portal.variatype.htb, el problema es que no tenemos permisos de escritura para ninguno de los dos, así que posiblemente el directorio con los elementos que realmente se están sirviendo está por debajo de estos.

Encontrando .git

Pasado un rato largo, pruebo a enumerar de nuevo ambos subdominios por si me había dejado algo, encuentro lo siguiente:

Y tenemos lo que necesitábamos. Aunque al solicitarlo no parece que tengamos permisos:

Al solicitar a algo que sepamos que existe:

Así que usamos una herramienta como git-dumper:

Entramos al repo y listamos commits, aunque de primeras en el escaneo de nmap habíamos visto que el último commit era remove hardcoded credentials, pero de todas formas encontramos lo siguiente:

Y tenemos unas credenciales gitbot:G1tB0t_Acc3ss_2025!, con las que accedemos al panel, y desde el que podemos ver todos los archivos .ttf creados anteriormente por nuestros intentos de explotar la vulnerabilidad:

Al lado aparecen unos botones de Download y View. Dado que este panel (como pone también) está intencionado para ser de uso interno únicamente, es posible que haya alguna vulnerabilidad de path traversal. Tras probar un rato:

Así que ahora podemos buscar la configuración de nginx para ver dónde se están guardando nuestros archivos realmente. Según Internet, esta se guarda en /etc/nginx/sites-available/<nombre_Dominio>:

Y ahí lo tenemos:

Nuestro payload debe ir a /var/www/portal.variatype.htb/public, así que ahí lo mandamos. Modificamos el .designspace:

Lo mandamos y miramos el directorio de nuevo:

Y tenemos el shell accesible. Ahora mandamos un reverse shell como el siguiente:

Desde BurpSuite:

Y en el handler en escucha:

Privesc: www-data -> steve

Listamos qué usuarios interactivos hay:

Así que parece que nuestro siguiente objetivo va a ser steve.

Si buscamos archivos que pertenezcan a steve:

En /opt parece destacar process_client_submissions.bak:

Si nos fijamos, vemos que hace lo siguiente:

  1. Va a $UPLOAD_DIR (Directorio en el que tenemos permisos de escritura)

  2. Busca archivos no vacíos con extensiones .ttf, .otf, etc.

  3. Pasa el nombre de cada archivo por un filtro, mandando los que tengan determinados caracteres a cuarentena.

  4. Si pasa el filtro, se ejecuta un código python.

Lo relevante aquí es que, una vez se ha pasado el filtro y se va a ejecutar el código Python, el código importa un módulo en un directorio que controlamos (y en el que podemos escribir), lo que hace esto potencialmente vulnerable a Python Library Hijacking.

Posible Python Library Hijacking

En el código python se hace lo siguiente al iniciar:

Dado que Python busca los módulos primero en el directorio de ejecución, si nosotros, en $UPLOAD_DIR, añadimos un fontforge.py malicioso, haremos que se ejecute en lugar del original.

Creamos el módulo malicioso:

Ahora creamos un .ttf cualquiera que no esté vacío en el directorio.

Pero si esperamos un rato, no pasa nada. Podemos usar pspy para ver si se ejecuta el script de forma automática cada rato, porque si no lo hace posiblemente tengamos que buscar una forma de hacer que se ejecute manualmente:

Y vemos que efectivamente se ejecuta de forma automatizada cada cierto tiempo, así que, si no nos funciona, es porque no es la vulnerabilidad que buscamos.

Buscando alternativas

Si podemos ver el script, y además se ejecuta periódicamente, casi seguro tiene que ser nuestra forma de escalar privilegios. Dado que ya hemos visto que fontools tenía una vulnerabilidad, podemos probar a ver si fontforge tiene otra. Tras una búsqueda:

FontForge has several vulnerabilities, including a critical remote code execution vulnerability related to SFD file parsing, which allows attackers to execute arbitrary code if a user interacts with a malicious file.

Si buscamos archivos de fontforge para ver la versión:

Ahí encontramos algún archivo relevante:

Buscamos a qué versión corresponde:

Y vemos que se trata de la versión 20230101, que, tras una búsqueda, vemos que tiene una vulnerabilidad de inyección de comandos: CVE-2024-25081/25082arrow-up-right.

Splinefont in FontForge through 20230101 allows command injection via crafted archives or compressed files.

De todas formas, primero deberíamos saber qué es FontForge, así que lo buscamos y tras una búsqueda encontramos esto:

FontForge is a free, open-source font editor used to create, edit, and convert outline and bitmap fonts (TrueType, OpenType, WOFF, etc.) on Windows, Mac, and Linux. It acts as a versatile tool for both custom typeface design and modifying existing fonts. It can run scripts from its GUI and from the command line, and also offers its features as a Python module.

Aprovechando el CVE, deberíamos poder fácilmente crear un nombre de archivo malicioso e inyectar comandos, pero tenemos el problema de la siguiente línea:

Si intentamos pasar cualquier cosa fuera de lo común, el archivo no va a llegar a FontForge, aunque lo que sí podemos hacer es aprovechar la parte de la vulnerabilidad que dice:

...allows command injection via crafted archives or compressed files.

Como veíamos en el script, también se admiten .zip, por lo que la idea será pasar un archivo .zip con un nombre que pase los filtros, para que luego FontForge lo descomprima, y dentro haya un archivo que sí explote la vulnerabilidad.

  1. Creamos el payload:

  1. Creamos el archivo malicioso:

  1. Intentamos crear el .zip

Dado que no hay zip en la máquina, lo hacemos en la nuestra y lo descargamos:

  1. Descargamos el .zip

Y finalmente ponemos nuestro puerto en escucha:

Vemos que la reverse shell 1 no ha durado mucho, pero cada vez que se ejecuta el script al parecer llega un nuevo shell. De todas formas podemos coger la siguiente (sessionID 2). Y tan pronto como la cogemos, para intentar conseguir un método de shell estable hacemos lo siguiente:

Así, si se cierra, podemos ir desde nuestro shell anterior como www-data y hacernos steve directamente:

En cualquier caso, somos steve.

Privesc: steve -> root

Ejecutamos sudo -l y vemos esto:

Si vemos qué hay en este script:

Vemos que el script hace lo siguiente en main():

  1. Comprueba que se pase un parámetro <PLUGIN_URL>

  2. Comprueba que la URL empiece por http:// o https://

  3. Comprueba que la URL no tenga más de 10 /

  4. Instala el plugin

En el proceso de instalación del plugin (install_validator_plugin(plugin_url)), el programa hace esto:

  1. Comprueba que exista el directorio PLUGIN_DIR, si no, lo crea.

  2. Descarga el archivo en un directorio determinado por los parámetros de la función (en este caso index.download(plugin_url, PLUGIN_DIR)), si miramos el código fuente de la función, vemos que tiene una forma similar a esto:

Aquí vemos que filename, el archivo en que se va a guardar, se determina usando os.path.join(tmpdir, name), que en nuestro caso corresponde a os.path.join(PLUGIN_DIR, <URL_Saneada>). Esta <URL_Saneada> corresponde a plugin_url con unas comprobaciones:

  1. Se toma la url, p.ej https://ejemplo.com/plugin16.zip, y se guarda plugin16 en la variable name

  2. Se reemplazan .. con . y \ con _. Esto bloquea intentos de escapar del directorio, pero no cuenta con / al principio, que definen rutas absolutas.

Si buscamos precisamente esto de las "rutas absolutas" en el filename, daremos con un CVE (CVE-2025-47273arrow-up-right) de Setuptools que habla justamente de esto. Esto se aprovecha de la funcionalidad de la función os.path.join(), que funciona de la siguiente manera (ejemplo):

  • os.path.join('/tmp/easy', '/etc/passwd') da como resultado '/etc/passwd'

CVE-2025-47273

Si pasamos una ruta absoluta a través de <URL_Saneada>, podremos guardar el archivo donde queramos. Podemos, por ejemplo, crear un par de claves SSH y copiar la pública a /root/.ssh/authorized_keys.

Como la solicitud va a ser exactamente de /root/.ssh/authorized_keys, necesitamos crear la estructura de carpetas. Por suerte, basta con que estemos ubicados (en nuestra máquina) en /.../<Directorios_cualquiera>/.../root/.ssh/authorized_keys y sirvamos desde root/.ssh/authorized_keys. En otras palabras, no hace falta que creemos un authorized_keys en nuestro directorio absoluto /root/.ssh/authorized_keys, pues el server de Python hace algo similar a chroot.

Creamos la ruta y nos ponemos en escucha:

Ahora hacemos la solicitud:

Y vemos nuestro servidor:

Así que el archivo se ha guardado exitosamente, ahora simplemente usamos ssh:

Y tenemos root.

Última actualización