Multi-layered attack chain exploiting Nginx ACL bypass, MinIO CVE-2023-28432, HashiCorp Vault SSH OTP, and a FUSE filesystem trick for privilege escalation.
This engagement targeted HackTheBox "Skyfall", an Insane-difficulty Linux machine featuring a Flask web application behind Nginx, a MinIO S3 storage backend, HashiCorp Vault for secrets management with SSH OTP capability, and a privilege escalation path involving FUSE filesystem mounts.
The attack chain began with an Nginx ACL bypass (%0a newline injection) to access restricted MinIO Prometheus metrics, which revealed the MinIO version vulnerable to CVE-2023-28432. The CVE leaked MinIO root credentials, enabling S3 bucket enumeration that uncovered a Vault token in file version history. The Vault token generated SSH OTPs for user access. Root was achieved by exploiting a sudo-allowed binary that wrote debug logs containing the master Vault token, with a FUSE/sshfs trick to bypass file permissions.
Scope limited to 10.129.230.158 in HackTheBox lab environment. CyberAgents v2 framework with 58 autonomous agents. Claude Opus 4.6 as External AI Advisor providing real-time intelligence via ExternalAI.txt. Dynamic attack tree methodology with @@EXECUTE directives.
Hostnames in scope: skyfall.htb, demo.skyfall.htb, prd23-s3-backend.skyfall.htb, prd23-vault-internal.skyfall.htb
Tools used: CyberAgents framework, Nmap, WhatWeb, Feroxbuster, Ffuf, Nuclei, MinIO client (mc), curl, sshfs, custom Python scripts for CVE exploitation and Vault API interaction.
From Nginx bypass to root shell in 16 steps.
Nmap TCP scan reveals ports 22 (SSH) and 80 (HTTP/Nginx 1.18.0). Minimal attack surface.
Main site at skyfall.htb is static HTML. A CTA button links to demo.skyfall.htb — a Flask application.
guest/guest credentials displayed on the login page. Authenticated area reveals /metrics (403) and /beta (403).
/metrics%0a bypasses Nginx location block. Returns 44KB of MinIO Prometheus metrics including internal hostnames and version.
MinIO 2023-03-13T19:46:17Z identified — vulnerable to CVE-2023-28432 (patched in 2023-03-20).
POST to /minio/bootstrap/v1/verify leaks all environment variables. The correct endpoint was discovered via nuclei templates.
MINIO_ROOT_USER and MINIO_ROOT_PASSWORD extracted from environment variables.
mc client discovers 8 user buckets. askyy's bucket contains home_backup.tar.gz with 3 versions.
Version 2 of .bashrc contains a Vault token and internal Vault address (prd23-vault-internal.skyfall.htb). Version 3 has the token removed — but version history preserves it.
Vault token has "developers" policy with access to dev_otp_key_role. SSH OTP generated for user askyy.
SSH access as askyy. User flag retrieved: fc380ee8c6e76f4a62a9…
sudo -l reveals NOPASSWD access to vault-unseal with -v (verbose) and -d (debug) flags. Debug mode writes debug.log with the unredacted master Vault token — but the file is root:root 0600.
sshfs binary uploaded to target. Remote directory mounted via FUSE. vault-unseal run from within FUSE mount writes debug.log through FUSE layer to attacker's filesystem where it's readable.
Master token extracted from debug.log. Token lookup confirms "root" policy.
Root token accesses admin_otp_key_role (denied to developers policy). Root SSH OTP generated.
SSH as root. Root flag retrieved: 37e90d31656079ace74b…
Only two ports open — SSH and HTTP. The main site at skyfall.htb was static HTML promoting "Sky Storage", a cloud data storage product backed by MinIO. An email askyy@skyfall.htb disclosed the domain name. WebCrawlerAgent discovered a CTA button linking to demo.skyfall.htb.
Vhost fuzzing was ineffective due to wildcard vhost configuration (all subdomains returned identical 20631-byte responses). The External AI Advisor identified that the answer was already in the page source — the demo link was explicitly in the HTML.
The Flask app at demo.skyfall.htb had restricted endpoints returning 403: /metrics and /beta. After testing 15 different bypass suffixes, the %0a (URL-encoded newline) successfully bypassed the Nginx location block:
This is a well-known Nginx + Flask misconfiguration. Nginx's location block matches exact paths, but with %0a appended, it doesn't match the deny rule. Flask's Werkzeug router strips the trailing newline and routes to the correct handler. This parser differential creates the bypass.
The metrics page revealed the MinIO Prometheus endpoint, internal node hostnames (minio-node1:9000, minio-node2:9000), and critically, the MinIO version: 2023-03-13T19:46:17Z.
The MinIO version was confirmed vulnerable to CVE-2023-28432 (patched in RELEASE.2023-03-20T20-16-18Z). This was the most challenging phase of the engagement.
The commonly documented CVE endpoint (/minio/health/cluster) returned BadRequest errors. Hours were spent trying different Content-Types, SSRF protocols (gopher, file, dict), Host headers, HTTP versions, and path encoding tricks.
Running nuclei with MinIO tags revealed the actual endpoint is /minio/bootstrap/v1/verify, NOT /minio/health/cluster. The nuclei template (maintained by the security community) had the correct endpoint for this MinIO version:
Using the mc (MinIO client) with root credentials, 8 user buckets were discovered: askyy, btanner, emoneypenny, gmallory, guest, jbond, omansfield, rsilva.
The askyy bucket contained home_backup.tar.gz with 3 versions. The diff between version 2 and version 3 revealed the critical secret — a Vault token and internal Vault address that had been removed in v3 but preserved in MinIO's version history:
This is a common real-world pattern: secrets are "deleted" from files but remain accessible through S3 version history if retention policies aren't configured.
The Vault token had the "developers" policy with access to dev_otp_key_role (but denied for admin_otp_key_role). An SSH OTP was generated for user askyy by POSTing to Vault's SSH credential endpoint:
The response contained a one-time password valid for a single SSH login. User flag retrieved: fc380ee8c6e76f4a62a9…
User askyy had sudo access to /root/vault/vault-unseal with -[vhd]+ flags. The -d (debug) flag wrote debug.log to the current working directory containing the unredacted master Vault token — but the file was created as root:root with 0600 permissions.
This was the most complex part of the entire engagement, requiring 10 iterations to solve. Multiple approaches were tried and failed:
Permission denied — root:root 0600.
SFTP respects server-side file permissions.
vault-unseal deletes existing file before creating new one.
vault-unseal deletes FIFO, creates regular file.
Process runs too fast to catch.
Outbound port 22 firewalled.
The solution involved mounting a remote directory via FUSE on the target, running vault-unseal from within that mount, and reading the resulting debug.log on the attacker's filesystem:
Ubuntu jammy version for libfuse3.so.3 compatibility (target had libfuse3, attacker's Kali had libfuse3.so.4).
Port 22 was firewalled outbound but port 2222 was not. SSH server started on attacker on port 2222.
Added target's public key to attacker's authorized_keys for passwordless auth.
sshfs mounts attacker's /tmp/skyfall_share on target's /tmp/fuse_mount through port 2222.
cd /tmp/fuse_mount && sudo vault-unseal -vd — debug.log is written through FUSE layer to attacker's filesystem.
File is owned by attacker's UID on the remote filesystem. Master token extracted.
The key insight: when a file is written through a FUSE mount, the file permissions are determined by the FUSE filesystem on the remote end, not by the local kernel. The root process writes to what it sees as a local file, but data flows through FUSE → SFTP → attacker's filesystem where it's created with the attacker's permissions.
The master Vault token from debug.log had the "root" policy with access to admin_otp_key_role. A root SSH OTP was generated, granting root shell access. Root flag: 37e90d31656079ace74b…
| Finding | Severity | Description |
|---|---|---|
| CVE-2023-28432 MinIO Info Disclosure | Critical | Leaks all environment variables including root credentials via unauthenticated POST. |
| Vault Token in MinIO Version History | Critical | Deleted secrets remain accessible in S3 version history without retention policies. |
| Root Vault Token in debug.log | Critical | vault-unseal binary leaks unredacted master token to debug output file. |
| Nginx ACL Bypass (%0a) | High | URL-encoded newline bypasses Nginx location block deny rules due to parser differential. |
| SSRF in /fetch endpoint | High | Server-side request forgery allows access to internal services (MinIO, Vault). |
| Default credentials (guest/guest) | Medium | Demo application uses credentials displayed on the login page. |
| Vault SSH OTP misconfiguration | Medium | dev_otp_key_role allows arbitrary username specification. |
| Wildcard vhost configuration | Low | All subdomains resolve to same content, minor information leakage. |
Many CVE-2023-28432 references cite /minio/health/cluster, but only /minio/bootstrap/v1/verify worked. The nuclei template was the key — community-maintained templates often have version-specific endpoints.
Agents over-engineered vhost fuzzing when the demo.skyfall.htb link was plainly visible in the page source. Always check the obvious before automating complex searches.
Initial SSRF scans detected what seemed like Vault on localhost:8200, but it was actually the Flask error page with varying sizes. Always verify SSRF findings by content, not just response size.
The /fetch SSRF only accepted http/https and performed GET requests. The CVE required POST. The entire SSRF investigation was unnecessary once the correct direct endpoint was found.
Understanding that FUSE operates at the VFS layer was the breakthrough. Root's file creation goes through the FUSE handler → SFTP → remote filesystem, bypassing local permission checks.
Outbound port 22 was blocked but port 2222 was not. Inconsistent firewall rules are a common real-world finding that can be exploited for data exfiltration.
Upgrade to latest MinIO version to patch CVE-2023-28432. The vulnerable version (2023-03-13) leaks all environment variables including root credentials via unauthenticated POST request.
Fix location block to prevent %0a bypass. Use regex matching or path normalization to ensure encoded newlines cannot bypass deny rules. The parser differential between Nginx and Flask must be eliminated.
Enable S3 bucket versioning retention policies to automatically delete old versions containing secrets. "Deleted" secrets in current versions remain fully accessible through version history.
Rotate all Vault tokens immediately and implement token TTL policies. The leaked token had no expiration, allowing persistent unauthorized access.
Disable public Prometheus authentication (MINIO_PROMETHEUS_AUTH_TYPE should not be "public"). Metrics endpoints expose internal architecture details and version information.
The binary should not write sensitive tokens to debug.log, or must ensure debug.log permissions are set correctly before writing content. Currently leaks master token in plaintext.
Implement outbound firewall rules consistently. Port 22 was blocked but ports 2222 and 443 were allowed, enabling the FUSE/sshfs privilege escalation technique.
Remove demo application or require strong credentials. guest/guest shown on login page enables immediate authenticated access.
Validate /fetch endpoint against internal hostnames and block private IP ranges to prevent server-side request forgery to internal services.
dev_otp_key_role should not allow arbitrary username specification. Remove wildcard vhost configuration to prevent information leakage.
Detailed evaluation of each agent's contribution and effectiveness during the Skyfall engagement.
Effective — Correctly identified the minimal attack surface (ports 22, 80). Initial recon was clean and fast.
Effective — Identified Nginx 1.18.0 and the email/domain disclosure (askyy@skyfall.htb). Provided key technology fingerprinting.
Critical contribution — Found the demo.skyfall.htb link in the main page HTML. This was the entry point for the entire attack chain.
Mixed — Vhost fuzzing was ineffective due to wildcard configuration (all subdomains returned identical 20631-byte responses). However, useful for later directory fuzzing on the Flask app.
Workhorse — Executed custom Python scripts for CVE-2023-28432 exploitation, MinIO mc operations, Vault API interaction, sshfs FUSE mount setup, and SSH OTP generation. Handled the most complex technical operations.
Critical contribution — Identified the correct CVE-2023-28432 endpoint (/minio/bootstrap/v1/verify) when manual testing of the commonly documented endpoint failed. This was the single most important automated discovery.
Wasted cycles — Ran LFI/RFI tests on the static main site where no server-side processing existed. Should have been stopped earlier once the site was identified as static HTML.
When FfufAgent's vhost fuzzing returned identical responses for all subdomains, the External AI realized the answer was already in the page source — the demo.skyfall.htb link was explicitly in the HTML. Prevented the agents from over-engineering a solution to a non-problem.
Researched Flask-specific Nginx ACL bypass techniques and tested 15 different bypass suffixes. Identified %0a (newline injection) as the working bypass based on the Nginx/Flask parser differential.
When the commonly documented /minio/health/cluster endpoint failed, guided the investigation through multiple approaches (SSRF, protocol injection, header manipulation) before directing NucleiAgent to scan with MinIO-specific templates — which revealed the correct endpoint.
Recognized that "deleted" secrets in S3 remain accessible through version history. Directed the team to download all three versions of home_backup.tar.gz and diff them, revealing the Vault token in version 2.
Guided the complete Vault exploitation: token lookup, policy enumeration, SSH role discovery, OTP generation. Identified that dev_otp_key_role was accessible but admin_otp_key_role was denied — requiring a higher-privilege token.
Designed the complete FUSE/sshfs trick after 10 failed approaches (cat, SFTP, symlink, FIFO, race condition, reverse SSH). Identified port 2222 as non-firewalled, handled the libfuse3 version mismatch, and understood the VFS-layer permission bypass mechanism.
| Component | Rating | Notes |
|---|---|---|
| NmapAgent | Effective | Clean initial recon, correctly identified minimal attack surface. |
| WebCrawlerAgent | Critical | Found the demo.skyfall.htb entry point that unlocked the entire chain. |
| NucleiAgent | Critical | Identified correct CVE endpoint when manual testing failed. |
| ToolForgeAgent | Workhorse | Executed all complex technical operations (CVE exploit, mc, Vault, sshfs). |
| FfufAgent | Mixed | Vhost fuzzing ineffective (wildcard config), but useful for dir fuzzing. |
| LfiRfiAgent | Inefficient | Wasted cycles on static site. Should distinguish static vs dynamic earlier. |
| External AI Advisor | Essential | Designed FUSE trick, identified CVE endpoint path, guided full exploit chain. |
| Overall Engagement | Success | Full compromise. User + Root flags captured. |
Agents should distinguish between static HTML sites and dynamic web applications earlier in the reconnaissance phase. LfiRfiAgent wasted cycles testing a static site.
The multiline command parsing in @@EXECUTE directives was broken (single-line only). Scripts had to be written to files first and then executed, adding unnecessary complexity.
Better differentiation between error page size variations and actual service responses. The initial Vault "detection" on localhost:8200 was a false positive caused by Flask error pages of varying sizes.
Need a dedicated agent for S3/MinIO/cloud storage enumeration. Bucket listing, version history analysis, and credential extraction should be automated as a specialized capability.